Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,57 @@ bump_version() {
echo "${major}.${minor}.${patch}"
}

# Restore VERSION / CHANGELOG.md if the release aborts after they are written
# but before the release commit lands. Without this, an abort (a flaky test, a
# failed tag) leaves a dirty tree — and main() refuses to run on a dirty tree,
# so the retry is blocked until someone works out what to revert.
release_files_written=false
release_committed=false

restore_release_files() {
[ "$release_files_written" = true ] || return 0
[ "$release_committed" = false ] || return 0

log_warn "Release aborted before committing — restoring VERSION and CHANGELOG.md"

# Restore from HEAD, not from the index: commit_and_tag stages both files
# before committing, so if the commit itself fails they are already staged
# and a plain `git checkout --` would restore them from the index — copying
# the modified versions back over themselves.
#
# One path per invocation. `git checkout HEAD -- a b` resolves the pathspec
# first and bails before touching the worktree if any entry is missing from
# HEAD, so a single call would restore *neither* file when one is untracked.
# That is reachable here: commit_and_tag guards CHANGELOG.md with `[ -f ]`,
# so this script already allows for a repo without one. git's stderr is left
# visible; a generic "couldn't restore" would send the next person down the
# wrong trail.
local f
for f in VERSION CHANGELOG.md; do
if git cat-file -e "HEAD:$f" 2>/dev/null; then
git checkout HEAD -- "$f" || \
log_warn "Could not restore $f; revert it by hand before retrying"
elif [ -f "$f" ]; then
# Absent from HEAD but on disk means this run created it: main()
# refuses to start on a dirty tree and `git status --porcelain`
# lists untracked files, so it cannot have pre-existed. Removing it
# restores the exact pre-run state. Both steps are needed —
# commit_and_tag may already have staged it, and a bare `rm` would
# leave an "A " entry that still counts as dirty.
git rm -f --quiet --ignore-unmatch -- "$f" 2>/dev/null || true
rm -f "$f"
log_warn "Removed $f, which this run created"
fi
done
}
trap restore_release_files EXIT

# Update version files
update_versions() {
local new_version=$1

log_step "Updating VERSION file to $new_version"
release_files_written=true
echo "$new_version" > VERSION

log_info "Version files updated to $new_version"
Expand Down Expand Up @@ -153,6 +199,10 @@ Release LVT CLI v$new_version

🤖 Generated with automated release script"

# VERSION / CHANGELOG.md now live in a commit; restoring them on a later
# failure would discard the release commit itself, so stand the guard down.
release_committed=true

log_step "Creating git tag v$new_version"
git tag -a "v$new_version" -m "Release v$new_version"

Expand Down Expand Up @@ -420,10 +470,15 @@ main() {
log_info "Starting release process..."
echo ""

# Execute release steps
# Execute release steps. Tests run FIRST, before anything is written: the
# only Go code reading the VERSION file is getCurrentVersion(), which no
# test exercises (and which resolves to "unknown" under `go test` anyway,
# since it reads a relative path from the package directory). So the bump
# cannot change the result, and a failure — a flaky test especially — leaves
# the tree untouched instead of half-bumped.
build_and_test
update_versions "$new_version"
generate_changelog "$new_version"
build_and_test
commit_and_tag "$new_version"
publish_github "$new_version" "$branch"

Expand Down
Loading