ci(nightly-next): notarize the app beside the DMG Mac's download, staple it there

Closed
TE
14d ago
+261-81

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-app now packs and uploads the signed app right after signing (same artifact name, now signed-app.tar).
  • A new release-environment Linux job, signer-app-notary, checks that artifact against signer-app's digest and has Apple notarize those exact bytes with the unchanged release-signer/notarize-app.sh.
  • At the same time signer-dmg-image downloads 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 staple until Apple publishes the ticket for the app's CDHash, at most 25 min, then stapler 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-dmg needs signer-app-notary, so an app Apple rejected never reaches a signed DMG. The DMG is still signed, notarized and stapled on its own, and verify-final-dmg-nightly still 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.

metricbeforeafterdeltaharness or run link
signer-app (sign, notarize, pack, upload)3.2-5.0 minpending (sign, pack, upload: expected about 1.2 min)expected -2 to -3.5 min before the Mac startsruns 40893555177, 40905688542, 40909458084, 40913618519, 40918131595
app notarization wait (Apple)129-196 sunchanged, now beside the Mac's download and checks0same runs
signer-app end to signer-dmg-image end3.0-5.5 minpendingexpected -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 minpendingexpected a further -0.5 to -2 min on top of #27508same 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) and SignerStapleApp (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.py pass. tests/test_ci_upload_artifact_retention.py fails only on the base's own cmux-tui.yml entry.

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