#!/usr/bin/env bash set -euo pipefail repository_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "$repository_dir" remote_url=https://gitea.maximumdirect.net/eric/ConfigKit.git fail() { printf '%s\n' "$*" >&2 exit 1 } check_candidate() { local current status current=$(git branch --show-current) || fail "cannot read the current branch" [[ "$current" == main ]] || fail "release requires the main branch" status=$(git status --porcelain=v1 --untracked-files=all) || fail "cannot inspect the worktree" [[ -z "$status" ]] || fail "release requires a clean index and worktree" current=$(git rev-parse --verify HEAD) || fail "cannot identify the candidate" [[ "$current" == "$candidate" ]] || fail "candidate changed; commit and validate it again" } check_unused_version() { if git show-ref --verify --quiet "refs/tags/$version"; then fail "version already has a local tag" fi local oid result if oid=$(read_remote_ref "refs/tags/$version"); then fail "version already has an authoritative tag" else result=$? ((result == 2)) || fail "cannot determine authoritative tag state; publication status is unknown" fi } read_remote_ref() { local ref=$1 response result if response=$(git ls-remote --refs --exit-code "$remote_url" "$ref" 2>/dev/null); then [[ "$response" == *$'\t'"$ref" ]] || return 3 printf '%s\n' "${response%%$'\t'*}" else result=$? ((result == 2)) && return 2 return 3 fi } if (($# != 1)); then fail "usage: ./scripts/release.sh vMAJOR.MINOR.PATCH" fi version=$1 [[ "$version" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || fail "release version must be a stable vMAJOR.MINOR.PATCH without leading zeroes" fetch_urls=$(git remote get-url --all origin 2>/dev/null) || fail "origin is unavailable" push_urls=$(git remote get-url --push --all origin 2>/dev/null) || fail "origin push destination is unavailable" [[ "$fetch_urls" == "$remote_url" && "$push_urls" == "$remote_url" ]] || fail "origin must have exactly the authoritative fetch and push destination" candidate=$(git rev-parse --verify HEAD) || fail "cannot identify the candidate" check_candidate check_unused_version note_path="docs/releases/$version.md" note_file=$(mktemp) trap 'rm -f -- "$note_file"' EXIT git show "$candidate:$note_path" >"$note_file" 2>/dev/null || fail "matching release note is absent from the candidate" IFS= read -r title <"$note_file" || [[ -n "$title" ]] || fail "matching release note is empty" [[ "$title" == "# ConfigKit $version" ]] || fail "release note title does not match the version" if remote_main=$(read_remote_ref refs/heads/main); then : else result=$? ((result == 2)) && fail "authoritative main is absent" fail "cannot read authoritative main; publication status is unknown" fi git fetch --no-tags "$remote_url" refs/heads/main >/dev/null || fail "cannot fetch authoritative main" fetched_main=$(git rev-parse --verify FETCH_HEAD) || fail "cannot inspect fetched main" git merge-base --is-ancestor "$fetched_main" "$candidate" || fail "candidate cannot advance authoritative main without discarding history" ./scripts/release-check.sh || fail "release validation failed; no refs were published" check_candidate check_unused_version if git push --no-follow-tags "$remote_url" "$candidate:refs/heads/main"; then : else if observed_main=$(read_remote_ref refs/heads/main); then [[ "$observed_main" == "$candidate" ]] || fail "main push did not publish; authoritative main differs from the candidate" else result=$? ((result == 2)) && fail "main push failed; authoritative main is absent" fail "main push outcome is unknown; inspect authoritative main before retrying" fi fi check_candidate git tag -a -F "$note_file" --cleanup=verbatim "$version" "$candidate" || fail "could not create the local annotated tag" tag_target=$(git rev-parse --verify "refs/tags/$version^{}") || fail "cannot verify the local tag target" [[ "$tag_target" == "$candidate" ]] || fail "local tag does not identify the candidate" tag_type=$(git cat-file -t "refs/tags/$version") || fail "cannot inspect the local tag" [[ "$tag_type" == tag ]] || fail "release tag must be annotated" tag_oid=$(git rev-parse --verify "refs/tags/$version") || fail "cannot identify the annotated tag" if git push --no-follow-tags "$remote_url" "refs/tags/$version:refs/tags/$version"; then : else if observed_tag=$(read_remote_ref "refs/tags/$version"); then [[ "$observed_tag" == "$tag_oid" ]] || fail "authoritative tag conflicts with the local tag; never replace it" else result=$? ((result == 2)) && fail "tag push failed; main is published, but the local tag is unpublished" fail "tag push outcome is unknown; retain the local tag and inspect authoritative state" fi fi printf 'Published %s at %s\n' "$version" "$candidate"