134 lines
5.4 KiB
Bash
Executable File
134 lines
5.4 KiB
Bash
Executable File
#!/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 context=$1 current status
|
|
current=$(git branch --show-current) || fail "$context: cannot read the current branch"
|
|
[[ "$current" == main ]] || fail "$context: release requires the main branch"
|
|
status=$(git status --porcelain=v1 --untracked-files=all) || fail "$context: cannot inspect the worktree"
|
|
[[ -z "$status" ]] || fail "$context: release requires a clean index and worktree"
|
|
current=$(git rev-parse --verify HEAD) || fail "$context: cannot identify the candidate"
|
|
[[ "$current" == "$candidate" ]] || fail "$context: 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 origin "$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 "before validation"
|
|
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 origin 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 "after validation"
|
|
check_unused_version
|
|
|
|
# Use the named remote whose resolved destinations were checked above: a literal
|
|
# URL can apply pushInsteadOf even when origin's explicit push URL does not.
|
|
# Disable mirror defaults as well as implicit tags; publish only our refspec.
|
|
if git -c remote.origin.mirror=false push --no-follow-tags origin "$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 candidate $candidate; authoritative main differs"
|
|
else
|
|
result=$?
|
|
((result == 2)) && fail "main push failed for $candidate; authoritative main is absent"
|
|
fail "main push outcome is unknown for $candidate; inspect authoritative main before retrying"
|
|
fi
|
|
fi
|
|
|
|
check_candidate "main at $candidate; tag not yet published"
|
|
git tag -a -F "$note_file" --cleanup=verbatim "$version" "$candidate" ||
|
|
fail "main is at $candidate; could not create local tag $version"
|
|
tag_target=$(git rev-parse --verify "refs/tags/$version^{}") ||
|
|
fail "main is at $candidate; cannot verify local tag $version"
|
|
[[ "$tag_target" == "$candidate" ]] || fail "main is at $candidate; local tag $version targets another commit"
|
|
tag_type=$(git cat-file -t "refs/tags/$version") ||
|
|
fail "main is at $candidate; cannot inspect local tag $version"
|
|
[[ "$tag_type" == tag ]] || fail "main is at $candidate; local tag $version is not annotated"
|
|
tag_oid=$(git rev-parse --verify "refs/tags/$version") ||
|
|
fail "main is at $candidate; cannot identify local tag $version"
|
|
|
|
if git -c remote.origin.mirror=false push --no-follow-tags origin "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 $version conflicts with the local tag at $candidate; never replace it"
|
|
else
|
|
result=$?
|
|
((result == 2)) &&
|
|
fail "tag push failed; main is at $candidate, but local tag $version is unpublished"
|
|
fail "tag $version push outcome is unknown for $candidate; retain the local tag and inspect authoritative state"
|
|
fi
|
|
fi
|
|
|
|
printf 'Published %s at %s\n' "$version" "$candidate"
|