Repository navigation
Ship the desktop app as a signed DMG that updates itself - #801
Conversation
Velopack packs and updates the bundle; the daemon is signed before its digest is computed so the CLI's embedded check survives signing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The daemon already restarts itself when its binary changes, so the app's update flow only holds the skew dialog for a grace window instead of owning the restart. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A failed apply relaunches the old app with the same restart marker and package, so an unguarded startup apply would loop before any UI appeared. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two runs of one version can each win a different channel; the app job now proves its bytes are the ones on the registry before it uploads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Velopack's Run() must be the first statement of Main: its hooks exit from inside it, and auto-apply stays off so the coordinator applies packages after the install-location guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IAppUpdater/UpdateCandidate/InertAppUpdater keep Velopack types out of the coordinator; PrereleaseFilteringSource evaluates the installed version per feed read so a stable install never sees a beta. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Update ready keeps the existing decline button ("Not now"); update
info is acknowledge-only like quarantine, so ShowDeclineButton now
excludes both kinds.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Serializes checks onto one in-flight task so a scheduled tick and a manual click never race, and reads the shared ready-candidate through the same lock every other mutable field in the class already uses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RunCheckAsync holds _lock across its synchronous prefix, through _menu.OnNext and into ConfirmAsync; a UI-thread Ready read via that same lock could deadlock against a blocking dialog. Volatile.Read/Write keep the single write serialized by _inflight while UI reads never wait. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TrayMenuModel and TrayViewModel gain trailing UpdateItemLabel/updateMenu/ updateAction parameters so a live update coordinator can be wired in later without touching either type's existing call sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The updater's own wait is bounded to 60 s for process exit, so it must fire after every disposal and immediately before TryShutdown, guarded so a throw there still lets shutdown complete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The daemon's own restart coordinator replaces its binary within a poll interval once idle, so a takeover accepted mid-hold must revalidate against the daemon's now-current version rather than reinstall it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Promotion uses renamex_np(RENAME_EXCL) rather than a plain rename, so an existing destination — even an empty directory — is never replaced by the move. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MoveAsync's catch previously excluded OperationCanceledException, so a cancel mid-ditto or mid-verification left a real <name>.staging-<guid> directory in /Applications. It now cleans up and rethrows, since the caller is shutting down and must not see a fabricated moved/failed outcome. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The install-location guard and pending-apply check run before the daemon graph exists, so a stale package on disk is applied (or the guard shown) before anything else is built rather than racing it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OnExplicitShutdown means Avalonia never ends the process just because its last window closed, so the titlebar close (or Cmd+W) left a windowless process running. The guard flag also keeps the resulting reentry through Closed from calling Shutdown a second time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An out-of-contract core (extra segments, a leading zero) hit unguarded bash arithmetic that either errored past set -e or read as octal, silently reporting equal. semver_cmp now validates both cores first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both compare versions with semver_cmp rather than string equality, so a re-run or a late-published tag never regresses a kept baseline or a promoted DMG alias. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both refuse to let a rebuild silently overwrite what a previous run, or npm, already published under the same version tag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
None fall back to unsigned or unstamped output: every secret and argument is required, so a misconfigured run fails rather than shipping a partially-built artifact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Under set -e, a failing security/xcrun call previously skipped the inline rm -f and left a plaintext certificate or notary key on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The release wait now matches matrix-suffixed check names, so it actually gates on App bundle (osx-arm64) instead of a name it never sees. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The daemon must be signed before its digest is computed: signing rewrites its bytes, and the CLI embeds a digest of what it will actually launch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
app-publish runs in its own concurrency group because the R2 feed manifest is read-merge-write and cannot tolerate concurrent writers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A transient head-object/fetch failure previously read as "not published", letting vpk overwrite an already-published immutable nupkg. Both gates now distinguish a genuine 404 from any other error and refuse to proceed on the latter, mirroring the existing npm immutability guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ready-update dialog's TCS resolves on the thread pool, so TryShutdown (and the tray teardown it triggers) could run off the UI thread; also guards a throwing ApplyNow at startup and drops a duplicated hold comment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
set-key-partition-list is needed on a headless runner or codesign prompts/fails; the immutables fetch now surfaces the real R2 error instead of swallowing it; npm pack retries against registry propagation lag the same way verify-release-immutable already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
app-macos and app-publish had no job timeout, and notarytool --wait could hang indefinitely on an Apple-side stall. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
PR Summary by QodoDistribute the macOS app as a signed, self-updating DMG
AI Description
Diagram
High-Level Assessment
Files changed (78)
|
Code Review by Qodo
1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd64e17647
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| version="${1:?usage: render-info-plist.sh <version> <out-file>}" | ||
| out="${2:?usage: render-info-plist.sh <version> <out-file>}" | ||
| short="${version%%-*}"; short="${short%%+*}" | ||
| sed -e "s/{VERSION}/${version%%+*}/" -e "s/{SHORT_VERSION}/$short/" "$here/../src/Capacitor.App/Packaging/Info.plist" > "$out" |
There was a problem hiding this comment.
Generate a valid macOS bundle build number
For the planned prerelease tags, this substitutes values such as 0.12.0-beta.1 or 0.12.0-beta.1.N directly into CFBundleVersion. Apple restricts that key to a numeric, period-separated build version with only its documented compact suffix syntax, so the generated bundle metadata is invalid and may be rejected during signing/notarization or misread by Launch Services; plutil -lint checks only plist syntax and will not catch this. Derive a separate Apple-compatible build number while retaining the SemVer for Velopack and display purposes. See Apple's CFBundleVersion documentation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Keeping the SemVer in CFBundleVersion: Velopack's own plist writer does the same (CFBundleVersion = packVersion, CFBundleShortVersionString = the numeric core), as does electron-builder, and notarization validates signing, hardened runtime, timestamp and entitlements, not this key's grammar. Velopack does not read it, and the SemVer there is what lets a crash report name the exact beta. If the first tagged release is rejected after all, the spec's manual first-release gate catches it and the fix is one line in render-info-plist.sh.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Velopack's check has no cancellation overload; WaitAsync releases the coordinator while the request finishes on its own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NativeAOT does not lay a string literal out as plain UTF-16 on disk, so the byte search failed on a correct bundle; the gate's verdict is the evidence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AI-1653 — no GitHub issue exists for this slice (it was filed in Linear only), so the closing keyword is dropped.
What & why
The desktop app has only ever run from source behind a dev seam. This packages it as a signed, notarized macOS DMG that bundles
kcapandkcap-daemonbeside the app, publishes a self-hosted Velopack feed to R2 (served through the kurrent.io Worker), and lets the app update itself: a coordinator checks a prerelease-filtered feed on a schedule, downloads in the background, prompts once, and hands the bundle swap to Velopack as the last step of shutdown. The daemon restarts itself once idle after the swap, so the app only holds its skew dialog for a grace window instead of owning the restart.Where to look
The release matrix signs the daemon before its digest is computed and never re-signs it;
vpk packruns with deep signing disabled so the CLI's embedded digest survives. Publication is a separate serialized job that fails closed on anything but a 404 from R2 and proves its CLI and daemon bytes are the ones npm shipped. Before the first app release: cutv0.12.0-beta.1(the newApp bundle (osx-arm64)check is red until it exists), add the Apple signing/notary and R2 secrets, and ship the kcap-web/download/desktop/*route.Verification
dotnet run --project test/Capacitor.App.Tests.Unitcodex)bash scripts/run-shell-tests.shokdotnet publish -c Releasefor CLI and daemon