← plans · north_os · DESIGN.md22 juil. 2026 à 22:31

Desktop build & release pipeline — design spec

Status: Approved · Author: Claude (with Christophe) · Date: 2026-07-22 · Repo: north-os (apps/polaris, apps/polaris-win, apps/web, .github/workflows)
Related: apps/polaris/scripts/release.sh (legacy local pipeline), .github/workflows/release-desktop.yml (rewritten by this spec), polaris-win.yml, polaris-mac.yml

Decision

Both desktop apps (Polaris macOS, Polaris Windows) build, sign, and package entirely on GitHub Actions hosted runners. A release is triggered by pushing one git tag (desktop-v<version>); both platforms release in lockstep under one shared version number. Artifacts and auto-update feeds are served from a public Cloudflare R2 bucket behind updates.heynorth.dev. A latest.json manifest published alongside the feeds drives a new authenticated /downloads page in apps/web.

Current state (why this spec exists)

1 · Origin — R2 behind updates.heynorth.dev

One public R2 bucket (north-updates) with custom domain updates.heynorth.dev (Cloudflare R2 custom-domain routing; this replaces the broken DNS record). Bucket layout:

polaris-mac/appcast.xml                       Sparkle feed
polaris-mac/North-<version>.dmg               versioned DMGs (no more single-file overwrite)
polaris-win/RELEASES                          Velopack feed index
polaris-win/*.nupkg, NorthSetup.exe           Velopack packages + installer
latest.json                                   manifest for the web download page
staging/<ref>/...                             dry-run output, never polled by apps

latest.json shape:

{
  "version": "1.7",
  "pubDate": "2026-07-22T10:00:00Z",
  "notes": "one-paragraph release notes",
  "mac": { "url": "https://updates.heynorth.dev/polaris-mac/North-1.7.dmg", "size": 11852050, "minOS": "14.0" },
  "win": { "url": "https://updates.heynorth.dev/polaris-win/NorthSetup.exe", "size": 89000000 }
}

Retention: the publish job keeps the last 5 DMG versions and lets Velopack manage its own package set; older objects are pruned during publish.

2 · Release workflow — release-desktop.yml (rewritten)

Trigger: push of tag desktop-v* (e.g. desktop-v1.7), plus workflow_dispatch with inputs version and dry_run.

Job mac (macos-latest)

  1. Import the Developer ID .p12 into an ephemeral keychain (created and destroyed within the job).
  2. xcodebuild archive with MARKETING_VERSION from the tag and CURRENT_PROJECT_VERSION = monotonically increasing build number (GitHub run number offset past the current appcast build).
  3. Export Developer-ID-signed .app; notarize with an App Store Connect API key via notarytool (no interactive keychain profile).
  4. create-dmg with the existing drag-to-Applications background; notarize + staple the DMG.
  5. Sparkle: sign_update with the EdDSA private key from a secret (passed via file, never echoed); download the current appcast.xml from R2 and prepend the new item (bootstrap case: start a fresh appcast if none exists or only legacy makesomething items are present — those are dropped).
  6. Upload DMG + new appcast as job artifacts only — this job never touches R2.

Job win (windows-latest)

  1. dotnet test (meeting-detection logic) then dotnet publish self-contained win-x64, -p:Version=<tag version>.<run_number>.
  2. vpk download the previous release from the R2 feed (S3-compatible endpoint) so vpk pack emits delta packages — users patch instead of re-downloading.
  3. Upload the Velopack output as job artifacts only.

Job publish (ubuntu-latest, needs: [mac, win])

  1. Download both artifact sets.
  2. Single aws s3 sync push of everything (both feeds + regenerated latest.json) to R2 — to staging/<ref>/ when dry_run, live prefixes otherwise.
  3. Prune old versions per retention policy.

The single publish job is the lockstep guarantee: if either platform build fails, nothing ships, and the feeds can never desync. A failed run is retried by deleting and re-pushing the same tag after the fix.

3 · Versioning

Tag desktop-v1.7 → marketing version 1.7 on both apps. Mac build number and Windows fourth version segment derive from the workflow run number, so re-runs are always monotonic. The tag is the single source of truth; appcast.xml no longer drives version detection.

4 · Existing workflows — disposition

WorkflowFate
polaris-mac.ymlKept as-is (unsigned compile check on Polaris paths).
polaris-win.ymlKept for CI + the polaris-win-dev portable branch builds. The polaris-win-latest rolling release stops being the auto-update channel (step removed); the channel is now tag-driven R2.
release-desktop.ymlFully rewritten per §2 (the current "promote staged artifacts" flow assumed humans stage signed builds — that assumption dies).
release.shKept as documented local fallback, retargeted to upload to R2 instead of rsync-to-Caddy.
Caddy updates-heynorth.caddyfile + /var/www/updates on cosmicDecommissioned once R2 serves the domain.

5 · Download page — apps/web /downloads

Authenticated route (confidential project — not public). Server-side fetch of https://updates.heynorth.dev/latest.json, cached ~5 minutes. Two cards (macOS: DMG, "requires macOS 14+"; Windows: NorthSetup.exe) showing version, release date, and notes, with direct download buttons. Linked from the user menu / settings. Follows DESIGN.md (semantic tokens, card surface ladder). Graceful degradation: if latest.json is unreachable, the page renders with an "updates feed unavailable" state rather than erroring.

6 · Secrets (one-time provisioning — the only manual part)

SecretSource
APPLE_CERT_P12 (base64) + APPLE_CERT_PASSWORDExport Developer ID Application cert from Keychain
ASC_API_KEY_P8 + ASC_KEY_ID + ASC_ISSUER_IDNew App Store Connect API key (Developer role)
SPARKLE_ED_PRIVATE_KEYExport from Keychain (generate_keys -x)
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_UPDATES_BUCKETCloudflare dashboard (some may already exist for release-desktop.yml)

Cloudflare dashboard (manual): create north-updates bucket, attach custom domain updates.heynorth.dev (replaces the stale A record).

Accounts: no new paid account. GitHub-hosted runners are included with the repo (macOS bills at 10× multiplier — a release run ≈ 200 billed minutes, a handful per month fits the included quota). The Apple Developer Program ($99/yr) is already held — its cert is what release.sh signs with today.

Verification

Out of scope