Skip to content

feat: add self-update foundations#281

Merged
munezaclovis merged 5 commits into
mainfrom
feat/self-update-foundations
Jun 11, 2026
Merged

feat: add self-update foundations#281
munezaclovis merged 5 commits into
mainfrom
feat/self-update-foundations

Conversation

@munezaclovis

@munezaclovis munezaclovis commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Document the v1 PV app update manifest schema and the PR22B-only implementation boundary in DESIGN.md.
  • Add a typed self-update manifest parser with stable-channel validation, current-platform asset selection, checksum/URL/size/timestamp validation, and compatibility checks.
  • Add app release layout helpers plus an OS-level update lock that rejects foreground and background mutating daemon reconciliation while keeping health available.

Out of scope

  • No pv update, pv update --check, binary download/swap, daemon restart coordination, rollback, installer/release metadata generation, Managed Resource update orchestration, artifact recipe changes, or runtime channel selection.

Verification

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • cargo nextest run -p self-update -p state -p daemon --all-features --locked
  • cargo insta pending-snapshots --workspace
  • git diff --cached --check

Summary by CodeRabbit

  • New Features

    • Added self-update infrastructure with manifest parsing and validation for app release management.
    • Implemented update lock mechanism to prevent concurrent update operations.
    • Daemon reconciliation now respects active updates, deferring work when updates are in progress.
  • Chores

    • Extended workspace configuration to include self-update crate.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c43f330-ae8d-4b42-a3d4-1c875f606e3f

📥 Commits

Reviewing files that changed from the base of the PR and between 717ceff and 3e59305.

📒 Files selected for processing (4)
  • crates/daemon/src/jobs.rs
  • crates/daemon/src/managed_resources/tests.rs
  • crates/daemon/src/reconciliation.rs
  • crates/state/src/update_lock.rs
💤 Files with no reviewable changes (1)
  • crates/state/src/update_lock.rs

📝 Walkthrough

Walkthrough

This PR establishes the self-update subsystem foundation with manifest parsing, binary release management, and concurrent-update protection. It adds the crates/self-update crate for validating app-update manifests, extends crates/state with filesystem utilities and an RAII update lock, wires the lock into daemon job execution to prevent concurrent updates, and updates DESIGN.md with the manifest specification.

Changes

Self-update foundation implementation

Layer / File(s) Summary
Design specification and workspace setup
DESIGN.md, Cargo.toml
Specifies the v1 manifest JSON schema with validation rules, publication endpoints, and implementation scope. Registers the new crate in the workspace and adds fs feature to rustix for filesystem operations.
Manifest types, parsing, and validation
crates/self-update/Cargo.toml, crates/self-update/src/lib.rs, crates/self-update/tests/app_update_manifest.rs
Introduces AppUpdateManifest, AppUpdateVersion, AppUpdateAsset, and AppUpdatePlatform types with a JSON parser that enforces schema version, channel, version bounds, asset validation (HTTPS URLs, non-zero size, no duplicate platforms), and RFC3339 timestamps. Comprehensive tests validate error reporting and platform selection.
Filesystem utilities and state model
crates/state/src/fs.rs, crates/state/src/error.rs, crates/state/src/paths.rs, crates/state/src/lib.rs
Adds atomic file copy, symlink operations, directory syncing, and path helpers for release artifacts. Extends StateError with variants for invalid release versions, missing releases, invalid symlink targets, and concurrent update conflicts.
Release binary installation and activation
crates/state/src/app_release.rs, crates/state/tests/state_foundation.rs
Implements AppReleaseLayout for installing versioned PV binaries with executable permissions via atomic copy, activating releases by swapping the active symlink, and resolving the current active release. Validates version format (major.minor.patch) and rejects path-traversal attack patterns. Tests verify installation, activation, and rejection of unsafe versions.
Update lock primitive
crates/state/src/update_lock.rs, crates/state/tests/state_foundation.rs
Implements UpdateLock as an RAII guard holding a non-blocking exclusive file lock. Acquisition fails with UpdateInProgress error when a lock is already held, enabling detection of concurrent update attempts. Tests verify mutual exclusion and lock reacquisition after release.
Queue and daemon job integration with update lock
crates/daemon/src/reconciliation.rs, crates/daemon/src/jobs.rs, crates/daemon/tests/daemon_foundation.rs, crates/daemon/src/managed_resources/tests.rs
Wires update lock into reconciliation queue via enqueue_mutating_with_abandon, acquiring the lock during job enqueueing and releasing when idle. Daemon job execution detects lock conflicts and reports UpdateInProgress to callers. Tests validate lock behavior during background reconciliation and mutating job execution alongside health checks. Port-binding helpers reduce timing dependency in resource tests.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant AppUpdateManifest
  participant Validation
  participant Conversion
  Caller->>AppUpdateManifest: parse(json)
  AppUpdateManifest->>AppUpdateManifest: deserialize raw
  AppUpdateManifest->>Validation: enforce schema_version
  Validation->>Validation: enforce channel="stable"
  Validation->>Validation: parse and bounds-check versions
  Validation->>Conversion: parse assets
  Conversion->>Conversion: validate URLs (https, host, filename)
  Conversion->>Conversion: enforce asset size > 0
  Conversion->>Conversion: reject duplicate platforms
  Conversion-->>AppUpdateManifest: return typed manifest
  AppUpdateManifest-->>Caller: Result<AppUpdateManifest>
Loading
sequenceDiagram
  participant AppReleaseLayout
  participant FileSystem
  participant Symlink
  AppReleaseLayout->>AppReleaseLayout: validate version format
  AppReleaseLayout->>FileSystem: verify release binary exists
  AppReleaseLayout->>Symlink: create temporary symlink to new version
  Symlink->>FileSystem: atomic rename to "active"
  FileSystem->>FileSystem: sync bin directory
  FileSystem-->>AppReleaseLayout: sync complete
  AppReleaseLayout-->>AppReleaseLayout: success or InvalidAppReleasePointer
Loading
sequenceDiagram
  participant Client
  participant ReconciliationQueue
  participant DaemonJobs
  participant UpdateLock
  Client->>DaemonJobs: run_job(scope)
  DaemonJobs->>ReconciliationQueue: enqueue_mutating_with_abandon(scope)
  alt No existing lock
    ReconciliationQueue->>UpdateLock: acquire(paths)
    UpdateLock-->>ReconciliationQueue: UpdateLock guard
    ReconciliationQueue->>ReconciliationQueue: store in QueueState
    ReconciliationQueue-->>DaemonJobs: EnqueueResult
    DaemonJobs-->>Client: Ok(job_id)
  else Lock already held
    UpdateLock-->>ReconciliationQueue: Err(UpdateInProgress)
    ReconciliationQueue-->>DaemonJobs: Err(UpdateInProgress)
    DaemonJobs->>DaemonJobs: write error over transport
    DaemonJobs-->>Client: error response
  end
  alt On queue idle
    ReconciliationQueue->>UpdateLock: drop guard (release_update_lock_if_idle)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prvious/pv#246: Both PRs modify the daemon's reconciliation job execution path in crates/daemon/src/jobs.rs by extending how reconciliation jobs are enqueued and completed with lock-aware semantics.
  • prvious/pv#240: Both PRs modify reconciliation job plumbing in crates/daemon/src/jobs.rs and the reconciliation flow; the update-lock-aware changes in this PR build upon the supervisor foundation introduced in that PR.

Poem

🐰 A rabbit's ode to the self-update foundation

Manifest parsed with validated care,
Symlinks swap with atomic flair,
Update locks stand guard at the gate,
Preventing races, keeping safe state—
Now PV daemons can rest at ease,
With concurrent updates at peace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add self-update foundations' accurately describes the main changes—adding manifest parsing, app release layout helpers, and update lock infrastructure for self-update support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/self-update-foundations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq

codspeed-hq Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing feat/self-update-foundations (3e59305) with main (47d0048)

Open in CodSpeed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

This PR leaves two self-update foundations unsafe for merge: daemon reconciliation is not mutually excluded for the update window, and installed release binaries may not be executable.

Reviewed changes — This PR adds the stable app update manifest parser, PV app release layout helpers, and daemon update-lock checks needed for later pv update work.

  • Document the v1 app update contractDESIGN.md now scopes the manifest to stable-channel PV app releases with exact macOS platform assets and explicit out-of-scope update behavior.
  • Add self-update manifest parsingcrates/self-update validates schema, channel, versions, timestamps, URLs, checksums, sizes, duplicate platforms, and platform selection with snapshot coverage.
  • Add release layout and locking primitivesstate now exposes AppReleaseLayout, active PV binary symlink helpers, app release paths, filesystem helpers, and UpdateLock at ~/.pv/run/update.lock.
  • Gate daemon reconciliation during updates — foreground and background reconciliation paths now reject requests when the update lock is already held while keeping daemon health available.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT𝕏

Comment thread crates/daemon/src/server.rs Outdated
Comment thread crates/state/src/app_release.rs

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

This incremental update fixes the prior lock lifetime issue, but it introduces a concurrency regression for normal daemon reconciliation requests.

Reviewed changes — This run reviewed the updates since the prior Pullfrog review, focused on lock lifetime and release binary executability fixes.

  • Held daemon mutation locks longerrun_job and background reconciliation now acquire UpdateLock guards and hold them across queued/running reconciliation work.
  • Expanded daemon lock coverage — new daemon tests cover lock-held request rejection and queued background reconciliation reserving the update lock.
  • Made installed app binaries executableAppReleaseLayout::install_release_binary now secures copied release binaries with executable permissions, and state tests assert mode 700.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT𝕏

Comment thread crates/daemon/src/server.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/daemon/tests/daemon_foundation.rs (1)

111-149: ⚡ Quick win

Add a same-daemon contention regression.

This only proves that an externally held update lock rejects run_job. It will still pass if one daemon reconciliation holds UpdateLock and a second daemon request starts failing early instead of reaching the queue/coalescing path, which is the more fragile contract here. Please add a two-request case that keeps the first reconcile alive and asserts the second is queued/coalesced rather than rejected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/daemon/tests/daemon_foundation.rs` around lines 111 - 149, Extend the
test update_lock_rejects_mutating_jobs_but_keeps_health_available to add a
same-daemon contention scenario: start a RunningDaemon, acquire an UpdateLock in
test harness, then send two "run_job" reconcile requests via request_lines where
the first request is kept alive (simulate a long-running reconcile—e.g., send a
reconcile that blocks or uses a job ID you can inspect) and the second request
is issued while the first is active; assert that the second is queued or
coalesced (not rejected) by checking daemon's job queue/state (use
database.recent_jobs() and the existing run_job_lines/normalize_update_lock_path
helpers) and adjust assertions to expect a queued/coalesced result for the
second request instead of an immediate rejection while preserving the existing
health check assertions.
crates/state/src/update_lock.rs (1)

33-38: Limit exposure of UpdateLock::check_available (or return the guard)

check_available() immediately drops the lock guard after checking, and it’s only declared in crates/state/src/update_lock.rs (no other call sites found). Consider making it pub(crate)/test-only, or have it return UpdateLock so callers must hold the reservation through the mutation window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/state/src/update_lock.rs` around lines 33 - 38, The check_available
function currently acquires then immediately drops the UpdateLock guard (in
UpdateLock::check_available using PvPaths), which either should be restricted or
should return the guard so callers must hold the reservation; fix by either
making check_available non-public (change pub fn check_available to pub(crate)
or annotate as #[cfg(test)] for test-only) if it’s only used internally, or
change its signature to return Result<UpdateLock, StateError> (i.e., return the
acquired lock instead of dropping it) so callers retain the UpdateLock guard
during the mutation window; update call sites/tests accordingly to use the
returned UpdateLock when chosen.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/daemon/src/server.rs`:
- Around line 146-167: handle_connection currently acquires UpdateLock
(UpdateLock::acquire) before calling run_job, which causes concurrent in-process
requests to fail with StateError::UpdateInProgress instead of allowing
enqueue_reconciliation_job to return EnqueueResult::Coalesced; fix by deferring
the UpdateLock acquisition until after the reconciliation job has been
enqueued/coalesced (i.e., remove the upfront UpdateLock::acquire in
handle_connection and instead acquire the lock inside run_job or just before the
actual update work after enqueue_reconciliation_job returns a non-coalesced
result), or make UpdateLock reuse a single process-wide file descriptor so
multiple acquires in the same process do not trigger EWOULDBLOCK; update
run_job/enqueue_reconciliation_job logic accordingly to ensure coalescing occurs
before taking the exclusive filesystem lock.

---

Nitpick comments:
In `@crates/daemon/tests/daemon_foundation.rs`:
- Around line 111-149: Extend the test
update_lock_rejects_mutating_jobs_but_keeps_health_available to add a
same-daemon contention scenario: start a RunningDaemon, acquire an UpdateLock in
test harness, then send two "run_job" reconcile requests via request_lines where
the first request is kept alive (simulate a long-running reconcile—e.g., send a
reconcile that blocks or uses a job ID you can inspect) and the second request
is issued while the first is active; assert that the second is queued or
coalesced (not rejected) by checking daemon's job queue/state (use
database.recent_jobs() and the existing run_job_lines/normalize_update_lock_path
helpers) and adjust assertions to expect a queued/coalesced result for the
second request instead of an immediate rejection while preserving the existing
health check assertions.

In `@crates/state/src/update_lock.rs`:
- Around line 33-38: The check_available function currently acquires then
immediately drops the UpdateLock guard (in UpdateLock::check_available using
PvPaths), which either should be restricted or should return the guard so
callers must hold the reservation; fix by either making check_available
non-public (change pub fn check_available to pub(crate) or annotate as
#[cfg(test)] for test-only) if it’s only used internally, or change its
signature to return Result<UpdateLock, StateError> (i.e., return the acquired
lock instead of dropping it) so callers retain the UpdateLock guard during the
mutation window; update call sites/tests accordingly to use the returned
UpdateLock when chosen.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba216143-50f4-4725-a1c6-1267d35bd277

📥 Commits

Reviewing files that changed from the base of the PR and between 47d0048 and 717ceff.

⛔ Files ignored due to path filters (6)
  • Cargo.lock is excluded by !**/*.lock
  • crates/daemon/tests/snapshots/daemon_foundation__update_lock_rejects_mutating_jobs_but_keeps_health_available.snap is excluded by !**/*.snap
  • crates/self-update/tests/snapshots/app_update_manifest__app_update_manifest_parses_stable_release_and_selects_platform.snap is excluded by !**/*.snap
  • crates/self-update/tests/snapshots/app_update_manifest__app_update_manifest_rejects_invalid_asset_fields.snap is excluded by !**/*.snap
  • crates/self-update/tests/snapshots/app_update_manifest__app_update_manifest_rejects_schema_channel_version_and_compatibility_errors.snap is excluded by !**/*.snap
  • crates/self-update/tests/snapshots/app_update_manifest__app_update_manifest_selection_reports_missing_platform.snap is excluded by !**/*.snap
📒 Files selected for processing (15)
  • Cargo.toml
  • DESIGN.md
  • crates/daemon/src/jobs.rs
  • crates/daemon/src/server.rs
  • crates/daemon/tests/daemon_foundation.rs
  • crates/self-update/Cargo.toml
  • crates/self-update/src/lib.rs
  • crates/self-update/tests/app_update_manifest.rs
  • crates/state/src/app_release.rs
  • crates/state/src/error.rs
  • crates/state/src/fs.rs
  • crates/state/src/lib.rs
  • crates/state/src/paths.rs
  • crates/state/src/update_lock.rs
  • crates/state/tests/state_foundation.rs

Comment thread crates/daemon/src/server.rs Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — This run reviewed the daemon queue-lock update added since the prior Pullfrog review, focused on preserving reconciliation coalescing while maintaining self-update exclusion.

  • Moved daemon mutation locking into ReconciliationQueueenqueue_mutating_with_abandon now keeps a single daemon-owned UpdateLock across queued and running reconciliation work instead of requiring each same-daemon request to acquire the OS lock before queueing.
  • Preserved same-daemon reconciliation behavior — foreground and background reconciliation can now continue queueing or coalescing while an existing daemon reconciliation owns the update lock.
  • Added a coalescing regression testbackground_reconciliation_coalesces_under_daemon_update_lock covers a queued background reconciliation receiving a duplicate request while the daemon-held update lock is active.

Pullfrog  | View workflow run | Using GPT𝕏

@pullfrog

pullfrog Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Fixed the RustFS CI failure by making rustfs_port_reassignment_renders_current_endpoint_for_ready_allocation wait until the stopped runtime releases its ports before rebinding blockers, eliminating the macOS socket-release race seen as Address already in use. Pushed commit 3e59305 to the existing PR branch. Verification: cargo fmt --all -- --check passed; the exact/focused test command could not run on this Linux runner because netstat-esr fails to compile with ambiguous __be16, matching the repo’s known Linux limitation.

Task list (8/8 completed)
  • Check out PR branch
  • Fetch failed check suite logs
  • Confirm failure was introduced by PR
  • Reproduce CI failure locally
  • Implement minimal fix
  • Verify with exact CI command
  • Review diff and commit fix
  • Push branch and report outcome

Pullfrog  | View workflow run | via Pullfrog | Using GPT𝕏

@munezaclovis
munezaclovis merged commit 17c4515 into main Jun 11, 2026
4 checks passed
@munezaclovis
munezaclovis deleted the feat/self-update-foundations branch June 11, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant