fix(release): survive an aborted release instead of leaving a half-bumped tree - #339
Conversation
…mped tree Same ordering bug as livetemplate#499, found there and confirmed here. The order was update_versions -> generate_changelog -> build_and_test, so a test failure aborted with VERSION and CHANGELOG.md already rewritten. main() refuses to run on a dirty tree, so the retry was blocked until someone worked out what to revert. - build_and_test now runs FIRST. The only Go code reading the VERSION file is getCurrentVersion(), which no test exercises and which resolves to "unknown" under 'go test' anyway (it reads a relative path from the package directory), so the bump cannot change the result. - A trap restores VERSION/CHANGELOG.md if the run still aborts between writing them and committing. It restores from HEAD rather than the index, because commit_and_tag stages both files before committing: on a failed commit they are already staged, and a plain 'git checkout --' would restore them from the index, copying the modified versions back over themselves. Verified in a throwaway clone: a forced failure after the bump leaves VERSION at 0.2.0 with zero dirty files, and the reorder is visible in the log (tests run before the version bump). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReviewReordering Correctness
Style / minor nits
Test coverageNo automated coverage (no existing bats/shell test harness for Security / performanceNo concerns — this only touches local git plumbing in an interactive release script; no new external inputs or elevated privileges introduced. Overall: correct fix for the bug it targets, with thoughtful comments explaining non-obvious reasoning. The one edge case above is worth a look but doesn't block given the current repo state. |
git checkout HEAD -- a b resolves the pathspec before touching the worktree and bails if any entry is missing from HEAD, so one call would restore *neither* file when CHANGELOG.md is untracked. That is reachable here: commit_and_tag guards CHANGELOG.md with [ -f ], so this script already allows for a repo without one. Verified in a throwaway clone with CHANGELOG.md untracked: the single-call form left VERSION bumped; per-path restore returns it to 0.2.0 and reports the untracked file with an actionable message. Also stopped swallowing git's stderr, and moved the trap block above the '# Update version files' header it was awkwardly sitting under. Addresses claude-review feedback on #339. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
All three applied — the pathspec one was a real bug, thanks. Applied: single-call You were also right that it's reachable here specifically: Applied: Applied: comment block under the wrong header. Moved the trap machinery above Acknowledged, no change: no automated coverage for |
|
Reviewed the current diff (both commits). This is a solid, well-reasoned fix — the reasoning trail in the commit messages made it easy to verify. Correctness
Minor/non-blocking observations
Security/performance: no concerns — this only touches local git plumbing in a release script gated behind interactive confirmation and existing Overall: the fix addresses a real, previously-hit pain point (per the linked livetemplate#499 precedent), and the trap logic holds up under the edge cases I traced through by hand. |
The trap restored tracked files but only told the operator to delete an untracked CHANGELOG.md by hand, so the 'retry unblocked' guarantee held for the common case and not the edge one: git status --porcelain lists untracked and staged-new files alike, and main() refuses to start on any of them. Removing it is provably safe rather than a guess. main() already refuses to start on a dirty tree, and --porcelain reports untracked files, so a CHANGELOG.md absent from HEAD at trap time cannot have pre-existed — this run created it. Both git rm and rm are needed: commit_and_tag may already have staged it, and a bare rm would leave an 'A ' entry that still counts as dirty. Verified in throwaway clones, both branches of the condition: with CHANGELOG.md untracked the abort now leaves zero dirty files; with it tracked it is restored rather than deleted. Raised by claude-review on lvt#339. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Applied the The description promised "retry unblocked," but On safety — removing it is provable here, not a judgement call:
So removing it restores the exact pre-run state rather than destroying anything the operator had. One wrinkle your suggestion didn't mention and my scenario-2 test surfaced: the file may already be staged, since Re-verified both branches of the condition in throwaway clones:
Same change applied to livetemplate#499, which shares this function. |
ReviewWell-scoped fix with unusually thorough comments explaining the why. I read through What it does
Correctness
Test coverage
Style / minor nit
SecurityNo concerns — no new external input handling, no injection surface introduced (all variables used in Overall: solid, well-reasoned fix for a real operational trap. The main gap is the missing regression test given how easy this class of bug is to reintroduce silently. |
|
Thanks — and noting this as the convergence point: this round raised no new functional issue, it independently re-derived the reasoning (including checking On the coupling you flagged — "if The code comment currently states the premise (" Leaving it as-is deliberately rather than pushing a fourth revision for a comment, since a push costs another full CI + review cycle. Worth folding in whenever this function is next touched — and if you'd rather have it now, say so and I'll add it. For the record, the three rounds each caught something real, all of which survived reading the diff and only fell out of running the abort:
That's a decent argument for the bats suite you mentioned, if this grows further. |
Same ordering bug as livetemplate#499 — found while releasing livetemplate v0.19.1, then confirmed present here.
The problem
The step order was
update_versions→generate_changelog→build_and_test, so a test failure aborted withVERSIONandCHANGELOG.mdalready rewritten:main()refuses to start on a dirty tree, so the retry is blocked until someone works out what to revert — a confusing trap at exactly the wrong moment, and the failure that triggers it is most likely a flake.The fix
build_and_testruns first. Worth a note for this repo specifically, because unlike core, lvt does read theVERSIONfile from Go —getCurrentVersion()incommands/install_agent.go. It's safe regardless: no test exercises it, and it resolves to"unknown"undergo testanyway since it reads a relative path from the package directory.A trap covers the remaining window between writing the files and committing. It restores from
HEAD, not the index:That was a real bug in the first draft of the core fix, caught by the abort test rather than by reading the diff.
Verification
In a throwaway clone with a forced post-bump failure:
VERSIONrestored to0.2.0, 0 dirty files, retry unblockedRunning Go tests…precedesUpdating VERSION file🤖 Generated with Claude Code
https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG