Simplify source releases with one guarded publication command
This commit is contained in:
56
scripts/release.sh
Executable file
56
scripts/release.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
fail() { printf '%s\n' "release: $*" >&2; exit 1; }
|
||||
|
||||
[ "$#" -eq 1 ] || fail "usage: $0 vMAJOR.MINOR.PATCH"
|
||||
version=$1
|
||||
printf '%s\n' "$version" | grep -E -x 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' >/dev/null || fail "invalid stable release version"
|
||||
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||
cd "$script_dir/.."
|
||||
export GOWORK=off
|
||||
candidate=$(git rev-parse 'HEAD^{commit}')
|
||||
|
||||
check_candidate() {
|
||||
[ "$(git branch --show-current)" = main ] || fail "release branch must be main"
|
||||
[ -z "$(git status --porcelain)" ] || fail "worktree and index must be clean"
|
||||
[ "$(git rev-parse 'HEAD^{commit}')" = "$candidate" ] || fail "candidate changed during validation"
|
||||
git cat-file -e "$candidate:docs/releases/$version.md" || fail "release note must be committed"
|
||||
}
|
||||
|
||||
check_publication() {
|
||||
# Query failure is not proof that the tag is unused.
|
||||
remote_tag=$(git ls-remote --tags origin "refs/tags/$version" "refs/tags/$version^{}") || fail "cannot determine remote tag availability"
|
||||
[ -z "$remote_tag" ] || fail "remote tag already exists: $version"
|
||||
if git show-ref --verify --quiet "refs/tags/$version"; then
|
||||
fail "local tag already exists: $version"
|
||||
fi
|
||||
git fetch --no-tags origin refs/heads/main || fail "cannot fetch authoritative main"
|
||||
remote_main=$(git rev-parse 'FETCH_HEAD^{commit}')
|
||||
git merge-base --is-ancestor "$remote_main" "$candidate" || fail "candidate does not contain authoritative main"
|
||||
}
|
||||
|
||||
check_candidate
|
||||
check_publication
|
||||
"$script_dir/check-release-source.sh" "$version"
|
||||
check_candidate
|
||||
check_publication
|
||||
|
||||
if ! git -c push.followTags=false push origin "$candidate:refs/heads/main"; then
|
||||
# Do not publish a tag after a failed branch push, even if the branch arrived.
|
||||
git ls-remote origin refs/heads/main >&2 || fail "branch publication status unknown; inspect origin before retrying"
|
||||
fail "branch push failed; inspect the reported remote state before retrying"
|
||||
fi
|
||||
|
||||
git -c tag.gpgSign=false tag "$version" "$candidate"
|
||||
[ "$(git cat-file -t "$version")" = commit ] || fail "tag is not lightweight"
|
||||
[ "$(git rev-parse "$version^{commit}")" = "$candidate" ] || fail "tag target changed"
|
||||
if ! git -c push.followTags=false push origin "refs/tags/$version:refs/tags/$version"; then
|
||||
# A connection can fail after the server accepted the tag. Preserve it locally
|
||||
# until authoritative state proves whether publication succeeded.
|
||||
remote_tag=$(git ls-remote --tags origin "refs/tags/$version" "refs/tags/$version^{}") || fail "tag publication status unknown; preserve local tag and inspect origin"
|
||||
expected=$(printf '%s\t%s' "$candidate" "refs/tags/$version")
|
||||
[ "$remote_tag" = "$expected" ] || fail "tag push failed; preserve local tag and inspect origin before retrying"
|
||||
fi
|
||||
printf 'Released %s at %s\n' "$version" "$candidate"
|
||||
Reference in New Issue
Block a user