The release pipeline survived its first real failure
Shipping TokenSight 1.3.0 taught me more about my release pipeline than building it did.
What 1.3.0 is
The session drill-down had been a fixed 460×520 popover with a hand-rolled list since the dashboard redesign. Fine for ten requests, useless for a real session — TokenSight regularly sees sessions with hundreds of requests, and the sheet had no sorting, no filtering, no way to find the one huge shell call in a sea of file reads.
1.3.0 replaces it with a real table: sortable columns (time, input, cached, output, total, dominant type), a free-text filter, a dominant-type dropdown, and a live N-of-M count over the 400 most recent requests. Two small fixes ride along: the daily-usage legend follows the By type / By model toggle (it was hardcoded to type rows, so the toggle looked inert), and the input/output icons had their metaphors reversed.
Gates before release: 53 tests, 0 failures, and parity PASS against the Python oracle — 216 sessions, 21 weeks, 49 tool labels, worst attributed drift 12.5%, inside the ≤ max(10K tokens, 3%) proportional tolerance. The oracle-first discipline from the Swift rewrite is still catching real drift.
The failure
scripts/release.sh is a linear pipeline: version bump → build + notarize the app → build the DMG → notarize the DMG → Sparkle-sign → appcast → push. It failed at step 3, mid-DMG-notarization:
Error: HTTPError(statusCode: nil, error: Error Domain=NSURLErrorDomain
Code=-1009 "The Internet connection appears to be offline."
The file had already been uploaded — “Successfully uploaded file, id: e0f7574f-…” — then the status polling died with no network route.
The recovery
The old instinct is to re-run the whole script. That re-builds, re-uploads, and burns ten minutes of notary latency to re-learn what the first run already knew. The better question: what does Apple already know?
xcrun notarytool history …
createdDate: 2026-08-31T20:26:38.238Z
id: e0f7574f-4bc3-410c-8003-379b839a97dc
name: TokenSight-1.3.0.dmg
status: Accepted ← accepted while the connection was down
Accepted. So the recovery was small and local:
xcrun stapler staplethe already-accepted DMG, thenvalidate.- Re-run Sparkle’s
sign_updateon the stapled DMG. - Append the appcast item, write the release-notes page, prune old downloads, commit, push — steps 4–5 of the release script, mirrored verbatim in a resume script.
No rebuild, no re-upload, no re-notarization. The failure cost one staple and a git push.
Why the pipeline shape mattered
Recovery was cheap because the pipeline is a linear script of inspectable steps over inspectable artifacts: the DMG on disk, the submission ID in the error, the appcast as plain XML. Every intermediate state could be examined and resumed from, instead of the failure being an opaque crash. The script also refuses to run without release notes — which sounds bureaucratic until it stops you shipping a build nobody can describe.
The Sparkle updater picks up the new appcast on its next check (≤ 6 hours, or Check for Updates). Distribution didn’t blink: the DMG that shipped is the exact artifact Apple accepted.