Repository navigation
Retry notarization with fresh credential deliveries - #256
Conversation
There was a problem hiding this comment.
Review round 1: request changes (independent Opus review of the PR head, verified by the coordinator)
No credential leaks and no automatic resubmission. But the retry policy would not have saved the v0.5.0 release:
- P1: retries outlive the delivered key file. Ask Key deletes a delivered temporary file 5 minutes after delivery or at approval expiry (
FileDeliveryManager.swift~42, ~136-137; the approval expiry counts from request creation, so time spent approving is included). Before this PR onenotarytool submit --waitread the key once; now everywaitis a new process that must re-read$PRIVATE_KEY_FILE, so later attempts fail with a non-transient key/file error while Apple is still processing. Fix: in the Python runner, read the key bytes once at start and give eachnotarytoolcall the key through an inherited pipe (--key /dev/fd/Nwithpass_fds), never writing it anywhere. Confirmnotarytoolaccepts that path with a stubbed check in tests and note in the PR that the real path is verified at the next release; if it does not work, stop with a clear message rather than looping. - P1: the budget is attempts, not time, and submit is unprotected. Four attempts with 1/2/4 s backoff can be spent in seconds on fast-failing codes, against 1-5 minutes of processing; and
submititself fails about one time in three on that network. Fix: poll withnotarytool info <id>(one request per process) under a wall-clock deadline (for example 30 minutes) with capped backoff (for example 5 s growing to 60 s), treating transient errors as "try again". For submit, on a transient failure, first look for the submission innotarytool history(same artifact name, created after this submit started) and adopt its ID; only if none exists, submit again, at most a few times. Print every submission ID obtained. - P2: tests cannot see either P1. Make the fake tool model elapsed time (or a call counter standing in for it), a key file that disappears after a deadline and on target exit, a submit that times out and then appears in history, and fast-failing transient codes. Keep test run time short (inject sleep).
- P3s to take now: update
docs/testing.md(~123-126) for the new flow; make the "submission ID unavailable" message distinguish approval denial/timeout from a failed submit; add a test that the inline script stays well under the broker's 4 KB per-argument limit (BrokerLimits.swift~6) or move the runner into a separate file underscripts/passed by path; avoid the nested heredoc inside$(...)if you keep it inline.
Out of scope here (note it in the PR): resuming stapling from an existing submission ID.
|
Addressed review round 1 at final head
|
There was a problem hiding this comment.
Review round 2: request changes (independent Opus re-review of the round-1 head, plus a coordinator check on the real tool)
Round 1 is implemented and bounded, with no leak path found. But one blocker:
- Blocker:
notarytooldoes not accept a key through a pipe. I checked with a real (non-release) key on this Mac:notarytool history --key /dev/fd/3 … 3< <(cat key)and--key /dev/fd/0both fail every time withThe file "3" couldn't be opened because you don't have permission to view it(it opens the path as a regular file). So credential mode as written would fail on every release. Replace the in-memory pipe with one Ask Key run per notarytool request: the runner stays outside Ask Key and callsaskkey run --wait-for-approval --credential <name> --operation-id <new uuid> … -- /bin/bash -c 'xcrun notarytool <action> … --key "$PRIVATE_KEY_FILE" --key-id "$KEY_ID" --issuer "$ISSUER_ID" --output-format json' bash <args>for submit, history and each status poll. Each request then gets a freshly delivered key file well inside its 5-minute lifetime, and the key never leaves Ask Key's delivery. Before the first request, print one line telling the operator to approve the first card with a timed allowance (Allow for N Minutes) so later polls don't each need a card; if a later request still needs approval,--wait-for-approvalhandles it within the remaining deadline. Keychain-profile mode keeps callingnotarytooldirectly. Keep the 30-minute deadline, capped backoff and bounded uploads. - P2: transient
historyfailures end the run. Retry transient errors on read-onlyhistory/infowithin the deadline and don't count them as empty snapshots; still stop on invalid JSON, bad entries or multiple matches. - P2: no clock-skew margin in history matching. Round
starteddown to the second and subtract a margin (for example 3 minutes); same-name entries inside[started - margin, started)are ambiguous: stop and report their IDs rather than ignoring or adopting them. Add tests for a fast and a slow local clock. - P2: adopted submission that never finished uploading. When an adopted submission hits the deadline still In Progress, report a distinct reason (likely incomplete upload; check
notarytool logbefore resubmitting) and mention it indocs/testing.md. - P2: IDs lost on Ctrl-C/TERM. Make the INT/TERM handlers print any submission IDs already in the response file before exiting.
- P2:
test_…window starts at the first uploadcannot fail becausenow()is constant; make it advance with elapsed time and place the entry between the first and second attempt. - P3s: the "request" failure text mentions key pipes (drop it); after a credentials failure don't say "inspect history before submitting again"; let the fake
askkeyfilter the environment like the broker (HOME, PATH, TMPDIR, LANG, TERM, SHELL, USER, LOGNAME, LC_*); resetrunner.delaybetween subtests; cover the profile-mode constructor.
Real notarization is still verified at the next release; say so in the PR.
|
Addressed review round 2 in
Local validation: 19 packaging tests and 22 runner tests passed (41 total, 0 failed, 0 skipped); bash syntax, hygiene, module dependencies and diff checks passed. Shellcheck is unavailable; no Swift rebuild was required under the coordinator waiver. Branch rebased onto |
There was a problem hiding this comment.
Review round 3: one small change (independent Opus re-review of 37f0dc5, verified). Last round: after this, only P0–P2 block merge.
Round 2 is fully in place, including the per-request Ask Key runs, history retries, the clock-skew window, adopted-incomplete reporting, IDs on INT/TERM, the fixed test and the P3s. No leak path found.
- P2: after an upload has started, a later
approvalorrequestfailure says nothing was submitted.summarize()picks the hint from the final reason only (notarize-release.py~241-242), andrequest()raisesapprovalwhenever the start marker is missing (~97-99), including for a history query after a submit that already began uploading. The operator is then told the request "did not start", with no history hint, and a rerun makes a duplicate. Emit anupload-startedevent the first time a submit passes the marker (credential mode) or starts (profile mode); suppress the history hint and the "did not start" wording only when that event is absent. Add a test: transient submit, then a history request without a marker. - P3s to take with it:
docs/testing.md(~126) should show the target shell with the start marker asTARGET_SHELLactually uses it; give credential-mode requests the same per-request cap as profile mode (300 s submit, 120 s query, bounded by the remaining budget); theambiguoustext should not say "Multiple" when it is a single entry from the margin window; reportdeadline(oradopted-incomplete) rather thanapprovalwhen the runner itself kills a helper that was still waiting at the deadline.
|
Addressed review round 3 in
Final local validation: 24 runner tests and 19 packaging tests passed (43 total, 0 failed, 0 skipped), plus bash syntax, hygiene, module dependency and diff checks. Shellcheck is unavailable; no Swift code changed or rebuild was required. The branch was rebased before pushing, build artifacts are removed, and final-head CI is running at https://github.com/sudoHG/AskKey/actions/runs/41599032562. Real notarization remains NOT_VERIFIED until the next release. |
Closes #255
Summary
Recover transient notarization failures under a 30-minute monotonic budget per artifact, using capped backoff and status polling for a known submission ID.
Give each credential-mode request its own Ask Key run and fresh delivered key file; the runner never reads or copies the key, and credential variables expand only inside the helper's target shell.
Suggest a timed allowance on the first card; later requests may require approval again within the remaining budget, as explicitly accepted in the issue's updated scope.
Reconcile uncertain uploads through history before bounded resubmission, retain IDs on failures and interrupts, and preserve the four assets, stapling and verification gates.
Diff stat
Base:
3ad4258e17e90ef31baf663461674888e91bc235(origin/main). Head:501f593a80efe4a8d0f65cb15706419ec2e478f0. Rebased onto the fetched base before pushing.Tests
python3 -m unittest discover -s Tests/Automation -p test_package_release.py -v: 19 passed, 0 failed, 0 skipped (217.976 seconds).python3 -m unittest discover -s Tests/Automation -p test_notarize_release.py -v: 24 passed, 0 failed, 0 skipped (0.012 seconds).--no-notarizepackaging with an ad-hoc-signed synthetic app produced the unnotarized DMG and checksum, mounted read-only, verified the signature and Applications symlink, checked the checksum, detached, and cleaned up. Synthetic notarization still verifies all four release assets and both checksums.swift build: PASS on the initial implementation headbd315187(15.51 seconds). The coordinator waived repeat builds for the review fixes because no Swift files changed. Local Swift unit tests and desktop E2E: NOT_RUN; final-head full-suite and desktop CI results are recorded below.Checks
bash -n scripts/package-release.sh.python3 scripts/check_hygiene.py,python3 scripts/check_module_deps.py, andgit diff --check.df -k /System/Volumes/Databefore the only build: 252,209,624 KiB available, above 80 GiB. Task.buildremoved after validation (about 843 MiB plus small manifest caches); temporary packaging fixtures and mounts cleaned up.501f593a80efe4a8d0f65cb15706419ec2e478f0.Deviations and questions
The coordinator approved the expanded runner/docs/recovery scope and explicitly replaced the original one-approval-per-artifact requirement with per-request delivery. A timed allowance can avoid repeated cards; later cards remain possible and accepted. No unresolved implementation question remains.
History matching uses the first upload time rounded down to a second, with a three-minute pre-start ambiguity margin. A matching entry in that margin or multiple matches stop with all observed IDs. Transient history failures retry without counting empty snapshots; only three successful empty snapshots permit another upload, up to three uploads total, and a known ID is never resubmitted. History visibility delays and larger clock skew remain limitations. An adopted ID still In Progress at the deadline reports a possibly incomplete upload and directs the operator to its log. Resuming stapling from an existing ID is out of scope.