Repository navigation
chore: signed appcast, a release script that refuses inconsistency, pinned CI - #15
Merged
Merged
Conversation
The appcast is served from main, which anyone with push access - or a stolen token - could rewrite to change the release notes or offer a link in the update dialog. Only the update archive was EdDSA-signed. Set SUVerifyUpdateBeforeExtraction and SURequireSignedFeed (Sparkle 2.9): from 1.1.0 on, the app accepts only an appcast and release notes signed with the release key, and verifies archives before extracting them. generate_appcast signs the feed automatically when a build requires it; the signature is an XML comment, so 1.0.x clients still read the feed. CONTRIBUTING now says appcast.xml must never be edited by hand.
- Preflight before any build: on main and equal to origin/main, no existing tag or GitHub release, no earlier output for this version, the exact Developer ID identity present, SUPublicEDKey set (now fatal), and Sparkle's tools resolved from the version Package.resolved pins instead of whatever generate_appcast DerivedData happened to hold. - Run the unit tests before archiving. - Keep each release in dist/<version>/ instead of deleting dist/ - which also deleted the previous release's dSYMs. - Build the appcast from a copy of the published one, so earlier releases stay in the feed (1.0.0 had already been dropped), without deltas. - Fatal checks that the appcast is validly signed and that the DMG matches its signature in it; generate_appcast only warns and writes an unsigned feed when it cannot sign. - Assert the exported app's team, sign the DMG with the full identity and a stable identifier, and copy the appcast into the repo itself.
- macos-26 and Xcode 26.6, the toolchain releases are built with, instead of macos-latest and whichever Xcode sorts last (a beta, soon). - actions/checkout pinned to the v7.0.1 commit SHA. - Read-only token, a concurrency group and a 30 minute timeout.
ENABLE_USER_SELECTED_FILES produced a user-selected-files entitlement that does nothing without the sandbox.
Findings from 2026-10-11, with what 1.1.0 fixed ticked, and what is partly done, needs hardware, needs Luca, or is deferred on purpose.
The update dialog showed no notes, so users updating to a release that changes behaviour saw nothing about it. release.sh now embeds Scripts/release-notes/<version>.md into the appcast, where the feed signature covers it, and adds the notes for 1.1.0. Also run the unit tests before creating dist/<version>/, so a failing test leaves nothing behind that would block the next run, and make the signature extractor print nothing rather than "None" when an item has no signature.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release hardening from the post-launch audit, to merge before 1.1.0 is cut.
Signed appcast (M2)
The appcast is served from
main. Only the update archive was EdDSA-signed, so anyone with push access — or a stolen token — could change the release notes or put a link in the update dialog. 1.1.0 setsSUVerifyUpdateBeforeExtractionandSURequireSignedFeed(Sparkle 2.9): it accepts only an appcast signed with the release key.generate_appcastsigns the feed automatically once a build requires it; the signature is an XML comment, so 1.0.x clients keep working. From now onappcast.xmlmust never be edited by hand (now in CONTRIBUTING).Scripts/release.shmainand equal toorigin/main; no existing tag, GitHub release ordist/<version>/; the exact Developer ID identity;SUPublicEDKeyset (now fatal); Sparkle tools resolved from the pinned 2.9.6 via-clonedSourcePackagesDirPath(the old DerivedData glob ran whichever version it found first).dist/<version>/— the oldrm -rf distalso deleted the previous dSYMs and dropped earlier appcast items (1.0.0 was already gone).sign_update --verify appcast.xml, and the DMG against itsedSignature—generate_appcastonly warns and writes an unsigned feed if it cannot sign.CI
macos-26+ Xcode 26.6 (the release toolchain) instead ofmacos-latest+ newest Xcode;actions/checkoutpinned to the v7.0.1 SHA; read-only token, concurrency group, 30 min timeout.Also
user-selected-filesentitlement; ignores.claude/settings.local.json.docs/post-launch-audit.mdwith its resolution status.Test plan
bash -npasses; the guards stop a dirty tree and a non-mainbranch before any work (run locally).-clonedSourcePackagesDirPathdelivers Sparkle 2.9.6'sgenerate_appcastandsign_update.user-selectedentitlement; unit tests pass.