123 lines
5.0 KiB
Bash
123 lines
5.0 KiB
Bash
#!/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.'
|