Skip to content

fix(release): harden the sign/notarize/appcast pipeline for the first real run#8

Merged
barscn merged 1 commit into
mainfrom
fix/release-pipeline-hardening
Jul 6, 2026
Merged

fix(release): harden the sign/notarize/appcast pipeline for the first real run#8
barscn merged 1 commit into
mainfrom
fix/release-pipeline-hardening

Conversation

@barscn

@barscn barscn commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Why

The Developer-ID → notarize → staple → appcast pipeline had never actually run signed (no cert until now). A pre-flight audit of it found 3 defects that would fail a real v0.1.0 release after notarization — the worst place to fail — plus a hazard that could silently ship an unsigned update.

Blockers fixed

  • generate_appcast undiscoverable on a clean CI runnerappcast.sh only searched .build/artifacts, which a fresh checkout lacks. Now it also searches the DerivedData that xcodebuild archive populates, and release.yml warms .build via swift package resolve (continue-on-error, since discovery is already covered).
  • dist/ held both Capsule-<v>.zip and .dmggenerate_appcast rejects same-version duplicates. Now it signs in an isolated dist/appcast-src/ holding only the update zip; the dmg is a human download Sparkle never sees.
  • Ambiguous Sparkle key encodingrelease.yml now accepts SPARKLE_ED_PRIVATE_KEY whether stored as the raw base64 key string or base64-encoded once more.

Guards & hardening

  • appcast.sh fails the build if any <enclosure> lacks sparkle:edSignature — a key/SUPublicEDKey mismatch can no longer silently publish an update every client rejects.
  • export.sh asserts TeamIdentifier == TEAM_ID and the Hardened Runtime flag on the app and every nested Sparkle component (Autoupdate / Updater.app / XPC services) — catches notarization-fatal mis-signs before submission.
  • ExportOptions pins signingCertificate so a second/renewed Developer ID cert can't be chosen.
  • SPARKLE_ACCOUNT threads the keychain account for local runs; the signing gate now also requires the Sparkle key; stale entitlements comment corrected.

Verification

  • make check + make release-dry green.
  • Unit tests of the key-encoding heuristic and the signature guard.
  • Functional test of the dmg-isolation + guard against the real appcast.sh.
  • Opus adversarial review: no blockers (its findings — the multi-flag runtime check, the resolve failure surface, the pre-mv guard, the gate — are all folded in).

After merge: push, then tag v0.1.0 to cut the first signed release.

🤖 Generated with Claude Code

https://claude.ai/code/session_016e2Q6fgsBk9ML2jwwxRAbf

… real run

A pre-flight audit of the never-yet-run signed pipeline found three defects
that would fail a real `v0.1.0` release *after* notarization (the worst place
to fail), plus a hazard that could silently ship an unsigned update:

- generate_appcast was undiscoverable on a clean CI runner (only .build/
  artifacts was searched). Broaden discovery to the DerivedData that
  `xcodebuild archive` populates, and warm .build via `swift package resolve`.
- package.sh emits Capsule-<v>.zip AND .dmg into dist/, and generate_appcast
  rejects same-version duplicates. Sign the appcast in an isolated dir holding
  only the update zip; the dmg is a human download Sparkle never sees.
- The Sparkle key secret's base64 encoding was ambiguous; release.yml now
  accepts either the raw key string or a double-encoded secret.
- appcast.sh now FAILS if any enclosure lacks sparkle:edSignature, so a key
  mismatch can't silently publish an update every client rejects.

Signing hardening: pin `signingCertificate` in ExportOptions; assert
TeamIdentifier + Hardened Runtime on the app and every nested Sparkle
component after export; thread SPARKLE_ACCOUNT for local keychain runs;
require the Sparkle key in the signing gate; fix a stale entitlements comment.

Verified: make check, make release-dry, unit tests of the key-encoding and
signature-guard logic, and a functional test of the dmg-isolation + guard
against the real appcast.sh. Opus adversarial review: no blockers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2Q6fgsBk9ML2jwwxRAbf
@barscn barscn merged commit 002c7f2 into main Jul 6, 2026
1 of 2 checks passed
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