Adopt updated policies and simplify release publication
This commit is contained in:
122
scripts/release.sh
Normal file
122
scripts/release.sh
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/bin/sh
|
||||
# Publish one validated source release; binary delivery is asynchronous.
|
||||
set -eu
|
||||
|
||||
die() {
|
||||
printf '%s\n' "release: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$#" -eq 1 ] || die 'usage: sh scripts/release.sh vMAJOR.MINOR.PATCH'
|
||||
version=$1
|
||||
case "$version" in
|
||||
*'
|
||||
'*) die 'invalid stable release version' ;;
|
||||
esac
|
||||
printf '%s\n' "$version" |
|
||||
grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' ||
|
||||
die 'invalid stable release version'
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
candidate=$(git rev-parse --verify 'HEAD^{commit}')
|
||||
tag_ref="refs/tags/$version"
|
||||
notes="docs/releases/$version.md"
|
||||
|
||||
# Inspect and publish to the same endpoint, including when origin has a
|
||||
# separate push URL. A raw URL avoids configured mirror/refspec behavior.
|
||||
remote=$(git remote get-url --push --all origin)
|
||||
case "$remote" in
|
||||
''|*'
|
||||
'*) die 'origin must have exactly one publication URL' ;;
|
||||
esac
|
||||
|
||||
check_candidate() {
|
||||
[ "$(git branch --show-current)" = main ] || die 'candidate must be on main'
|
||||
[ -z "$(git status --porcelain --untracked-files=all)" ] ||
|
||||
die 'worktree and index must be clean'
|
||||
[ "$(git rev-parse --verify 'HEAD^{commit}')" = "$candidate" ] ||
|
||||
die 'candidate changed during validation; validate the new candidate'
|
||||
[ "$(git remote get-url --push --all origin)" = "$remote" ] ||
|
||||
die 'origin publication URL changed during validation'
|
||||
}
|
||||
|
||||
read_remote() {
|
||||
remote_state=$(git ls-remote --refs "$remote" refs/heads/main "$tag_ref") ||
|
||||
return 1
|
||||
remote_main=$(printf '%s\n' "$remote_state" |
|
||||
awk '$2 == "refs/heads/main" { print $1 }')
|
||||
remote_tag=$(printf '%s\n' "$remote_state" |
|
||||
awk -v ref="$tag_ref" '$2 == ref { print $1 }')
|
||||
}
|
||||
|
||||
check_publication() {
|
||||
check_candidate
|
||||
if git show-ref --verify --quiet "$tag_ref"; then
|
||||
die 'version already exists locally; see docs/release.md for recovery'
|
||||
fi
|
||||
read_remote || die 'cannot establish authoritative publication state'
|
||||
[ "$remote_main" = "$candidate" ] ||
|
||||
die 'candidate must exactly match main on origin; publish main first'
|
||||
[ -z "$remote_tag" ] || die 'version already exists on origin'
|
||||
}
|
||||
|
||||
check_publication
|
||||
[ -z "$(git ls-tree -r --name-only "$candidate" -- go.work go.work.sum vendor)" ] ||
|
||||
die 'candidate must not contain a workspace or vendor tree'
|
||||
if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod; then
|
||||
die 'candidate must use published dependencies without go.mod replacements'
|
||||
fi
|
||||
git cat-file -e "$candidate:$notes" 2>/dev/null ||
|
||||
die 'matching release note is missing from the candidate'
|
||||
|
||||
release_tmp=$(mktemp -d "${TMPDIR:-/tmp}/scriptorium-release.XXXXXX")
|
||||
trap 'rm -rf -- "$release_tmp"' EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
git show "$candidate:$notes" >"$release_tmp/notes.md"
|
||||
awk -v version="$version" '
|
||||
NR == 1 {
|
||||
for (i = 2; i <= NF; i++) {
|
||||
if ($i == "Scriptorium") product = 1
|
||||
if ($i == version) matched = 1
|
||||
}
|
||||
valid = ($1 == "#" && product && matched)
|
||||
}
|
||||
END { exit !valid }
|
||||
' "$release_tmp/notes.md" || die 'release note title must contain Scriptorium and the exact version'
|
||||
|
||||
# Dependencies and the toolchain are provisioned before invocation. Do not
|
||||
# discover a workspace, use vendor mode, modify module metadata, or download
|
||||
# modules/toolchains during candidate validation.
|
||||
export GOWORK=off GOFLAGS=-mod=readonly GOPROXY=off GONOPROXY=none GOSUMDB=off GOTOOLCHAIN=local
|
||||
host_os=$(go env GOHOSTOS)
|
||||
host_arch=$(go env GOHOSTARCH)
|
||||
GOOS="$host_os" GOARCH="$host_arch" CGO_ENABLED=1 go test -race ./...
|
||||
for arch in amd64 arm64; do
|
||||
GOOS=linux GOARCH="$arch" CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' \
|
||||
-o "$release_tmp/scriptorium-$version-linux-$arch" ./cmd/scriptorium
|
||||
done
|
||||
|
||||
check_publication
|
||||
git tag --annotate --cleanup=verbatim \
|
||||
--file "$release_tmp/notes.md" "$version" "$candidate"
|
||||
tag_object=$(git rev-parse --verify "$tag_ref")
|
||||
[ "$(git cat-file -t "$tag_object")" = tag ] || die 'expected an annotated tag'
|
||||
[ "$(git rev-parse --verify "$tag_object^{commit}")" = "$candidate" ] ||
|
||||
die 'tag does not identify the validated candidate'
|
||||
check_candidate
|
||||
|
||||
# Freeze the source object and name only the intended destination ref. Disable
|
||||
# automatic following of other annotated tags even if enabled in user config.
|
||||
if git -c push.followTags=false push "$remote" "$tag_object:$tag_ref"; then
|
||||
: # Successful pushes do not need another network read-back gate.
|
||||
else
|
||||
printf '%s\n' 'release: push failed; resolving authoritative remote state' >&2
|
||||
read_remote || die 'publication status unknown; preserve the local tag and inspect origin before retrying'
|
||||
if [ "$remote_tag" != "$tag_object" ] || [ "$remote_main" != "$candidate" ]; then
|
||||
die 'publication not confirmed; preserve the local tag and follow docs/release.md recovery'
|
||||
fi
|
||||
printf '%s\n' 'release: origin confirms the intended tag despite the push error'
|
||||
fi
|
||||
|
||||
printf 'Source release %s published: commit %s, tag %s\n' "$version" "$candidate" "$tag_object"
|
||||
printf '%s\n' 'Binary delivery: asynchronous, not verified.'
|
||||
Reference in New Issue
Block a user