diff --git a/tools/release.sh b/tools/release.sh index b213ecfdb..c64ea305c 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -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 <