Skip to content

ci: let the nightly channel sign with its own Sparkle key - #14215

Merged
teamleaderleo merged 2 commits into
manaflow-ai:mainfrom
teamleaderleo:ci/nightly-sparkle-key
Sep 24, 2026
Merged

teamleaderleo merged 2 commits into
manaflow-ai:mainfrom
teamleaderleo:ci/nightly-sparkle-key

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Nightly and stable releases sign updates with the same Sparkle key (SPARKLE_PRIVATE_KEY), so anything that can sign a nightly can also sign a stable update. That blocks moving nightly signing onto a Mac mini (#14207, cmuxterm-hq#571). This PR gives the nightly channel its own key, behind a switch that is off by default.

What changes. NIGHTLY_SPARKLE_KEY=nightly (repository variable) makes nightly builds embed and sign with the NIGHTLY_SPARKLE_PRIVATE_KEY secret. The embedded SUPublicEDKey and the appcast signature both come from scripts/ci/nightly-sparkle-key.sh, so they can't drift apart. If the switch is on and the secret is missing, the build fails rather than quietly using the shared key. Unset, nothing changes. The rc channel always keeps the shared key.

Why installed nightlies keep updating. Sparkle accepts an update that changes the EdDSA key when the app stays code signed with the same Apple Developer ID (docs, "Rotating signing keys"). cmux doesn't set SUVerifyUpdateBeforeExtraction, so the DMG condition in that section doesn't apply. One rule follows: never change the Developer ID certificate in the same build as this switch.

That rule covers full updates only. Sparkle checks a delta against the installed app's key alone, so deltas from builds that embed the other key could never install. The appcast step now drops those previous DMGs before building deltas (scripts/ci/drop-previous-nightlies-with-other-sparkle-key.sh). The first build after a switch ships without deltas, and clients take the full DMG. For about two builds, generate_appcast also leaves the older feed items unsigned with a warning; clients take the newest item, which is signed.

Limit, stated once. This protects stable users from a leaked nightly Sparkle key. It doesn't protect them from a leaked Developer ID certificate, because Sparkle's rotation rule would accept a Developer ID signed stable update with an attacker's EdDSA key. What still stops that is that the stable appcast is only written by the hosted publish job, whose R2 credentials never go to a mini.

To turn it on (maintainer)

  1. generate_keys from Sparkle, with a new keychain account name so it doesn't overwrite the shared key. Export the private key.
  2. Save it as the repository secret NIGHTLY_SPARKLE_PRIVATE_KEY.
  3. gh variable set NIGHTLY_SPARKLE_KEY --repo manaflow-ai/cmux -b nightly
  4. After the next published nightly, check that an installed older nightly updates to it.

Rollback is deleting the variable. The build after that goes back to the shared key under the same rotation rule.

Validation

  • tests/test_nightly_sparkle_key.sh (new, runs in the release-notary guard group): the variable off, on, and misspelled; the rc channel; a missing secret; and that nightly.yml never passes SPARKLE_PRIVATE_KEY straight from secrets.
  • test_nightly_universal_build.sh, test_sparkle_generate_appcast_no_deltas.sh, test_ci_self_hosted_guard.sh, test_ci_change_areas.py, test_ci_workflow_guards_are_wired.py, actionlint and scripts/verify-local.py pass.
  • drop-previous-nightlies-with-other-sparkle-key.sh checked locally against two throwaway DMGs: it keeps the matching one and drops the other.
  • A subagent review traced the key flow through Sparkle 2.9.5's SUUpdateValidator and AppInstaller. It confirmed the full-update rotation and found the delta problem fixed above.
  • Not run end to end: the fork has no signing secrets. With the variable unset, the next published nightly is the check that nothing changed.

🤖 Generated with Claude Code


Summary by cubic

Stops nightly builds from signing with the shared Sparkle key stable releases use, so a machine that signs nightlies can no longer sign a stable update. The nightly channel gets its own key behind a switch that is off by default; this protects stable users from a leaked nightly key, though not from a leaked Developer ID certificate, which Sparkle's rotation rule would still accept. Because Sparkle validates deltas against the installed app's key only, a key change makes deltas from older builds uninstallable, so those previous DMGs are dropped before appcast generation.

Migration

  • Set NIGHTLY_SPARKLE_KEY=nightly and save a new key as the NIGHTLY_SPARKLE_PRIVATE_KEY secret to enable it.
  • With the switch on and the secret missing, the build fails instead of falling back to the shared key; unset, nothing changes and the rc channel always keeps the shared key.
  • Installed nightlies accept the switch because Sparkle allows an EdDSA key rotation when the Developer ID stays the same — don't change the Developer ID in the same build.

Written for commit 4de5394. Summary will update on new commits.

Review in cubic

nightly.yml derived SUPublicEDKey from, and signed the appcast with, the
same SPARKLE_PRIVATE_KEY stable releases use. A machine that signs
nightlies could therefore sign a stable update.

NIGHTLY_SPARKLE_KEY=nightly now moves the nightly channel onto the
NIGHTLY_SPARKLE_PRIVATE_KEY secret. Both the embedded public key and the
appcast signature come from scripts/ci/nightly-sparkle-key.sh, so they
cannot disagree, and a missing secret fails instead of falling back.
Unset keeps today's key; the rc channel never moves. Installed nightlies
accept the change without a transition build because Sparkle allows an
EdDSA key rotation while the Developer ID stays the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 4 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a7f42ded-b6d3-4a2b-8d74-1a6a11093487

📥 Commits

Reviewing files that changed from the base of the PR and between b9d4df0 and 4de5394.

📒 Files selected for processing (7)
  • .github/workflows/ci-guards.yml
  • .github/workflows/nightly.yml
  • scripts/ci/detect_ci_change_areas.py
  • scripts/ci/drop-previous-nightlies-with-other-sparkle-key.sh
  • scripts/ci/nightly-sparkle-key.sh
  • scripts/ci/workflow_guard_groups.py
  • tests/test_nightly_sparkle_key.sh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Sparkle validates deltas against the installed app's key only, so after
NIGHTLY_SPARKLE_KEY changes the key, deltas from builds embedding the
other key are always rejected. Drop those previous DMGs before
generate_appcast instead of building deltas clients refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo merged commit dd7ea7c into manaflow-ai:main Sep 24, 2026
54 of 55 checks passed
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 24, 2026
dd7ea7c ci: let the nightly channel sign with its own Sparkle key (manaflow-ai#14215)
b9d4df0 ci: reject errno read inside a Swift test assertion (manaflow-ai#14054)
4e35c3e ci: pin the Glaeda candidate that accepts cmux's current Xcode pins (manaflow-ai#14213)
1c1d27b ci: try the nightly app compile on an owned Mac mini first, Blacksmith fallback (manaflow-ai#14208)
1fc4b83 refactor: move the surface catalog's value types into a package (manaflow-ai#13135)
2ee69dd refactor: move 38 leaf mobile-host files into a CmuxMobileHost package (manaflow-ai#14093)
ad5ea20 test(ssh): assert the cmux-tui open flow for TTY cmux ssh (manaflow-ai#14204)
03d3759 test: repair the app-host suites that fail only on macOS 26 (manaflow-ai#13988)
teamleaderleo added a commit that referenced this pull request Sep 24, 2026
…14243)

Reverts the owned-Mac nightly route (#14208, #14223, #14233). There is
no separate nightly lane or fallback: nightlies build on Blacksmith until
Glaeda routing (glaeda#1174) sends every job std > light > Blacksmith >
GitHub-hosted.

nightly.yml is back to its pre-lane Blacksmith path, keeping the later
nightly Sparkle key change (#14215). Signing, notarization and
publication are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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