release: conditional Developer ID signing + notarization (unblocks macOS 26.3.1 launchd)#62
Open
Augustas11 wants to merge 1 commit into
Open
release: conditional Developer ID signing + notarization (unblocks macOS 26.3.1 launchd)#62Augustas11 wants to merge 1 commit into
Augustas11 wants to merge 1 commit into
Conversation
macOS 26.3.1 tightened launchd's AMFI policy to reject adhoc-signed binaries under `launchctl bootstrap`. Discovered 2026-06-12 during the v1.3.1 deploy attempt — fresh installs on macOS 26.3.1 fail with "Bootstrap failed: 5: Input/output error" because the binary has no CMS blob. Earlier macOS versions accepted adhoc-signed binaries; the policy change is OS-version-bound, not signature-format-bound. The fix is to ship a Developer ID Application signed and notarized binary. This commit adds the workflow step but guards it on operator secrets so it can merge immediately and activate when the operator populates the Apple Developer secrets — degrades gracefully to the current (adhoc-signed) behavior with a GitHub Actions warning when the secrets are absent. Workflow step order: 1. Build package (existing) 2. NEW: Sign + notarize binary - import .p12 cert into transient keychain - codesign --options runtime --timestamp --sign <Developer ID> - notarytool submit --wait - stapler staple - re-tar with signed binary - delete keychain 3. Tier-2 preflight (existing — now sees the signed tarball) 4. Prepare release assets (existing) 5. Publish release (existing) Five new operator secrets: - APPLE_DEVELOPER_ID_CERT_P12_BASE64 - APPLE_DEVELOPER_ID_CERT_PASSWORD - APPLE_NOTARY_APPLE_ID - APPLE_NOTARY_PASSWORD - APPLE_NOTARY_TEAM_ID The existing MACPROVIDER_RELEASE_SIGNING_KEY_PEM (checksums signing) is unrelated and stays as-is. Operator runbook lives in phase3-binary/dist/release-signing-runbook.md: how to enroll in the Apple Developer Program, generate the cert, export .p12, generate the app-specific notary password, find the Team ID, and populate the secrets. Plus the verification command for a macOS 26.3.1+ client and the common follow-up failure modes. Once the secrets are populated, the next tagged release passes through the new step. The expected duration overhead is 1-15 minutes for notarization on top of the existing ~5-10 minute build. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
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.
Summary
Wires up Developer ID signing + notarization in the release workflow, conditional on operator-supplied secrets. Can merge immediately; activates when secrets are populated.
Why
Discovered 2026-06-12 while attempting to install v1.3.1 on the operator's macOS 26.3.1 Mac.
launchctl bootstrapfails with the generic5: Input/output errorregardless of plist content, domain, or sudo escalation. AMFI kernel logs surface the root cause:macOS 26.3.1 tightened launchd's AMFI policy to require a CMS certificate chain. Adhoc-signed binaries (what Swift's default linker-signing produces, and what every release through v1.3.1 has shipped) lack the CMS blob. Earlier macOS versions accepted them; 26.3.1 does not. Existing already-loaded launchd services keep working — the policy applies to fresh
bootstrapcalls, which is exactly whatinstall.shtriggers.Operational impact: blocks all fresh v1.3.1+ installs on macOS 26.3.1+ until signing is wired up. Cannot upgrade existing pool members (air5, air8gb on v1.2.5) without taking them offline. The M1-1 / FR-C9 deploy is on hold until this PR lands and the operator populates the secrets.
What's in this PR
.github/workflows/release.yml— new step "Sign + notarize binary"Inserted between existing
Build packageandTier-2 provider artifact preflightso the preflight script sees the signed tarball.Pipeline:
.p12cert into a transient keychaincodesign --options runtime --timestamp --sign <Developer ID>xcrun notarytool submit --wait(1-15 min)xcrun stapler staple(offline launches work)Gated by
APPLE_DEVELOPER_ID_CERT_P12_BASE64. If empty: emits a GitHub Actions warning and ships an adhoc-signed binary (current behavior). If the cert secret is set but supporting secrets are missing: hard fails (partial setup would silently produce mis-labeled artifacts).phase3-binary/dist/release-signing-runbook.md— operator setupStep-by-step: enroll in Apple Developer Program, generate cert via Keychain Access, export
.p12, generate app-specific notary password, find Team ID, populate the 5 new GitHub repo secrets, verify on a macOS 26.3.1+ client. Includes common follow-up failure modes (staple failed, ticket not found, Gatekeeper quarantine).New secrets the operator needs to populate
APPLE_DEVELOPER_ID_CERT_P12_BASE64base64 -i path/to/cert.p12 | pbcopyAPPLE_DEVELOPER_ID_CERT_PASSWORDAPPLE_NOTARY_APPLE_IDAPPLE_NOTARY_PASSWORDAPPLE_NOTARY_TEAM_IDExisting
MACPROVIDER_RELEASE_SIGNING_KEY_PEM(checksums signing) is unrelated and stays as-is.Cost
Test plan
top-level keys: ['name', 'on', 'permissions', 'jobs']— verified locally)if [ -z ... ]early-exit branch)curl get.streamvc.live/install.sh | bashsucceeds atInstall as a background service? [Y/n] y(currently fails with I/O error)launchctl print gui/$(id -u)/live.streamvc.macprovidershowsstate = runningNot in this PR (separate follow-ups)
RequireProviderTokens=true— the v1.3.1 coordinator enforces TOFU unconditionally, which combined with old binaries in the pool (that can't persistassigned_provider_token) brick-kicks providers on the next reconnect. SPEC-003 v0.8.2 FR-C9 was designed assuming the settling window would let providers self-mint cleanly before flag flip, but TOFU bites immediately. Needs a flag-gated variant.limit_req_zoneinphase4-coordinator/dist/nginx-coordinator.streamvc.live.conf:18-19— collides with the same zone declared byapi.streamvc.livevhost. M1-4 (PR m1-4: rate-limit /ws/provider + per-IP semaphore (SECU-1) #39) didn't account for cross-vhost zone uniqueness. Operator hot-patched the live Pearl conf to comment out the duplicates; the source-of-truth file still has them.372c3372023e(air5) andd630849e8acb(air8gb) — minted during the brief 13:54-22:49Z v1.3.1 window today, harmless under v1.3.0-24 but would brick both providers on the next v1.3.1 deploy.🤖 Generated with Claude Code