Skip to content

fix(release): make appcast publish independently idempotent, validate sign_update binary#123

Merged
kazukinakai merged 1 commit into
mainfrom
fix/release-appcast-idempotency
Jul 5, 2026
Merged

fix(release): make appcast publish independently idempotent, validate sign_update binary#123
kazukinakai merged 1 commit into
mainfrom
fix/release-appcast-idempotency

Conversation

@kazukinakai

Copy link
Copy Markdown
Member

Summary

  • Unrecoverable half-published release — the only idempotency gate was the version tag, checked once and used to skip every step. A run dying between 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 new check_appcast step inspects the appcast branch for the version's <item>, and a phase step derives needs_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.
  • Feed-history wipe on transient fetch failure — both places reading the appcast branch treated any git fetch failure as "branch absent"; in the append step that seeded from the empty in-repo template and force-pushed, wiping the accumulated feed. Both now use git ls-remote --exit-code to distinguish confirmed-absent (exit 2) from transient failure, which aborts loudly.
  • sign_update diagnostics — the step blamed CMDIME_SPARKLE_PRIVATE_ED_KEY whenever 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

tag appcast entry behavior
missing full run (unchanged)
exists missing recovery: download assets → sign → appcast → cask
exists present no-op

Validation

No workflow run possible pre-merge: YAML parse (PyYAML), bash -n on all run blocks, appcast grep/awk logic simulated against real appcast content, signing/notarization/team-ID gates verified untouched.

🤖 Generated with Claude Code

… 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>
@kazukinakai kazukinakai enabled auto-merge July 5, 2026 06:14
@kazukinakai kazukinakai merged commit ce3915c into main Jul 5, 2026
2 checks passed
@kazukinakai kazukinakai deleted the fix/release-appcast-idempotency branch July 5, 2026 06:17
kazukinakai added a commit that referenced this pull request Jul 5, 2026
Patch release shipping the fixes merged in #122 (event-tap rebuild leak,
modifier-chord false fire, media-key stuck keyUp), #124 (settings
persistence and UI fixes), and #123 (release pipeline idempotency).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant