Adopt updated policies and simplify release publication

This commit is contained in:
2026-09-26 14:25:32 +00:00
parent f33af20524
commit 537bddf40c
11 changed files with 1301 additions and 440 deletions

122
scripts/release.sh Normal file
View 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.'