Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/release-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`,
);
Expand Down
Loading