tools/release.sh: fix idempotency gaps in branch checks, SRPM reuse, release notes (#2136)

This commit is contained in:
John Kerl 2026-07-03 20:56:51 -04:00 committed by GitHub
parent 486f013870
commit 3682077d32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -35,6 +35,13 @@
# safe to re-run after a partial failure. `rpmbuild` is mandatory for
# `pre-release` and the script aborts in phase 0 if missing; `rpmlint` is
# optional (phase 3 runs it if installed, otherwise skips the lint step).
#
# Phase 0 also refuses to run `pre-release`/`docs` unless the working tree is
# currently on `--branch` (default `main`): committing/pushing from the wrong
# branch would otherwise fail silently, since `git push origin main` reports
# "Everything up-to-date" when the local `main` ref itself has no new commits,
# even if HEAD (on some other branch) does. `afterwork` is exempt since it
# explicitly `git checkout`s the branch itself.
set -euo pipefail
@ -101,6 +108,25 @@ confirm() {
esac
}
# push_if_needed pushes $1 (a local branch that is presumed checked out, or
# at least resolvable) to origin, but skips the confirmation prompt and the
# push entirely when origin already has those exact commits -- e.g. a
# resumed run whose earlier attempt already pushed successfully. Any
# additional args (e.g. -u) are passed through to `git push`.
push_if_needed() {
local branch="$1"; shift
run_cmd git fetch origin "$branch" >/dev/null 2>&1 || true
local local_sha remote_sha
local_sha="$(git rev-parse "$branch" 2>/dev/null || true)"
remote_sha="$(git rev-parse "origin/$branch" 2>/dev/null || true)"
if [ -n "$local_sha" ] && [ "$local_sha" = "$remote_sha" ]; then
note "origin/$branch already matches local $branch (idempotent: skipping push)"
return 0
fi
confirm "push branch '$branch' to origin?"
run_cmd git push "$@" origin "$branch"
}
# ============================================================================
# Argument parsing
# ============================================================================
@ -187,6 +213,20 @@ preflight_common() {
die "working tree has uncommitted changes; commit or stash first"
fi
# `afterwork` explicitly checks out $BRANCH itself, so it self-heals from
# being invoked on the wrong branch. `pre-release` and `docs` do not: they
# commit/push against whatever is currently checked out, so catch a stray
# branch here rather than silently committing to it (and having
# `git push origin $BRANCH` report "Everything up-to-date" without actually
# publishing the new commit).
if [ "$SUBCOMMAND" != "afterwork" ]; then
local current_branch
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$current_branch" != "$BRANCH" ]; then
die "currently on branch '$current_branch', expected '$BRANCH' -- 'git checkout $BRANCH' first, or pass --branch '$current_branch' if that's intentional"
fi
fi
log "preflight (common) ok"
}
@ -382,8 +422,7 @@ phase_1_bump_versions() {
run_cmd git commit -m "Prepare ${VERSION} release"
fi
confirm "push branch '$BRANCH' to origin?"
run_cmd git push origin "$BRANCH"
push_if_needed "$BRANCH"
log "phase 1 complete"
}
@ -434,13 +473,30 @@ phase_3_srpm() {
run_cmd mkdir -p "$rpm_top/SPECS" "$rpm_top/SOURCES" "$rpm_top/SRPMS"
# Check for an already-built SRPM for this version before rebuilding.
# Reuse is content-aware, not just version-keyed: a same-named SRPM from
# an earlier aborted run could have been built from a since-superseded
# tarball (e.g. a bugfix rebuild before the tag was ever pushed), so we
# only reuse it if its recorded source-tarball sha256 still matches the
# tarball on disk right now.
local tgz_sha=""
if [ "$DRY_RUN" != "yes" ]; then
tgz_sha="$(shasum -a 256 "$tgz" | awk '{ print $1 }')"
fi
local existing
existing="$(ls -1 "$rpm_top"/SRPMS/miller-"${VERSION}"-1*.src.rpm 2>/dev/null | head -n1 || true)"
if [ -n "$existing" ]; then
note "existing SRPM found (idempotent: reusing): $existing"
SRPM_PATH="$existing"
log "phase 3 complete"
return 0
local sha_marker="${existing}.srctarball.sha256"
local recorded_sha=""
[ -f "$sha_marker" ] && recorded_sha="$(cat "$sha_marker")"
if [ -n "$tgz_sha" ] && [ "$recorded_sha" = "$tgz_sha" ]; then
note "existing SRPM found and matches current $tgz sha256 (idempotent: reusing): $existing"
SRPM_PATH="$existing"
log "phase 3 complete"
return 0
else
warn "existing SRPM found but its recorded source tarball sha256 doesn't match current $tgz -- rebuilding: $existing"
fi
fi
run_cmd cp miller.spec "$rpm_top/SPECS/"
@ -466,6 +522,7 @@ phase_3_srpm() {
else
SRPM_PATH="$(ls -1 "$rpm_top"/SRPMS/miller-"${VERSION}"-1*.src.rpm | head -n1)"
[ -n "$SRPM_PATH" ] && [ -f "$SRPM_PATH" ] || die "rpmbuild reported success but no SRPM found under $rpm_top/SRPMS"
echo "$tgz_sha" > "${SRPM_PATH}.srctarball.sha256"
log "built SRPM: $SRPM_PATH"
fi
@ -483,7 +540,16 @@ phase_4_github_release() {
[ -n "${SRPM_PATH:-}" ] || die "SRPM_PATH not set -- phase 3 did not run?"
if gh release view "$TAG" >/dev/null 2>&1; then
note "gh release $TAG already exists (idempotent: will only upload missing assets)"
note "gh release $TAG already exists"
if [ -n "$NOTES_FILE" ]; then
# Resuming with a (possibly edited) --notes-file should still take
# effect -- otherwise editing NOTES_FILE and re-running silently
# leaves the already-created release's notes frozen at first creation.
log "updating existing release notes from '$NOTES_FILE'"
run_cmd gh release edit "$TAG" --notes-file "$NOTES_FILE"
else
note "no --notes-file given; leaving existing release notes/title untouched"
fi
else
[ -n "$NOTES_FILE" ] || die "gh release $TAG does not exist and --notes-file was not provided"
confirm "create GitHub pre-release $TAG from notes file '$NOTES_FILE'?"
@ -567,8 +633,7 @@ phase_5_docs_branch() {
run_cmd git commit -m "docs: Miller $VERSION Documentation site_name"
fi
confirm "push branch '$VERSION' to origin?"
run_cmd git push -u origin "$VERSION"
push_if_needed "$VERSION" -u
banner "PHASE 5: manual ReadTheDocs steps"
cat <<EOF
@ -626,8 +691,7 @@ phase_6_afterwork_bump() {
run_cmd git commit -m "Post-${VERSION} release: back to ${target}"
fi
confirm "push branch '$BRANCH' to origin?"
run_cmd git push origin "$BRANCH"
push_if_needed "$BRANCH"
fi
log "phase 6 complete"