fix(release): make appcast publish independently idempotent, validate sign_update binary#123
Merged
Merged
Conversation
… sign_update binary Previously the only idempotency gate was the v<version> git tag, checked once and used to skip every subsequent step. If a run died after `gh release create` (tag now exists) but before the appcast branch push — network blip, token expiry, etc. — a re-run would see the tag, skip the entire pipeline, and leave the release permanently invisible to Sparkle's auto-update feed. Add a second, independent check (`check_appcast`) that inspects the appcast branch directly for an <item> matching the current version, and combine it with the tag check into a single `phase` step that derives two decisions: `needs_build` and `needs_appcast`. All step conditions now gate on these instead of the raw tag check, giving three states: - tag missing -> needs_build=true, needs_appcast=true (full run) - tag exists, appcast entry missing -> needs_build=false, needs_appcast=true (recovery) - tag exists, appcast entry present -> needs_build=false, needs_appcast=false (no-op) In the recovery state, build/sign/notarize are skipped and a new step downloads the already-published DMG/ZIP from the existing GitHub release into the same paths the build steps would have produced, so every downstream step (SHA256, sign_update, appcast append, cask update) is unaware of which path produced its input. A "Resolve Sparkle CLI tools" step ensures sign_update is available even when the full app build was skipped. Both places that read the appcast branch now use `git ls-remote --exit-code` to distinguish "branch does not exist" (exit 2, first release ever) from a transient query failure. Previously a transient `git fetch` failure in the append step was treated as "branch absent", seeding from the empty in-repo template and force-pushing — wiping the accumulated feed history; in the new check_appcast step the same conflation would have appended a duplicate <item>. Transient failures now abort the step loudly instead. Also fix a separate sign_update usability bug: the step blamed the private key whenever the signature came back empty, even when the binary itself was missing (e.g., because the Sparkle package artifacts were never resolved). It now checks the binary exists and is executable first, failing with the exact missing path, and the empty-signature error now names both possible causes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gh release create(tag now exists) and the appcast branch push left the release permanently invisible to Sparkle users: re-runs saw the tag and skipped everything. A newcheck_appcaststep inspects the appcast branch for the version's<item>, and aphasestep derivesneeds_build/needs_appcast; steps gate on those. Recovery path (tag exists, appcast entry missing) downloads the already-published DMG/ZIP from the release into the same paths the build would have produced, resolves the Sparkle CLI tools on demand, then runs the unchanged sign/append/push/cask steps.git fetchfailure as "branch absent"; in the append step that seeded from the empty in-repo template and force-pushed, wiping the accumulated feed. Both now usegit ls-remote --exit-codeto distinguish confirmed-absent (exit 2) from transient failure, which aborts loudly.CMDIME_SPARKLE_PRIVATE_ED_KEYwhenever the signature came back empty, even when the hardcoded binary path was simply missing. It now checks the binary exists/is executable first and names the actual path on failure.States
Validation
No workflow run possible pre-merge: YAML parse (PyYAML),
bash -non all run blocks, appcast grep/awk logic simulated against real appcast content, signing/notarization/team-ID gates verified untouched.🤖 Generated with Claude Code