ci(dev tip): build, sign and notarize the tip DMG while verify runs
Leo, 2026-10-26: "And I thought we were incremental... Nobody has come back with the incremental building results."
What changes
In cmux-next-dev-daily.yml, the tip job waited for the Verify job before it did any DMG work. Verify's queue plus run added a median 3.6 min from "sign done" to the start of the DMG work (13 recent publish runs).
The job is now two jobs:
tip-dmg("Build the cmux NEXT DEV tip DMG", macOS) needs onlysign, so it runs while verify does. It reads the live feed (a stale build stops here), builds the DMG, signs it, notarizes and staples it, checks the key and build, and generates the appcast with deltas. Then it uploads the DMG, deltas, appcast and the digest of the archive it built from as an artifact for this attempt. It holds no R2 key and writes nothing public.tip("Publish the cmux NEXT DEV tip feed", now Linux) needs[sign, verify, tip-dmg]. Likepublish, itsifrequiresneeds.verify.result == 'success'andneeds.verify.outputs.verified == 'true'. It also requiresneeds.tip-dmg.outputs.ready == 'true', which only the step after the artifact upload sets. Before any upload it checks that the digest tip-dmg built from equalstested.sha256, the digest verify passed, byte for byte. The R2 upload step is the same as before: write-once archives first, then the compare-and-swap feed write.
Why the feed is gated rather than rolled back. You asked for one of two designs: tip publishes only after verify passes and does its work in parallel, or a failed verify rolls the feed back. A rollback would leave a bad build in the feed until the revert lands, and installed apps could pull it in that window. A gate means a build verify rejects never reaches the feed, so there is nothing to roll back. All a failed verify costs is one notarized DMG that nobody can see.
How this meets hotswap-opus's checklist
- codesign in tip-dmg. tip-dmg runs Apple's
/usr/bin/codesign --verify --deep --strict --verbose=2on the Linux-signed app beforehdiutil create, the DMGcodesign --forceandnotarytool submit. The step runs underset -euo pipefail, so codesign's exit status survives thetee. It has nocontinue-on-errorand no|| true. If the output contains "sealed resource is missing or invalid", the step fails before the DMG exists. Notarization and stapling still come before the appcast that names the DMG. TheSUPublicEDKeyandCFBundleVersionguards are unchanged.publishkeepsneeds: [sign, verify]and its gate. Tests assert all of this, both with string checks and with YAML-level checks. - Feed writes. Unchanged in this PR: the only feed write is still the
tip_feed.py mergecompare-and-swap withIf-Match, after the write-once uploads, and the cleanup trap rule is the same. The upload step's script is byte-identical apart from losing itsif:, and the existing bash harness for that step still passes.TIP_RUN_TAGcomes from tip-dmg's outputs, so a re-run oftipalone uploads exactly the files tip-dmg signed and named. A re-upload of an identical object is accepted by--write-once. Each tip-dmg attempt hands off under its own artifact name (cmux-next-dev-tip-<attempt>). - No concurrency group on publish, tip-dmg or tip.
tests/test_nightly_delivery_concurrency.pynow liststip-dmgtoo.test_cmux_next_dev_tip_feed.pychecks the whole file. - Timing. Before/after numbers come from the same runs, measured as sign done → DMG in the feed. They are in the comment below and will be filled in from real runs after landing.
Both tip jobs keep job-level continue-on-error: true, so they stay red-but-advisory and the dispatcher never re-publishes a source run because of them.
Risk
Medium, confined to the DEV tip feed. The rolling prerelease (publish) does not change. This workflow only runs from main (sign requires refs/heads/main), so the first real check is the first DEV publish after landing.
- If it breaks, the tip feed stops advancing. Installed apps keep the current build; nothing bad reaches them. Restore is a revert, within the hour.
- Extra cost when verify fails: tip-dmg's notarization runs anyway, because it no longer waits. That is Blacksmith macOS minutes only, with no effect on the feed.
- Owned-pool rescue: if the rescue cancels a run whose verify is stuck on an owned Mac, the cancel now also stops tip-dmg's work in progress. The re-run redoes it under new attempt names.
Signals
- Success: on a DEV publish run,
Build the cmux NEXT DEV tip DMGstarts right afterSign cmux NEXT DEV, alongsideVerify signed cmux NEXT DEV.Publish the cmux NEXT DEV tip feedthen finishes about 0.5 min after the later of the two.measure_next_build.py --per-landingshows l>tip dropping by verify's share. - Failure:
tipis skipped whileverifysucceeded andtip-dmgis green, ortipfails in "Require the archive verify tested". In either case, revert.
Tests
python3 tests/test_cmux_next_dev_tip_feed.py, tests/test_nightly_delivery_concurrency.py, tests/test_ci_signing_keychain_is_deleted_before_bundle_code.py and the tests/test_ci_*workflow* guards pass locally. actionlint is clean. The new tests run the digest gate in bash: a matching digest places the DMG, appcast and deltas where the upload step reads them, a different digest publishes nothing, and an empty run tag publishes nothing.
Agent session: agent session
🤖 Generated with Claude Code
Summary by CodeRabbit
- Reliability
- Daily preview apps are checked for valid signatures before installers are built, signed, and notarized.
- The download feed is updated only after release verification succeeds and the installer is confirmed to match the tested app.
- Recovery from interrupted or repeated release attempts is safer: valid downloads are retained when a feed update’s outcome is uncertain, while uploads from unsuccessful attempts are cleaned up when appropriate.