From 7937a70aac3231e63850ec214fa262868bde76fa Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 14 Oct 2026 15:45:00 -0500 Subject: [PATCH] release: retry stapler when Apple's ticket service drops the connection Three macos-15-intel jobs failed on 2026-10-14 after notarization was Accepted and the image stapled: twice `xcrun stapler validate` in the smoke step, once `stapler staple` in the notarize step, each with NSURLErrorDomain -1005 "The network connection was lost" against api.apple-cloudkit.com. stapler exits 68 for that as well as for a missing ticket, so the step read a network blip as "no stapled ticket" and failed a release build whose image was fine (#461). The v0.0.5 tag run 38631627111 hit it in the staple step. Both steps now call stapler through a small retry (five attempts, 20 s apart); a genuine rejection still fails every attempt. The smoke error names the second possibility. Closes #461 Coven-Harness: Claude Code / claude-fable-5-1 --- .github/workflows/release.yml | 40 +++++++++++++++++++++++++++++++---- src/release-workflow.test.ts | 12 +++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ff4def2..fb0e023f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -679,6 +679,22 @@ jobs: APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} run: | set -euo pipefail + # Apple's ticket service (api.apple-cloudkit.com) dropped the + # connection mid-request three times on macos-15-intel on 2026-10-14 + # (#461): once here, twice in the smoke test, each after the ticket + # had been issued and the image stapled. stapler exits 68 for that + # as well as for a missing ticket, so each call gets a few attempts + # with a pause between. A real rejection fails all of them. + stapler_retry() { + local attempt + for attempt in 1 2 3 4 5; do + if xcrun stapler "$@"; then return 0; fi + echo "stapler $1 failed (attempt ${attempt} of 5); retrying in 20s..." + sleep 20 + done + echo "::error::stapler $* failed on every attempt." + return 1 + } # Same key file `Build installers` wrote; the next step removes it. case "${NOTARY}" in api-key) @@ -710,8 +726,8 @@ jobs: if [ -n "${id}" ]; then xcrun notarytool log "${id}" "${credentials[@]}" || true; fi exit 1 fi - xcrun stapler staple "${dmg}" - xcrun stapler validate "${dmg}" + stapler_retry staple "${dmg}" + stapler_retry validate "${dmg}" spctl --assess --type open --context context:primary-signature --verbose=2 "${dmg}" done [ "${found}" -eq 1 ] || { echo "::error::No .dmg was produced to notarize."; exit 1; } @@ -811,6 +827,22 @@ jobs: TARGET: ${{ matrix.target }} run: | set -euo pipefail + # Apple's ticket service (api.apple-cloudkit.com) dropped the + # connection mid-request three times on macos-15-intel on 2026-10-14 + # (#461): once here, twice in the smoke test, each after the ticket + # had been issued and the image stapled. stapler exits 68 for that + # as well as for a missing ticket, so each call gets a few attempts + # with a pause between. A real rejection fails all of them. + stapler_retry() { + local attempt + for attempt in 1 2 3 4 5; do + if xcrun stapler "$@"; then return 0; fi + echo "stapler $1 failed (attempt ${attempt} of 5); retrying in 20s..." + sleep 20 + done + echo "::error::stapler $* failed on every attempt." + return 1 + } cd "${STAGE_DIR}" assert_nonempty() { @@ -877,8 +909,8 @@ jobs: # The image is what people download, so its own ticket is # checked too, not only the app's (#407). for dmg in *.dmg; do - xcrun stapler validate "${dmg}" || { - echo "::error::${dmg} has no stapled notarization ticket."; exit 1; } + stapler_retry validate "${dmg}" || { + echo "::error::${dmg} has no stapled notarization ticket, or Apple's ticket service could not be reached (#461)."; exit 1; } spctl --assess --type open --context context:primary-signature --verbose=2 "${dmg}" || { echo "::error::spctl assessment failed for ${dmg}."; exit 1; } done diff --git a/src/release-workflow.test.ts b/src/release-workflow.test.ts index 74bad9d7..6c2e32e2 100644 --- a/src/release-workflow.test.ts +++ b/src/release-workflow.test.ts @@ -303,10 +303,18 @@ describe('release workflow specification', () => { expect(notarize).toContain(`"\${RUNNER_TEMP}/AuthKey_\${APPLE_API_KEY}.p8"`); expect(notarize).toContain('xcrun notarytool submit'); expect(notarize).toContain(`if [ "\${status}" != "Accepted" ]; then`); - expect(notarize).toContain(`xcrun stapler staple "\${dmg}"`); + // stapler reaches api.apple-cloudkit.com for both staple and validate, + // and exit 68 covers a dropped connection as well as a missing ticket; + // three Intel jobs failed that way on 2026-10-14 after the ticket was + // issued (#461). Both steps therefore retry each call. + expect(notarize).toContain('stapler_retry() {'); + expect(notarize).toContain(`stapler_retry staple "\${dmg}"`); + expect(notarize).toContain(`stapler_retry validate "\${dmg}"`); // The artifact check holds the staged image to the same standard. - expect(smoke).toContain(`xcrun stapler validate "\${dmg}"`); + expect(smoke).toContain('stapler_retry() {'); + expect(smoke).toContain(`stapler_retry validate "\${dmg}"`); + expect(smoke).not.toContain(`xcrun stapler validate "\${dmg}"`); expect(smoke).toContain( `spctl --assess --type open --context context:primary-signature --verbose=2 "\${dmg}"`, );