Skip to content

fix(release): keep the tag on main and push it atomically - #64

Merged
charliewwdev merged 2 commits into
mainfrom
fix/release-script-tag-on-main
Sep 1, 2026
Merged

fix(release): keep the tag on main and push it atomically#64
charliewwdev merged 2 commits into
mainfrom
fix/release-script-tag-on-main

Conversation

@charliewwdev

Copy link
Copy Markdown
Member

Root cause of the "tag exists but no release / wrong contents" problem behind #57.

What went wrong

Two releases were published from commits that are not on mainv0.9.24 and v0.9.36. The tag is what triggers release.yml, so npm and pub.dev shipped a tree that exists nowhere in the branch history.

v0.9.36 is the clearest case:

time (UTC+8) event
04-14 10:47 PR #38 merged into origin/main376cdd7b
04-15 14:48:57 #43 fix committed on a stale local main, still at v0.9.35
04-15 14:49:27 release.sh creates chore: Release v0.9.36 (b73fd0c2) + tag
04-15 14:49:32 CI starts from b73fd0c2 — the tag landed, the branch did not
04-15 14:50:03 pull --rebase replays both commits onto 376cdd7b2e6f6ba3 + 1ce1ecca

Evidence that the last step was a rebase rather than two separate edits: the pairs share author dates to the second but have committer dates 36 s later, and git patch-id is identical (41ffdbba…). gh run view confirms the release run's headSha is b73fd0c2, the orphaned commit.

Why the tag landed but the branch didn't

git push origin main --tags updates each ref independently. Reproduced against a scratch remote:

 * [new tag]         v9.9.9 -> v9.9.9
 ! [rejected]        main -> main (fetch first)

The remote is left holding a tag that is not on main — exactly the production state. set -e does not help: the tag is already delivered by the time the command reports failure.

Consequence

Everything merged after the divergence never shipped: #38, #47, #51, #53, #55, #58, #59. #51 is the painful one — released v0.9.36 still has packaging/npm/dart/bin/server.dart importing package:flutter_skill/… while the vendored pubspec is name: flutter_skill_npm, so the Dart fallback cannot compile. That is the direct cause of the errors still open in #45, #49 and #50.

Changes

Pre-flight, before any file is touched — the version bumps run before the confirmation prompt, so a late abort leaves a dirty tree:

  • HEAD must be main
  • the tag must not already exist
  • main must match origin/main exactly, reporting whether it is ahead or behind and what to do about it

Pushgit push --atomic origin main "refs/tags/v$VERSION":

  • --atomic means a rejected branch update can no longer let the tag through
  • naming the single tag avoids --tags sweeping up unrelated local tags
  • on failure the local tag is deleted so a retry starts clean

Test plan

  • Atomic push: same divergence against a scratch remote → ! [rejected] v9.9.9 (atomic push failed), remote has no tag. The old form pushed the tag.
  • Wrong branch → aborts with Releases must be cut from main
  • Tag exists (0.9.36) → aborts
  • Behind origin/main — the v0.9.36 scenario, tested in a scratch clone reset by 2 commits → behind: 6 commit(s) ahead: 0, aborts, suggests git pull --rebase
  • Ahead of origin/mainbehind: 0 ahead: 1, aborts, suggests git push origin main
  • Verified no file is modified in any abort path
  • bash -n scripts/release.sh

Not exercised: a real end-to-end release, which needs a live tag push.

Follow-up

v0.9.36 still points at the orphaned commit. Once the queued PRs merge, the next release.sh run cuts 0.9.37 from main and picks up all seven stranded PRs — worth spelling out in the CHANGELOG, since the change volume is far larger than the version bump suggests.

🤖 Generated with Claude Code

Two releases were published from commits that are not on main. The tag is
what triggers the release workflow, so the artifacts on npm and pub.dev
were built from a tree nobody can see.

v0.9.36 is the clearest case. PR #38 merged into origin/main on 04-14
02:47Z. On 04-15 06:48Z the release was cut from a stale local main still
at v0.9.35, and `git push origin main --tags` pushed each ref
independently: the branch was rejected as non-fast-forward, the tag went
through anyway. CI started from that orphaned commit 5 seconds later. The
`pull --rebase` that followed 31 seconds after replayed the same two
commits onto origin/main — same author dates, later committer dates,
identical patch-ids — leaving the tag on the pre-rebase copy. v0.9.24
failed the same way on 03-17. Reproduced the ref-splitting behaviour
against a scratch remote.

Before touching any file the script now requires that HEAD is main, that
the tag does not already exist, and that main matches origin/main
exactly, naming whether it is ahead or behind. Aborting this early
matters because the version bumps run before the confirmation prompt.

The push is now `--atomic` and names only this release's tag, so a
rejected branch update can no longer let the tag through, and unrelated
local tags are not swept along by `--tags`. On failure the local tag is
deleted so a retry starts clean.
The generated entry carries a "TODO: Add your changes here" line and the
warning to replace it only prints, so it is easy to walk past. Ten
released versions have that literal string as their entire changelog,
including the three most recent.

Block on it instead. The check reads only the section for the version
being released, so an older unfilled entry does not stop a new release.
@charliewwdev

Copy link
Copy Markdown
Member Author

Added a second guard to this PR while preparing the 0.9.37 release.

release.sh generates the CHANGELOG entry with a TODO: Add your changes here line and only warns that it should be replaced. That warning has been missed ten times — including the three most recent releases:

## 0.9.36
**Fix EACCES: ensure native binary has execute permission before spawn**
### Changes
- TODO: Add your changes here

The script now refuses to proceed while the placeholder is still there. The check is scoped to the section for the version being released (awk stops at the next ## heading), so the ten existing unfilled entries do not block future releases — verified against the real 0.9.36 entry (blocks), a filled entry (passes), and a version with no entry at all (passes).

@charliewwdev
charliewwdev merged commit fb79c8f into main Sep 1, 2026
4 of 5 checks passed
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