Skip to content

Ship the desktop app as a signed DMG that updates itself - #801

Merged
alexeyzimarev merged 45 commits into
mainfrom
alexeyzimarev/ai-1653-app-distribution-cli-bundling-signingnotarization-dmg-auto
Sep 9, 2026
Merged

alexeyzimarev merged 45 commits into
mainfrom
alexeyzimarev/ai-1653-app-distribution-cli-bundling-signingnotarization-dmg-auto

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

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 kcap and kcap-daemon beside 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 pack runs 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: cut v0.12.0-beta.1 (the new App 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

Check Result
dotnet run --project test/Capacitor.App.Tests.Unit 1495 passed
Core / CLI / Transcripts / Integration unit suites 2905 / 3996 / 126 / 245 passed
Daemon unit suite 2961 passed, 1 pre-existing environmental failure (Codex vendored pin vs a newer local codex)
bash scripts/run-shell-tests.sh 9 files ok
dotnet publish -c Release for CLI and daemon 0 IL2xxx/IL3xxx warnings
Signing, notarization, R2 upload not yet exercised: needs the secrets and the first tag

alexeyzimarev and others added 30 commits September 6, 2026 13:56
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>
alexeyzimarev and others added 9 commits September 7, 2026 00:02
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>
@linear-code

linear-code Bot commented Sep 7, 2026

Copy link
Copy Markdown

AI-1653

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-07T07:54:00.916669Z bd64e17 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Distribute the macOS app as a signed, self-updating DMG

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Packages and notarizes the macOS app with a byte-verified bundled CLI and daemon.
• Publishes an immutable Velopack feed and DMG aliases through serialized R2 release jobs.
• Adds prerelease-aware updates, install-location safeguards, and graceful daemon version-skew
 handling.
Diagram

graph TD
  A["Release Workflow"] --> B["Signed Trio"] --> C["Velopack Packages"] --> D[("R2 Feed")]
  C --> E["Notarized DMG"] --> F["Desktop App"] --> G["Update Coordinator"]
  D --> G
  G --> F
  F --> H["Bundled Daemon"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Sparkle 2 updater
  • ➕ Mature, macOS-native update framework
  • ➕ Established signing and update UX
  • ➖ Requires an Objective-C interoperability layer and bundled XPC framework
  • ➖ Mac-only design would not support the planned Windows distribution path
  • ➖ Adds native packaging complexity to the .NET application
2. Custom atomic updater
  • ➕ Complete control over feed format, rollback, and update behavior
  • ➕ Avoids a runtime dependency on Velopack
  • ➖ Requires owning privileged bundle replacement and rollback semantics
  • ➖ Expands the security-sensitive surface substantially
  • ➖ Duplicates packaging, delta, and platform lifecycle functionality
3. Separate app-only CLI build
  • ➕ Decouples Apple certificate availability from npm publishing
  • ➕ Allows app-specific signing or packaging choices
  • ➖ Creates two signed byte sets and daemon digests for one version
  • ➖ Increases AOT build cost and provenance complexity
  • ➖ Weakens the guarantee that npm and the desktop app ship identical binaries

Recommendation: Keep the current Velopack approach and single signed trio. It provides atomic replacement, static-feed support, delta packaging, and a future Windows path while the explicit signing order and npm hash gate preserve the CLI-to-daemon digest invariant. Sparkle and a custom updater add disproportionate native or security complexity, while a second binary build undermines release provenance.

Files changed (78) +6671 / -55

Enhancement (27) +739 / -43
App.axamlSet the macOS application display name +1/-0

Set the macOS application display name

• Names the Avalonia application Kurrent Capacitor so development and packaged application menus use the product identity.

src/Capacitor.App/App.axaml

App.axaml.csIntegrate install guarding and self-update lifecycle +134/-11

Integrate install guarding and self-update lifecycle

• Enforces a stable Applications location, initializes the updater, applies eligible cached updates safely, and wires update scheduling into prompts and the tray. Pending updates are handed to Velopack only after daemon teardown and immediately before process shutdown.

src/Capacitor.App/App.axaml.cs

kcap-icon.icnsAdd the macOS bundle icon +0/-0

Add the macOS bundle icon

• Adds the multi-resolution ICNS asset used by the packaged app and DMG.

src/Capacitor.App/Assets/kcap-icon.icns

kcap-icon.pngReplace the app icon with a high-resolution product mark +0/-0

Replace the app icon with a high-resolution product mark

• Replaces the previous 96-pixel bitmap with the rendered 512-pixel icon used by existing app surfaces.

src/Capacitor.App/Assets/kcap-icon.png

kcap-icon.svgAdd the canonical application icon source +6/-0

Add the canonical application icon source

• Adds the scalable Kurrent Capacitor product mark from which PNG and ICNS assets are generated.

src/Capacitor.App/Assets/kcap-icon.svg

Program.csBootstrap Velopack before Avalonia +16/-2

Bootstrap Velopack before Avalonia

• Runs Velopack lifecycle hooks first, disables automatic startup application, and records update relaunches for guarded pending-apply and skew handling.

src/Capacitor.App/Program.cs

ApplicationsMover.csMove app bundles into Applications without replacement +63/-0

Move app bundles into Applications without replacement

• Copies through a verified staging bundle and atomically promotes it with macOS RENAME_EXCL. Existing destinations, partial copies, cancellation, and failures leave the final path untouched.

src/Capacitor.App/Services/ApplicationsMover.cs

CliResolver.csResolve the CLI bundled beside the app +10/-9

Resolve the CLI bundled beside the app

• Adds a bundle-sibling lookup between the explicit development override and PATH fallback, enabling stable absolute paths for the shim and daemon service.

src/Capacitor.App/Services/CliResolver.cs

DaemonLifecycleController.csDelay stale daemon skew prompts after updates +49/-2

Delay stale daemon skew prompts after updates

• Holds version-skew handling for 45 seconds after an update relaunch so an idle daemon can restart itself. It retains incompatible evidence and revalidates that versions still differ before replacing the service.

src/Capacitor.App/Services/DaemonLifecycleController.cs

ILifecycleSurface.csAdd software-update prompt kinds +2/-0

Add software-update prompt kinds

• Defines update-ready and informational prompt identifiers for the shared serialized lifecycle surface.

src/Capacitor.App/Services/ILifecycleSurface.cs

InstallLocation.csClassify safe macOS bundle locations +31/-0

Classify safe macOS bundle locations

• Finds the containing app bundle and distinguishes system or user Applications installs from DMG, translocated, and other unstable locations.

src/Capacitor.App/Services/InstallLocation.cs

IAppUpdater.csDefine the application updater boundary +21/-0

Define the application updater boundary

• Introduces a Velopack-independent interface for availability, checking, downloading, pending packages, and immediate or shutdown-time application.

src/Capacitor.App/Services/Update/IAppUpdater.cs

InertAppUpdater.csProvide a no-op updater for unpackaged runs +14/-0

Provide a no-op updater for unpackaged runs

• Keeps source runs and updater-construction failures operational without exposing update actions.

src/Capacitor.App/Services/Update/InertAppUpdater.cs

PrereleaseFilteringSource.csFilter prereleases based on the installed channel +22/-0

Filter prereleases based on the installed channel

• Wraps the Velopack source so stable installations ignore prerelease assets while prerelease installations continue receiving all releases.

src/Capacitor.App/Services/Update/PrereleaseFilteringSource.cs

UpdateCandidate.csModel an updater-agnostic release candidate +5/-0

Model an updater-agnostic release candidate

• Adds a version and prerelease record that prevents Velopack asset types from leaking into coordination logic.

src/Capacitor.App/Services/Update/UpdateCandidate.cs

UpdateCoordinator.csCoordinate scheduled checks and update application +186/-0

Coordinate scheduled checks and update application

• Adds single-flight scheduled and manual checks, background downloads, one ready prompt, terminal ready state, and tray relabeling. It validates cached startup packages and defers requested updates until the last shutdown step.

src/Capacitor.App/Services/Update/UpdateCoordinator.cs

UpdateMenuItem.csModel update tray-item state +4/-0

Model update tray-item state

• Represents whether the update action is visible and whether it checks or requests a restart.

src/Capacitor.App/Services/Update/UpdateMenuItem.cs

VelopackAppUpdater.csAdapt Velopack to the updater boundary +51/-0

Adapt Velopack to the updater boundary

• Wraps UpdateManager and the prerelease-filtered web source, tracks checked assets, downloads packages, and applies them immediately or after process exit.

src/Capacitor.App/Services/Update/VelopackAppUpdater.cs

LifecyclePromptViewModel.csPresent update-ready and update-info dialogs +10/-3

Present update-ready and update-info dialogs

• Maps update prompts to appropriate titles, acknowledgment behavior, and Restart now or Not now actions.

src/Capacitor.App/ViewModels/LifecyclePromptViewModel.cs

TrayModels.csCarry the update action label in tray state +4/-1

Carry the update action label in tray state

• Extends the tray menu model with an optional coordinator-owned update label.

src/Capacitor.App/ViewModels/TrayModels.cs

TrayViewModel.csProject updater state into the tray menu +23/-10

Project updater state into the tray menu

• Combines update visibility and labels with existing tray state and exposes a reactive command for the coordinator's check-or-restart action.

src/Capacitor.App/ViewModels/TrayViewModel.cs

TrayMenuBuilder.csRender the desktop update menu item +7/-1

Render the desktop update menu item

• Adds the coordinator-controlled Check for Updates or Restart to update item when updater state makes it available.

src/Capacitor.App/Views/TrayMenuBuilder.cs

DaemonRunner.csAnswer daemon version checks before initialization +10/-0

Answer daemon version checks before initialization

• Adds an exact --version fast path used to smoke-test signed daemon binaries without configuration or profile setup.

src/Capacitor.Cli.Daemon/DaemonRunner.cs

StatusCommand.csIdentify app-bundled CLI status output +12/-1

Identify app-bundled CLI status output

• Marks the status version line as bundled and suppresses npm update advisory work for app-owned installations.

src/Capacitor.Cli/Commands/StatusCommand.cs

UpdateCommand.csDelegate bundled CLI updates to the desktop app +26/-1

Delegate bundled CLI updates to the desktop app

• Makes bundled update commands return app-specific guidance instead of contacting npm. Check-only output preserves its JSON contract while reporting no newer npm update and an app install tag.

src/Capacitor.Cli/Commands/UpdateCommand.cs

InstallProvenance.csDetect CLI execution inside an app bundle +24/-0

Detect CLI execution inside an app bundle

• Adds cached detection based on the .app/Contents/MacOS path shape and adjacent Info.plist.

src/Capacitor.Cli/InstallProvenance.cs

UpdateNotice.csSuppress npm notices for bundled CLIs +8/-2

Suppress npm notices for bundled CLIs

• Short-circuits human-facing update notices when installation provenance shows the desktop app owns updates.

src/Capacitor.Cli/UpdateNotice.cs

Tests (25) +1094 / -9
assert-app-cli-version.test.shTest bundled CLI version validation +31/-0

Test bundled CLI version validation

• Covers matching versions, build metadata, floor precedence, malformed versions, mismatches, and invalid command output.

scripts/assert-app-cli-version.test.sh

assert-bundle-digest.test.shTest packed bundle digest verification +37/-0

Test packed bundle digest verification

• Exercises matching binaries, substituted daemon bytes, placeholder digests, and missing embedded digests.

scripts/assert-bundle-digest.test.sh

desktop-baseline.test.shTest delta baseline selection +32/-0

Test delta baseline selection

• Covers lower, equal, higher-prerelease, and missing baseline packages using fixture nupkg files.

scripts/desktop-baseline.test.sh

semver.test.shTest release-script SemVer behavior +47/-0

Test release-script SemVer behavior

• Covers stable and prerelease ordering, numeric identifiers, MinVer heights, build metadata, and malformed version rejection.

scripts/lib/semver.test.sh

promote-desktop-aliases.test.shTest stable and beta alias promotion +36/-0

Test stable and beta alias promotion

• Verifies newest, late, stable, prerelease, and missing-candidate publication scenarios.

scripts/promote-desktop-aliases.test.sh

run-shell-tests.shRun all release shell tests +11/-0

Run all release shell tests

• Discovers script and library test files, executes all of them, and reports a failing aggregate status.

scripts/run-shell-tests.sh

verify-desktop-immutables.test.shTest immutable desktop publication checks +28/-0

Test immutable desktop publication checks

• Covers absent objects, identical retries, conflicting bytes, and indeterminate fetch failures.

scripts/verify-desktop-immutables.test.sh

verify-npm-trio.test.shTest npm binary identity verification +27/-0

Test npm binary identity verification

• Covers matching tarballs, CLI or daemon substitutions, and missing package archives.

scripts/verify-npm-trio.test.sh

AppStartupTests.csTest final-step update handoff during shutdown +29/-0

Test final-step update handoff during shutdown

• Verifies updates are applied after disposal but before platform shutdown, and that updater exceptions cannot block shutdown.

test/Capacitor.App.Tests.Unit/AppStartupTests.cs

ApplicationsMoverTests.csTest safe promotion into Applications +134/-0

Test safe promotion into Applications

• Covers successful staging, incomplete copies, existing and racing destinations, cancellation cleanup, and macOS exclusive rename behavior.

test/Capacitor.App.Tests.Unit/ApplicationsMoverTests.cs

CliResolverTests.csTest bundle-relative CLI resolution +27/-4

Test bundle-relative CLI resolution

• Updates existing resolver cases and verifies sibling selection, override precedence, and PATH fallback.

test/Capacitor.App.Tests.Unit/CliResolverTests.cs

DaemonLifecycleControllerTests.csTest post-update daemon skew handling +94/-2

Test post-update daemon skew handling

• Covers delayed prompts, daemon self-restart during the grace window, retained incompatible evidence, immediate non-update behavior, and stale acceptance after versions converge.

test/Capacitor.App.Tests.Unit/DaemonLifecycleControllerTests.cs

FakeAppUpdater.csAdd a scriptable updater test double +39/-0

Add a scriptable updater test double

• Provides controllable check, download, pending-package, apply, failure, and invocation-count behavior for coordinator tests.

test/Capacitor.App.Tests.Unit/FakeAppUpdater.cs

InstallLocationTests.csTest macOS bundle location classification +43/-0

Test macOS bundle location classification

• Covers bundle-root extraction, system and user Applications paths, DMG volumes, translocation, other locations, and unpackaged runs.

test/Capacitor.App.Tests.Unit/InstallLocationTests.cs

LifecyclePromptViewModelTests.csTest update prompt presentation +26/-0

Test update prompt presentation

• Verifies ready prompts offer restart or deferral and informational prompts are acknowledgment-only.

test/Capacitor.App.Tests.Unit/LifecyclePromptViewModelTests.cs

PrereleaseFilteringSourceTests.csTest installed-channel feed filtering +28/-0

Test installed-channel feed filtering

• Confirms stable installs drop prereleases, prerelease installs retain them, and empty feeds remain empty.

test/Capacitor.App.Tests.Unit/PrereleaseFilteringSourceTests.cs

TrayAdapterTests.csTest update item menu rendering +42/-2

Test update item menu rendering

• Ensures the native tray omits hidden update state and binds visible labels to the update command.

test/Capacitor.App.Tests.Unit/TrayAdapterTests.cs

TrayViewModelTests.csTest reactive update tray state +37/-0

Test reactive update tray state

• Verifies updater observations drive the menu label and the update command invokes its injected action.

test/Capacitor.App.Tests.Unit/TrayViewModelTests.cs

UpdateCoordinatorTests.csTest update scheduling, prompting, and application +214/-0

Test update scheduling, prompting, and application

• Covers availability, timing, ready-state behavior, manual result reporting, single-flight checks, restart handoff, startup eligibility, relaunch-loop prevention, and apply failures.

test/Capacitor.App.Tests.Unit/UpdateCoordinatorTests.cs

UpdateFeedTests.csTest update feed URL resolution +21/-0

Test update feed URL resolution

• Verifies the production URL, explicit override, trimming, and blank-value fallback.

test/Capacitor.App.Tests.Unit/UpdateFeedTests.cs

DaemonVersionFlagTests.csTest the daemon version fast path +28/-0

Test the daemon version fast path

• Verifies exact --version output and rejects other argument combinations.

test/Capacitor.Cli.Daemon.Tests.Unit/DaemonVersionFlagTests.cs

InstallProvenanceTests.csTest app-bundled CLI detection +35/-0

Test app-bundled CLI detection

• Covers valid bundles, missing metadata, non-MacOS bundle locations, malformed shapes, and absent process paths.

test/Capacitor.Cli.Tests.Unit/InstallProvenanceTests.cs

StatusCommandVersionLineTests.csTest bundled status version formatting +11/-0

Test bundled status version formatting

• Confirms the version line identifies Kurrent Capacitor ownership without an npm advisory.

test/Capacitor.Cli.Tests.Unit/StatusCommandVersionLineTests.cs

UpdateCommandBundledTests.csTest bundled update command contracts +23/-0

Test bundled update command contracts

• Verifies the check-only JSON reports no newer version with app provenance and that interactive guidance points users to the app menu.

test/Capacitor.Cli.Tests.Unit/UpdateCommandBundledTests.cs

UpdateNoticeIsHumanFacingTests.csTest bundled update-notice suppression +14/-1

Test bundled update-notice suppression

• Confirms app-bundled commands are never considered human-facing update-notice targets while ordinary installations retain existing behavior.

test/Capacitor.Cli.Tests.Unit/UpdateNoticeIsHumanFacingTests.cs

Documentation (4) +4010 / -1
README.mdDocument macOS desktop installation and updates +10/-1

Document macOS desktop installation and updates

• Adds desktop platform requirements and explains downloading the DMG, moving the app to Applications, using the bundled CLI and daemon, and receiving updates through the app.

README.md

CHANGES.mdRecord the signed DMG and update architecture +20/-0

Record the signed DMG and update architecture

• Documents bundle composition, signing and digest ordering, npm byte identity, daemon restart ownership, and guarded startup application of cached updates.

docs/CHANGES.md

2026-09-06-ai1653-app-distribution.mdAdd the desktop distribution implementation plan +3714/-0

Add the desktop distribution implementation plan

• Provides the task-by-task implementation, testing, workflow, packaging, signing, publication, and documentation plan for the complete distribution slice.

docs/superpowers/plans/2026-09-06-ai1653-app-distribution.md

2026-09-06-ai1653-app-distribution-design.mdDefine the desktop distribution design +266/-0

Define the desktop distribution design

• Captures architectural decisions for Velopack, the shared signed binary trio, R2 hosting, update behavior, install-location enforcement, release safety, and first-release validation.

docs/superpowers/specs/2026-09-06-ai1653-app-distribution-design.md

Other (22) +828 / -2
ci.ymlBuild and validate an unsigned macOS app bundle in CI +102/-0

Build and validate an unsigned macOS app bundle in CI

• Runs the shell-script test suite and adds an osx-arm64 bundle job. The job builds the daemon, digest-bound CLI, and app at one version, packages them with Velopack, validates the bundle, and uploads the portable artifact.

.github/workflows/ci.yml

release.ymlSign, notarize, and publish desktop releases +276/-2

Sign, notarize, and publish desktop releases

• Signs the daemon before hashing it and signs the CLI afterward, preserving the embedded digest invariant. Adds serialized jobs that package and notarize the app and DMG, verify npm and R2 immutability, publish the Velopack feed and aliases, and attach the DMG to GitHub Releases.

.github/workflows/release.yml

Directory.Packages.propsPin Velopack 1.2.0 +1/-0

Pin Velopack 1.2.0

• Adds the centrally managed Velopack package version used by the desktop updater and matching release tool.

Directory.Packages.props

assert-app-cli-version.shValidate the bundled CLI version and compatibility floor +31/-0

Validate the bundled CLI version and compatibility floor

• Runs the bundled CLI version command, enforces exact output and expected SemVer, and rejects versions below the app's CLI compatibility floor.

scripts/assert-app-cli-version.sh

assert-bundle-digest.shVerify the packed daemon digest invariant +35/-0

Verify the packed daemon digest invariant

• Checks that the bundled daemon matches the recorded SHA-256 and that the bundled NativeAOT CLI embeds that digest instead of the placeholder.

scripts/assert-bundle-digest.sh

build-dmg.shBuild a drag-to-Applications DMG +12/-0

Build a drag-to-Applications DMG

• Copies the app into a staging directory, adds an Applications symlink, and creates a compressed Kurrent Capacitor disk image.

scripts/build-dmg.sh

desktop-baseline.shSelect a safe Velopack delta baseline +30/-0

Select a safe Velopack delta baseline

• Reads the downloaded package version and retains it only when strictly older than the candidate, otherwise forcing a full-only package.

scripts/desktop-baseline.sh

import-signing-keychain.shProvision an ephemeral macOS signing keychain +32/-0

Provision an ephemeral macOS signing keychain

• Imports the Developer ID certificate into a per-run keychain, configures codesign access, optionally stores notary credentials, and exports the keychain path.

scripts/import-signing-keychain.sh

hash.shAdd portable SHA-256 calculation +7/-0

Add portable SHA-256 calculation

• Provides one hashing function backed by sha256sum on Linux or shasum on macOS.

scripts/lib/hash.sh

semver.shAdd strict SemVer comparison helpers +64/-0

Add strict SemVer comparison helpers

• Implements build stripping, prerelease detection, core validation, and SemVer precedence for release scripts while rejecting malformed cores.

scripts/lib/semver.sh

promote-desktop-aliases.shPrevent desktop download alias regressions +39/-0

Prevent desktop download alias regressions

• Examines the merged Velopack manifest and promotes beta or stable DMG aliases only when the candidate is highest in the applicable version class.

scripts/promote-desktop-aliases.sh

render-app-icons.shRender committed macOS application icons +22/-0

Render committed macOS application icons

• Converts the SVG product mark into a 512-pixel PNG and a multi-resolution ICNS asset.

scripts/render-app-icons.sh

render-info-plist.shRender versioned bundle metadata +9/-0

Render versioned bundle metadata

• Substitutes full and short versions into the committed Info.plist template and validates the result.

scripts/render-info-plist.sh

sign-macos.shSign macOS binaries with hardened runtime +16/-0

Sign macOS binaries with hardened runtime

• Applies timestamped hardened-runtime signatures and component-specific entitlements, then strictly verifies every signed file.

scripts/sign-macos.sh

verify-desktop-immutables.shFail closed on conflicting published artifacts +44/-0

Fail closed on conflicting published artifacts

• Fetches existing versioned R2 objects and permits retries only when remote and local bytes match. Missing objects are distinguished from transient fetch errors.

scripts/verify-desktop-immutables.sh

verify-npm-trio.shVerify desktop binaries against npm +37/-0

Verify desktop binaries against npm

• Downloads the matching Darwin npm package with propagation retries and compares its CLI and daemon hashes with the desktop artifact records.

scripts/verify-npm-trio.sh

Capacitor.App.csprojReference the Velopack runtime +1/-0

Reference the Velopack runtime

• Adds Velopack to the desktop application for bootstrap and update management.

src/Capacitor.App/Capacitor.App.csproj

Info.plistDefine macOS bundle identity and requirements +32/-0

Define macOS bundle identity and requirements

• Adds the versioned bundle metadata template, product identity, icon, developer-tools category, and macOS 15 minimum version.

src/Capacitor.App/Packaging/Info.plist

app.entitlements.plistConfigure hardened-runtime app entitlements +14/-0

Configure hardened-runtime app entitlements

• Allows the self-contained .NET application to JIT, use required executable memory and dyld behavior, and load its runtime libraries.

src/Capacitor.App/Packaging/app.entitlements.plist

cli.entitlements.plistConfigure bundled CLI entitlements +8/-0

Configure bundled CLI entitlements

• Disables library validation so the signed CLI can load its downloaded native SQLite dependency.

src/Capacitor.App/Packaging/cli.entitlements.plist

daemon.entitlements.plistDefine minimal daemon signing entitlements +5/-0

Define minimal daemon signing entitlements

• Adds an empty entitlement set for the NativeAOT daemon and PTY shim.

src/Capacitor.App/Packaging/daemon.entitlements.plist

UpdateFeed.csConfigure the self-hosted desktop update feed +11/-0

Configure the self-hosted desktop update feed

• Defines the kurrent.io osx-arm64 feed URL with a trimmed environment override for testing.

src/Capacitor.App/Services/Update/UpdateFeed.cs

@qodo-code-review

qodo-code-review Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Prerelease builds cannot be packaged ✗ Dismissed 🐞 Bug ≡ Correctness
Description
render-info-plist.sh writes ${version%%+*} directly into CFBundleVersion, so a release such as
0.12.0-beta.1 leaves a hyphenated SemVer value in the bundle build-version field. The release
workflow renders this plist from prerelease tags before passing it to the macOS packer and
notarizer, but that field requires Apple's numeric build-version format, so the planned beta release
cannot produce the advertised distributable.
Code

scripts/render-info-plist.sh[R7-8]

+short="${version%%-*}"; short="${short%%+*}"
+sed -e "s/{VERSION}/${version%%+*}/" -e "s/{SHORT_VERSION}/$short/" "$here/../src/Capacitor.App/Packaging/Info.plist" > "$out"
Relevance

●●● Strong

Prerelease version handling bugs and release-workflow correctness fixes are consistently accepted.

PR-#582
PR-#363

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The renderer removes only build metadata and therefore preserves -beta.1; the template places that
result in CFBundleVersion. The release workflow renders this exact plist and supplies it to `vpk
pack`, while the checked-in distribution design explicitly identifies prerelease desktop releases.

scripts/render-info-plist.sh[5-9]
src/Capacitor.App/Packaging/Info.plist[15-18]
.github/workflows/release.yml[744-766]
docs/superpowers/specs/2026-09-06-ai1653-app-distribution-design.md[30-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Prerelease SemVer strings such as `0.12.0-beta.1` are currently written directly to `CFBundleVersion`, which is not a valid macOS bundle build version. Generate a numeric, monotonically ordered Apple-compatible build version while retaining the full SemVer for Velopack/package metadata.

## Issue Context
The release workflow's first planned desktop version is a beta tag. `plutil -lint` checks plist syntax but does not validate the semantic format of `CFBundleVersion`.

## Fix Focus Areas
- scripts/render-info-plist.sh[5-9]
- src/Capacitor.App/Packaging/Info.plist[15-18]
- .github/workflows/release.yml[744-766]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Redundant labels clutter test files ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The // ---- the update item ---- and // --- Suppressed... --- comments only label groups already
described by the adjacent test names. When tests are reorganized, maintainers must update these
labels even though they document no behavior or constraint.
Code

test/Capacitor.App.Tests.Unit/TrayAdapterTests.cs[404]

+    // ---- the update item ----
Relevance

●●● Strong

Recent test reviews accept removing redundant labels and comments that duplicate test names or
assertions.

PR-#703
PR-#692

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2762993 disallows comments that merely restate obvious structure. These added comments only
divide tests into groups whose purpose is already expressed by their method names.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.App.Tests.Unit/TrayAdapterTests.cs[404-404]
test/Capacitor.App.Tests.Unit/TrayViewModelTests.cs[1003-1003]
test/Capacitor.Cli.Tests.Unit/UpdateNoticeIsHumanFacingTests.cs[109-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remove test comments that merely label groups already made clear by the test names.

## Issue Context
Compliance rule 2762993 permits comments only for non-obvious, behavior-critical constraints.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/TrayAdapterTests.cs[404-404]
- test/Capacitor.App.Tests.Unit/TrayViewModelTests.cs[1003-1003]
- test/Capacitor.Cli.Tests.Unit/UpdateNoticeIsHumanFacingTests.cs[109-109]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Closing the app keeps update checks alive ✓ Resolved 🐞 Bug ☼ Reliability
Description
VelopackAppUpdater.CheckAsync accepts the lifetime cancellation token but calls
CheckForUpdatesAsync() without passing or observing it. UpdateCoordinator supplies the app
shutdown token to this method, so closing the app during a feed request leaves that request running
until Velopack completes it rather than ending the scheduled update operation with shutdown.
Code

src/Capacitor.App/Services/Update/VelopackAppUpdater.cs[R25-27]

+    public async Task<UpdateCandidate?> CheckAsync(CancellationToken ct) {
+        var info = await _manager.CheckForUpdatesAsync().ConfigureAwait(false);
+        _lastCheck = info;
Relevance

●●● Strong

Recent App reviews accept fixes ensuring asynchronous operations honor shutdown and cancellation
lifetimes.

PR-#653
PR-#740

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The concrete updater receives ct but invokes the Velopack check with no cancellation handling. The
coordinator passes its lifetime token to that method and relies on cancellation to end scheduled
work during shutdown.

src/Capacitor.App/Services/Update/VelopackAppUpdater.cs[25-27]
src/Capacitor.App/Services/Update/UpdateCoordinator.cs[115-124]
src/Capacitor.App/Services/Update/UpdateCoordinator.cs[147-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`VelopackAppUpdater.CheckAsync` drops its `CancellationToken`, so coordinator shutdown cannot stop awaiting an in-progress update check.

## Issue Context
The update coordinator passes its application-lifetime token into `IAppUpdater.CheckAsync`; the concrete Velopack adapter must observe it even if Velopack's check API itself has no cancellation-token overload.

## Fix Focus Areas
- src/Capacitor.App/Services/Update/VelopackAppUpdater.cs[25-27]
- src/Capacitor.App/Services/Update/UpdateCoordinator.cs[147-167]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Redundant comments clutter update logic ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
RunScheduleAsync and RunCheckAsync annotate OperationCanceledException handlers only with `//
shutdown`, which restates the surrounding cancellation control flow. Readers receive no rationale or
invariant from either label and must sift past them when changing shutdown behavior.
Code

src/Capacitor.App/Services/Update/UpdateCoordinator.cs[124]

+            // shutdown
Relevance

●●● Strong

Recent reviews accept removing comments that merely restate evident control flow.

PR-#703
PR-#766

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2762993 requires comments to explain non-obvious, behavior-critical constraints. Both added
comments merely name the evident purpose of an OperationCanceledException handler.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
src/Capacitor.App/Services/Update/UpdateCoordinator.cs[123-125]
src/Capacitor.App/Services/Update/UpdateCoordinator.cs[163-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remove comments that merely label cancellation handlers as shutdown paths.

## Issue Context
The caught exception and lifetime token already make the shutdown behavior apparent; the comments document no additional constraint.

## Fix Focus Areas
- src/Capacitor.App/Services/Update/UpdateCoordinator.cs[124-124]
- src/Capacitor.App/Services/Update/UpdateCoordinator.cs[164-164]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 58 rules
Review mode: 🧠 Deep: This is a high-risk, bug-dense release and auto-update change spanning CI signing/publication, packaging, security entitlements, app lifecycle, daemon coordination, and multiple independent code paths.

Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread test/Capacitor.App.Tests.Unit/TrayAdapterTests.cs Outdated
Comment thread src/Capacitor.App/Services/Update/UpdateCoordinator.cs Outdated
Comment thread scripts/render-info-plist.sh
Comment thread src/Capacitor.App/Services/Update/VelopackAppUpdater.cs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

alexeyzimarev and others added 6 commits September 7, 2026 12:49
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>
@alexeyzimarev
alexeyzimarev merged commit abe5c6f into main Sep 9, 2026
7 checks passed
@alexeyzimarev
alexeyzimarev deleted the alexeyzimarev/ai-1653-app-distribution-cli-bundling-signingnotarization-dmg-auto branch September 9, 2026 09:58
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