ci(nightly-next): notarize the app beside the DMG Mac's download, staple it there
Summary
On a pool-signed cmux NEXT nightly, signer-app signed the app and then waited 2.2-3.3 min for Apple before it packed and uploaded the stapled app. Only then could signer-dmg-image start, download that app over the office link (0.5-5 min) and check it. The Mac's download and checks waited behind Apple for no reason: Apple's ticket only has to be in the app before the DMG is built.
signer-appnow packs and uploads the signed app right after signing (same artifact name, nowsigned-app.tar).- A new release-environment Linux job,
signer-app-notary, checks that artifact againstsigner-app's digest and has Apple notarize those exact bytes with the unchangedrelease-signer/notarize-app.sh. - At the same time
signer-dmg-imagedownloads the same bytes, starts the signed-app and verify-signed checks on copies, then staples Apple's ticket with Apple's own stapler (scripts/ci/nightly-steps/signer-staple-app.sh:stapler stapleuntil Apple publishes the ticket for the app's CDHash, at most 25 min, thenstapler validate), runs Apple's checks of the stapled app (signer-apple-checks.sh: codesign, stapler validate, spctl Notarized Developer ID, syspolicy_check, the copied helper) and builds the DMG from it as before. signer-dmgneedssigner-app-notary, so an app Apple rejected never reaches a signed DMG. The DMG is still signed, notarized and stapled on its own, andverify-final-dmg-nightlystill requires the stapled app inside it.
What Apple notarizes is the exact signed bytes the Mac staples (one tar, one digest, checked on both sides). The Mac job holds no secret: stapling needs none. Nothing changes in build-nightly-app, prepare, publish or the macOS fallback signer.
Two smaller commits keep nightly.yml under tests/test_workflow_file_size.py's 490000-byte guard: the signed-app digest check and unpack moves into scripts/ci/nightly-steps/signer-unpack-app.sh, one step that both jobs share, and verify-final-dmg-nightly's inline "Verify final DMG artifact" block moves into scripts/ci/nightly-steps/verify-final-dmg-artifact.sh. That script runs from the workflow commit's tooling checkout and still calls the build tree's verify-dmg-artifact.sh. nightly.yml goes from 490342 bytes on base (already over the guard) to 489754.
On "notarize once": notarizing only the DMG would leave the app inside it without a stapled ticket. Today's checks require one (signer-apple-checks.sh's stapler validate and verify-dmg-artifact.sh --require-stapled-app), and it is what lets an app copied out of the DMG pass Gatekeeper offline. Dropping the app notarization would weaken those checks, so this PR keeps both notarizations and overlaps the work around the first one instead. The two Apple waits stay sequential because the app's staple changes the DMG's bytes.
Performance
Chain = build-nightly-app end to publish-nightly end. The before rows are the same five runs as #27508 (merged, f5082ea9). The after column is filled from real nightly-next runs once both land.
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| signer-app (sign, notarize, pack, upload) | 3.2-5.0 min | pending (sign, pack, upload: expected about 1.2 min) | expected -2 to -3.5 min before the Mac starts | runs 40893555177, 40905688542, 40909458084, 40913618519, 40918131595 |
| app notarization wait (Apple) | 129-196 s | unchanged, now beside the Mac's download and checks | 0 | same runs |
| signer-app end to signer-dmg-image end | 3.0-5.5 min | pending | expected -0.5 to -2 min (download and checks overlap Apple) | same runs |
| chain (build end to publish end) | 16.5, 17.9, 17.7, 15.4, 15.9 min | pending | expected a further -0.5 to -2 min on top of #27508 | same runs |
Testing
python3 tests/test_nightly_signer_jobs.py: 25 tests pass, including the new wiring test (signer-app no longer notarizes; signer-app-notary checks the digest before it notarizes; signer-dmg needs it; the Mac staples before Apple's checks and the DMG; the background checks start before the staple) andSignerStapleApp(retries until the stub stapler finds the ticket, then validates; fails at the deadline without validating; fails on a staple that does not validate; refuses a missing app).tests/test_nightly_step_coverage.py,tests/test_ci_signing_keychain_is_deleted_before_bundle_code.py,tests/test_supplychain_security.py,tests/test_nightly_next_recovery.py,tests/test_ci_nightly_off_blacksmith.py,tests/test_ci_runs_on_fallback.py,tests/test_ci_runs_on_values.py,tests/test_runs_on_labels.pypass.tests/test_ci_upload_artifact_retention.pyfails only on the base's owncmux-tui.ymlentry.
Observed / Not run
Observed: the tests above on Linux. Not run: Apple's stapler against a live submission (the stub models its "Record not found" exit before the ticket exists), and a real nightly-next run; the first runs after this lands fill the Performance table.
Changelog
none
š¤ Generated with Claude Code
Agent session: agent session