#!/usr/bin/env bash # Moves the floating `v1` tag forward to a gated commit on `main`, and never # backwards. Run from a clone whose `origin` is this repository. # # release-v1.sh merge merge-triggered job: release # if it is still main's tip # release-v1.sh sweep-check scheduled sweep: report whether v1 lags # main (tip=, v1=, needed= to # $GITHUB_OUTPUT, or stdout without one) # release-v1.sh push # scheduled sweep, after gating the tip # # Every push is leased on the v1 value the caller reasoned about. A lost lease # means another writer moved v1 first: that is a clean skip once v1 is at or # ahead of , a retry against the new value while v1 is still behind # it, and a failure otherwise. set -euo pipefail MAX_ATTEMPTS=3 fetch_main() { git fetch -q origin +refs/heads/main:refs/remotes/origin/main git rev-parse refs/remotes/origin/main } # Prints origin's v1 commit, or nothing when origin has no v1. fetch_v1() { if [ -z "$(git ls-remote origin refs/tags/v1)" ]; then git update-ref -d refs/release-v1/seen 2>/dev/null || true return 0 fi git fetch -q origin +refs/tags/v1:refs/release-v1/seen git rev-parse 'refs/release-v1/seen^{commit}' } # True when v1 already covers : at it, or a descendant of it. covers() { local sha="$1" v1="$2" [ -n "$v1" ] && git merge-base --is-ancestor "$sha" "$v1" } push_leased() { local sha="$1" expect="$2" now attempt for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do if git push -q --force-with-lease="refs/tags/v1:$expect" origin "$sha:refs/tags/v1"; then echo "v1 moved ${expect:-} -> $sha" return 0 fi now=$(fetch_v1) if [ "$now" = "$expect" ]; then echo "ERROR: push of v1 -> $sha rejected while v1 was still ${expect:-} -- not a lost lease" >&2 return 1 fi if covers "$sha" "$now"; then echo "lost the lease: another writer moved v1 to $now, at or ahead of $sha -- nothing to do" return 0 fi if [ -n "$now" ] && ! git merge-base --is-ancestor "$now" "$sha"; then echo "ERROR: v1 moved to $now, which is neither behind nor ahead of $sha" >&2 return 1 fi echo "lost the lease: v1 moved to ${now:-}, still behind $sha -- retrying" expect="$now" done echo "ERROR: lost the lease on v1 $MAX_ATTEMPTS times running" >&2 return 1 } cmd="${1:?usage: release-v1.sh merge | sweep-check | push }" shift case "$cmd" in merge) SHA="${1:?usage: release-v1.sh merge }" TIP=$(fetch_main) if [ "$TIP" != "$SHA" ]; then echo "main's tip ($TIP) is past this run's gated commit ($SHA) -- deferring; the sweep releases the tip" exit 0 fi V1=$(fetch_v1) if covers "$SHA" "$V1"; then echo "v1 ($V1) already at or ahead of $SHA -- nothing to do" exit 0 fi push_leased "$SHA" "$V1" ;; sweep-check) TIP=$(fetch_main) V1=$(fetch_v1) if covers "$TIP" "$V1"; then NEEDED=false; else NEEDED=true; fi echo "main=$TIP v1=${V1:-} release-needed=$NEEDED" printf 'tip=%s\nv1=%s\nneeded=%s\n' "$TIP" "$V1" "$NEEDED" >> "${GITHUB_OUTPUT:-/dev/stdout}" ;; push) push_leased "${1:?usage: release-v1.sh push }" "${2-}" ;; *) echo "release-v1.sh: unknown command '$cmd'" >&2 exit 2 ;; esac