fix: survive locked-keychain notarization and add RC resume path - #1256
Conversation
The 2026-07-30 release candidate failed at notarytool submit with "No Keychain password item found for profile: KeyPath-Profile" nine minutes after release-doctor validated that same profile: notarytool stores profiles in the data-protection keychain, which becomes unreadable when the login session locks mid-build. Recovery then required hand-stapling the extracted zip because a dev deploy had re-signed the installed app, detaching it from its eventual ticket. - Prefer the file-based App Store Connect API key for notarytool when present (no lock-state dependency); keychain-profile auth remains the fallback and can be forced with KP_NOTARY_AUTH=keychain-profile. release-doctor now validates whichever auth path the build will use. - Add Scripts/release-candidate.sh --resume-notarization (Scripts/notarize-resume.sh): finish the recorded submission or resubmit the checksum-verified archive, staple dist/KeyPath.app, and deploy it verbatim - no rebuild, no re-sign. - build-and-sign.sh refuses to deploy a bundle without a stapled ticket whenever notarization was requested, and both deploy paths share Scripts/lib/deploy-app.sh (ditto copy, never re-signs). - Extract kp_notary_await_submission so resume can wait on a recorded submission id; enforce the new invariants in verify-release-signing-contract.sh; extend SigningPipelineTests. Incident write-up: docs/bugs/2026-07-30-rc-notarization-keychain-lock.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed the diff. Overall solid — the notarization-resume path, the shared One finding worth a look: Brittle status check in if [[ -f "$state_file" ]] && ! grep -q '"status": "accepted"' "$state_file"; thenThis does a raw string match against the notary recovery-state JSON instead of using the Minor/non-blocking: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be8fecbc39
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| echo "🔁 No submission reached Apple; resubmitting the recorded archive..." | ||
| kp_notarize_zip "$ZIP_PATH" "$NOTARY_PROFILE" |
There was a problem hiding this comment.
Refuse resubmission after an inconclusive history lookup
When submit fails without returning an ID, an empty candidateSubmissions list does not prove that nothing reached Apple: kp_notarize_zip also persists an empty list when the best-effort history request fails, returns malformed data, or has not exposed the new upload yet. In those cases this branch automatically uploads the same archive again, creating the duplicate submission the recovery safeguards are intended to prevent; require positive evidence that the history lookup succeeded and is authoritative, or require an explicit operator opt-in before resubmitting.
Useful? React with 👍 / 👎.
| APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app" | ||
| ZIP_PATH="${DIST_DIR}/${APP_NAME}.zip" | ||
| STATE_FILE=${KP_NOTARY_STATE_FILE:-"${ZIP_PATH%.zip}.notary-submission.json"} | ||
| NOTARY_PROFILE="${NOTARY_PROFILE:-KeyPath-Profile}" |
There was a problem hiding this comment.
Restore the notary profile recorded in recovery state
If the failed release used a nondefault NOTARY_PROFILE with keychain-profile authentication, a later resume invocation without that environment override ignores the profile already stored in the state file and attempts KeyPath-Profile instead. This makes the advertised resume command fail authentication unless the operator remembers and repeats the original override; read the recorded profile as the fallback before using the hard-coded default.
Useful? React with 👍 / 👎.
| if [ -n "${KP_NOTARY_KEY_PATH:-}" ]; then | ||
| return 0 | ||
| fi | ||
| if [ "${KP_NOTARY_AUTH:-auto}" = "keychain-profile" ]; then | ||
| return 0 |
There was a problem hiding this comment.
Honor the explicit keychain-profile authentication override
When KP_NOTARY_AUTH=keychain-profile is set alongside an existing KP_NOTARY_KEY_PATH, this early return leaves the key path populated and kp_notary_resolve_auth consequently selects API-key authentication. Thus the documented force option cannot recover from a stale, inaccessible, or unwanted API key unless the caller also knows to unset the key variables; check the force setting before accepting KP_NOTARY_KEY_PATH, or make the resolver honor it directly.
Useful? React with 👍 / 👎.
- KP_NOTARY_AUTH=keychain-profile now beats a populated KP_NOTARY_KEY_PATH in kp_notary_resolve_auth, so a stale API key can be bypassed without unsetting variables (new SigningPipelineTests coverage). - notarize-resume falls back to the profile recorded in the recovery state when NOTARY_PROFILE is not overridden. - notarize-resume no longer treats an empty recorded candidate list as proof nothing reached Apple: it requires a successful notarytool history lookup now and refuses to resubmit when history is unavailable or shows fresh submissions of the archive (KP_NOTARY_SUBMISSION_ID overrides). - release-candidate.sh reads the recovery status via kp_notary_json_field instead of grepping the JSON formatting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed the diff (notarization auth refactor +
No security or correctness issues found in the auth-resolution/deploy-lib refactor itself. |
Summary
The 2026-07-30 release candidate died at
notarytool submitwith "No Keychain password item found for profile: KeyPath-Profile" — nine minutes afterrelease-doctor.shhad validated that exact profile in the same shell. Root cause (verified: the profile is absent fromlogin.keychain-db): notarytool on Xcode 26.x stores profiles in the data-protection keychain, whose items are unreadable once the login session locks — so the credential vanished mid-build when the screen locked.security unlock-keychaincannot help (file keychains only). Recovery then required hand-stapling the extracted zip because a dev deploy had re-signed the installed app, changing its cdhash and detaching it from the eventual ticket.Forensic note:
build-and-sign.shdid abort at the failed submit (set -euo pipefailworked); the unstapled/Applicationscopy came from a laterui-deploy.shre-sign. The guards below make both failure modes structurally impossible in the release path.Changes
kp_notary_resolve_authsupports the file-based App Store Connect API key (--key/--key-id/--issuer);kp_notary_default_auth_from_environmentprefers~/.appstoreconnect/private_keys/AuthKey_XQ4565NYZ7.p8when present (identifiers only, not secrets). Keychain-profile auth remains the fallback;KP_NOTARY_AUTH=keychain-profileforces it.release-doctor.shnow validates whichever auth path the build will actually use.Scripts/release-candidate.sh --resume-notarization→ newScripts/notarize-resume.sh: verifiesdist/KeyPath.zipstill matches the recorded SHA-256, waits on the recorded submission (or resubmits the identical archive when nothing reached Apple;KP_NOTARY_SUBMISSION_IDdisambiguates recorded candidates), staplesdist/KeyPath.app, and deploys it verbatim. On failure,release-candidate.shnow prints exactly this resume command instead of a buried nonzero exit.build-and-sign.shrefuses to deploy a bundle without a stapled ticket whenever notarization was requested (kp_staple_validateguard), and both deploy paths share newScripts/lib/deploy-app.sh(stop app →ditto→ relaunch; contains nocodesign). Enforced byverify-release-signing-contract.shandDeploymentScriptContractTests.kp_notary_await_submissionextracted fromkp_notarize_zipso resume can wait on a recorded submission id without re-uploading.docs/bugs/2026-07-30-rc-notarization-keychain-lock.md;docs/process/release-process.mddocuments resume + credential guidance.Test plan
TEST_FILTER='DeploymentScriptContractTests|SigningPipelineTests|ReleaseSigningContractTests' ./Scripts/run-tests-safe.sh— 33 passed, including 4 new SigningPipelineTests (API-key args used end-to-end, incomplete key config rejected before any notarytool call,kp_notary_await_submissionresumes a recorded id without re-uploading, default-auth opt-out)../Scripts/verify-release-signing-contract.sh --source— passes with the new invariants.notarize-resume.sh: accepted fast-path, wait-on-recorded-id, resubmit-when-nothing-reached-Apple, ambiguous-candidates abort withKP_NOTARY_SUBMISSION_IDoverride, and zip-SHA-mismatch abort.TIMEOUT_SECONDS=1800 ./Scripts/test-full.sh: 5069 passed; 6 failures = the contract test this PR updates (now green, see above) plus 5 pre-existingKeyPathSnapshotTestsreference-size mismatches (EasyView home-row timing ×3, launcher drawer, repair settings tab) in UI untouched by this script/docs-only diff — environment drift on the macOS 27 beta box.Review gate
./Scripts/review-gate.sh: remote review gate selected — do not merge until the GitHubclaude-reviewcheck passes.🤖 Generated with Claude Code