Files
Munitor/tests/test_allocate_version.sh
james.maesandClaude Opus 5.5 57cb7a2aca fix: atomic release versions and immutable image tags
Two main merges built minutes apart both computed the same GitVersion
version. The later pipeline overwrote the image tag in GHCR, github-release
tagged whatever main pointed at (--target main), and the GitOps bump was a
no-op, so production ran an image without the later merge (Website-Backend
0.1.24 and 0.1.32).

- allocate_version: on main, reserve vX.Y.Z by creating the git tag through
  the GitHub refs API (create-if-absent). On conflict with another commit,
  take the next patch; reruns of the same commit reuse their tag. Existing
  tags are never moved. Runs in mvn/gradle/node build-and-test before the
  version is baked into artifacts.
- guard_image_tags: refuse to push a main or supplemental version tag that
  already exists for a different commit (org.opencontainers.image.revision
  label, now set on every build); fail closed on registry errors.
- update_cd_repo: verify vX.Y.Z belongs to this commit before bumping the
  GitOps repo; record the source commit in the GitOps commit message.
- github_release: target CIRCLE_SHA1 instead of main and refuse when the tag
  points elsewhere.
- Generator: render cd_path only when cd.path is set, so manifests pinned to
  orb versions predating the parameter (website develop branches, Concilium,
  makers4) stop failing with "Unexpected argument(s): cd_path".

Tests: tests/test_allocate_version.sh (incl. 8 concurrent allocators against
an atomic fake refs API) and tests/test_release_guards.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:56:58 -05:00

183 lines
6.9 KiB
Bash

#!/usr/bin/env bash
# Tests for allocate_version.sh: atomic release version reservation via git tags.
#
# The GitHub refs API is replaced by a fake backed by a directory. Creating a
# ref uses a hard link from a fully-written temp file, which is atomic and fails
# if the target exists -- the same create-if-absent semantics GitHub provides
# for POST /git/refs (422 "Reference already exists").
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SCRIPT="${PROJECT_DIR}/src/scripts/allocate_version.sh"
PASS=0
FAIL=0
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT
pass() { echo "PASS"; PASS=$((PASS + 1)); }
fail() { echo "FAIL${1:+ ($1)}"; FAIL=$((FAIL + 1)); }
# Fake GitHub API: gh_api METHOD PATH [BODY]; state lives in ${FAKE_GH_DIR}/tags/<tag>
FAKE_API="${WORK}/fake_gh_api.sh"
cat > "${FAKE_API}" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
method="$1" path="$2" body="${3:-}"
dir="${FAKE_GH_DIR:?}/tags"
mkdir -p "${dir}"
if [[ -n "${FAKE_GH_FAIL:-}" ]]; then
echo '{"message":"Server Error"}'
exit 1
fi
case "${method} ${path}" in
"POST git/refs")
tag=$(echo "${body}" | jq -r '.ref' | sed 's#^refs/tags/##')
sha=$(echo "${body}" | jq -r '.sha')
# Widen the race window so concurrent allocators genuinely interleave.
sleep "0.0$((RANDOM % 9))"
tmp=$(mktemp "${dir}/.tmp.XXXXXX")
echo "${sha}" > "${tmp}"
if ln "${tmp}" "${dir}/${tag}" 2>/dev/null; then
rm -f "${tmp}"
echo "{\"ref\":\"refs/tags/${tag}\",\"object\":{\"type\":\"commit\",\"sha\":\"${sha}\"}}"
exit 0
fi
rm -f "${tmp}"
echo '{"message":"Reference already exists"}'
exit 3
;;
"GET git/ref/tags/"*)
tag="${path#git/ref/tags/}"
if [[ -f "${dir}/${tag}" ]]; then
sha=$(cat "${dir}/${tag}")
if [[ -f "${dir}/${tag}.annotated" ]]; then
echo "{\"object\":{\"type\":\"tag\",\"sha\":\"tagobj-${sha}\"}}"
else
echo "{\"object\":{\"type\":\"commit\",\"sha\":\"${sha}\"}}"
fi
exit 0
fi
echo '{"message":"Not Found"}'
exit 2
;;
"GET git/tags/tagobj-"*)
echo "{\"object\":{\"type\":\"commit\",\"sha\":\"${path#git/tags/tagobj-}\"}}"
exit 0
;;
esac
echo "unexpected call: ${method} ${path}" >&2
exit 1
EOF
chmod +x "${FAKE_API}"
# run_alloc STATE_DIR SHA VERSION [BRANCH] -> prints allocated PROJECT_VERSION (from BASH_ENV) or "-"
run_alloc() {
local state="$1" sha="$2" version="$3" branch="${4:-main}"
local env_file
env_file=$(mktemp "${WORK}/env.XXXXXX")
FAKE_GH_DIR="${state}" MUNITOR_GH_API_CMD="${FAKE_API}" BASH_ENV="${env_file}" \
PROJECT_VERSION="${version}" CIRCLE_BRANCH="${branch}" CIRCLE_SHA1="${sha}" \
CIRCLE_PROJECT_USERNAME="KofTwentyTwo" CIRCLE_PROJECT_REPONAME="Example" \
bash "${SCRIPT}" > "${env_file}.log" 2>&1 || { echo "ERR"; return 0; }
local v
v=$(grep "^export PROJECT_VERSION=" "${env_file}" | tail -1 | sed "s/^export PROJECT_VERSION='\(.*\)'$/\1/")
echo "${v:--}"
}
echo "=== allocate_version.sh Tests ==="
echo -n " TEST: reserves the computed version when free... "
S="${WORK}/s1"
got=$(run_alloc "${S}" sha-a 0.1.24)
if [[ "${got}" == "0.1.24" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: rerun on the same commit reuses its version... "
got=$(run_alloc "${S}" sha-a 0.1.24)
if [[ "${got}" == "0.1.24" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: collision with another commit takes the next patch (the PR #65/#66 case)... "
got=$(run_alloc "${S}" sha-b 0.1.24)
if [[ "${got}" == "0.1.25" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" && "$(cat "${S}/tags/v0.1.25")" == "sha-b" ]]; then
pass
else
fail "got ${got}"
fi
echo -n " TEST: existing tags are never moved... "
if [[ "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "v0.1.24 moved"; fi
echo -n " TEST: annotated tag on the same commit is recognized as a rerun... "
S2="${WORK}/s2"; mkdir -p "${S2}/tags"; echo sha-x > "${S2}/tags/v1.0.0"; touch "${S2}/tags/v1.0.0.annotated"
got=$(run_alloc "${S2}" sha-x 1.0.0)
if [[ "${got}" == "1.0.0" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: skips several already-taken versions... "
S3="${WORK}/s3"; mkdir -p "${S3}/tags"
for p in 5 6 7; do echo "other-${p}" > "${S3}/tags/v2.3.${p}"; done
got=$(run_alloc "${S3}" sha-y 2.3.5)
if [[ "${got}" == "2.3.8" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: non-main branches are not reserved... "
S4="${WORK}/s4"
got=$(run_alloc "${S4}" sha-z 0.2.0-SNAPSHOT.abc develop)
if [[ "${got}" == "-" && ! -e "${S4}/tags/v0.2.0-SNAPSHOT.abc" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: pre-release versions on main are not reserved... "
got=$(run_alloc "${S4}" sha-z 0.2.0-RC.1 main)
if [[ "${got}" == "-" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: API failure fails the build instead of guessing... "
S5="${WORK}/s5"
got=$(FAKE_GH_FAIL=1 run_alloc "${S5}" sha-q 0.1.0)
if [[ "${got}" == "ERR" ]]; then pass; else fail "got ${got}"; fi
echo -n " TEST: missing GITHUB_TOKEN fails on main... "
rc=0
env -u GITHUB_TOKEN BASH_ENV=/dev/null PROJECT_VERSION=0.1.0 CIRCLE_BRANCH=main CIRCLE_SHA1=abc \
CIRCLE_PROJECT_USERNAME=o CIRCLE_PROJECT_REPONAME=r bash "${SCRIPT}" >/dev/null 2>&1 || rc=$?
if [[ ${rc} -ne 0 ]]; then pass; else fail "expected failure"; fi
# Concurrent simulation: N pipelines for N different merge commits all computed
# the same version from GitVersion and race to reserve it.
N=8
echo -n " TEST: ${N} concurrent pipelines get ${N} distinct, contiguous versions... "
SC="${WORK}/concurrent"; mkdir -p "${SC}/tags"
pids=()
for i in $(seq 1 "${N}"); do
( run_alloc "${SC}" "merge-${i}" 0.1.24 > "${WORK}/result.${i}" ) &
pids+=($!)
done
for pid in "${pids[@]}"; do wait "${pid}"; done
results=$(for i in $(seq 1 "${N}"); do cat "${WORK}/result.${i}"; done | sort -t. -k3,3n)
expected=$(for p in $(seq 24 $((24 + N - 1))); do echo "0.1.${p}"; done)
ok=true
[[ "${results}" == "${expected}" ]] || ok=false
# Every tag maps to exactly the commit that claims that version.
for i in $(seq 1 "${N}"); do
v=$(cat "${WORK}/result.${i}")
[[ -f "${SC}/tags/v${v}" && "$(cat "${SC}/tags/v${v}")" == "merge-${i}" ]] || ok=false
done
if ${ok}; then pass; else fail "results: $(echo "${results}" | tr '\n' ' ')"; fi
echo -n " TEST: concurrent reruns of the same commits keep their versions... "
pids=()
for i in $(seq 1 "${N}"); do
( run_alloc "${SC}" "merge-${i}" 0.1.24 > "${WORK}/rerun.${i}" ) &
pids+=($!)
done
for pid in "${pids[@]}"; do wait "${pid}"; done
ok=true
for i in $(seq 1 "${N}"); do
[[ "$(cat "${WORK}/rerun.${i}")" == "$(cat "${WORK}/result.${i}")" ]] || ok=false
done
tag_count=$(find "${SC}/tags" -name 'v*' | wc -l | tr -d ' ')
[[ "${tag_count}" == "${N}" ]] || ok=false
if ${ok}; then pass; else fail "reruns changed versions or created extra tags (${tag_count})"; fi
echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"
[[ ${FAIL} -eq 0 ]]