#!/usr/bin/env bash set -euo pipefail fail() { echo "Release stopped: $*" >&2; exit 1; } if [[ $# != 2 || ( $1 != --check && $1 != --publish ) || ! $2 =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo 'Usage: scripts/release.sh --check|--publish vMAJOR.MINOR.PATCH' >&2 exit 2 fi mode=$1 version=$2 cd "$(dirname "$0")/.." [[ $(git symbolic-ref --quiet --short HEAD) == main ]] || fail 'switch to main' [[ -z $(git status --porcelain) ]] || fail 'commit or remove outstanding changes first' candidate=$(git rev-parse HEAD) note="docs/releases/$version.md" [[ -s $note ]] || fail "missing release note: $note" IFS= read -r title < "$note" || true [[ $title == "# Audita $version" ]] || fail "note must begin with # Audita $version" git show-ref --verify --quiet "refs/tags/$version" && fail 'version already exists locally' # Check mode is local and never contacts a remote or creates a tag. if [[ $mode == --publish ]]; then existing=$(git ls-remote --tags origin "refs/tags/$version") [[ -z $existing ]] || fail 'version already exists on origin' fi build_dir=$(mktemp -d) trap 'rm -rf "$build_dir"' EXIT GOWORK=off go test -mod=readonly ./... bash scripts/build-release.sh "$version" "$build_dir" [[ $(git rev-parse HEAD) == "$candidate" ]] || fail 'HEAD changed during validation' [[ -z $(git status --porcelain) ]] || fail 'worktree changed during validation' [[ $(git symbolic-ref --quiet --short HEAD) == main ]] || fail 'branch changed during validation' if [[ $mode == --check ]]; then echo "Validated $version at $candidate; nothing published." exit 0 fi # Fetch the authoritative branch before checking that this is a forward update. git fetch --no-tags origin refs/heads/main git merge-base --is-ancestor FETCH_HEAD "$candidate" || fail 'candidate does not include origin/main' existing=$(git ls-remote --tags origin "refs/tags/$version") [[ -z $existing ]] || fail 'version already exists on origin' git tag "$version" "$candidate" [[ $(git rev-parse "refs/tags/$version") == "$candidate" ]] || fail 'tag target changed' # Atomic publication prevents a rejected tag/branch update from publishing only half. if ! git push --atomic origin "$candidate:refs/heads/main" "refs/tags/$version:refs/tags/$version"; then fail "push failed or its outcome is uncertain; inspect origin/main and refs/tags/$version before retrying (local tag retained)" fi echo "Released $version at $candidate. Binary publication runs asynchronously; CI is not awaited."