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>
135 lines
5.7 KiB
Bash
135 lines
5.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Tests for the release guards: immutable image tags and conditional cd_path emission.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
SRC_SCRIPTS="${PROJECT_DIR}/src/scripts"
|
|
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 docker: `docker buildx imagetools inspect REF --format ...` reads
|
|
# ${FAKE_REGISTRY}/<ref with / and : replaced by _> holding the revision label.
|
|
mkdir -p "${WORK}/bin"
|
|
cat > "${WORK}/bin/docker" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
if [[ "$1" == "--version" ]]; then echo "Docker version 99"; exit 0; fi
|
|
if [[ "$1 $2 $3" == "buildx imagetools inspect" ]]; then
|
|
ref="$4"
|
|
if [[ -n "${FAKE_REGISTRY_ERROR:-}" ]]; then echo "unauthorized: authentication required" >&2; exit 1; fi
|
|
key=$(echo "${ref}" | tr '/:' '__')
|
|
f="${FAKE_REGISTRY}/${key}"
|
|
if [[ ! -f "${f}" ]]; then echo "ERROR: ${ref}: not found" >&2; exit 1; fi
|
|
rev=$(cat "${f}")
|
|
if [[ -z "${rev}" ]]; then echo '{"config":{"Labels":{}}}'; exit 0; fi
|
|
echo "{\"architecture\":\"amd64\",\"config\":{\"Labels\":{\"org.opencontainers.image.revision\":\"${rev}\"}}}"
|
|
exit 0
|
|
fi
|
|
echo "unexpected docker call: $*" >&2
|
|
exit 1
|
|
EOF
|
|
chmod +x "${WORK}/bin/docker"
|
|
|
|
put_image() { mkdir -p "${FAKE_REGISTRY}"; echo "$2" > "${FAKE_REGISTRY}/$(echo "$1" | tr '/:' '__')"; }
|
|
|
|
run_guard() {
|
|
PATH="${WORK}/bin:${PATH}" MUNITOR_SKIP_REGISTRY_LOGIN=1 FAKE_REGISTRY="${FAKE_REGISTRY}" \
|
|
IMAGE_NAME=website-backend REGISTRY=ghcr.io/koftwentytwo PROJECT_VERSION="$1" CIRCLE_SHA1="$2" \
|
|
SUPPLEMENTAL_IMAGES_JSON="${3:-[]}" bash "${SRC_SCRIPTS}/guard_image_tags.sh" >"${WORK}/guard.log" 2>&1
|
|
}
|
|
|
|
echo "=== guard_image_tags.sh Tests ==="
|
|
SUPP='[{"name":"migrations","dockerfile":"docker/Dockerfile.migrations"}]'
|
|
|
|
FAKE_REGISTRY="${WORK}/reg1"
|
|
echo -n " TEST: passes when the version tag does not exist... "
|
|
if run_guard 0.1.32 sha-new "${SUPP}"; then pass; else fail "$(cat "${WORK}/guard.log")"; fi
|
|
|
|
echo -n " TEST: passes on rerun when the tag was built from the same commit... "
|
|
put_image ghcr.io/koftwentytwo/website-backend:0.1.32 sha-new
|
|
put_image ghcr.io/koftwentytwo/website-backend-migrations:0.1.32 sha-new
|
|
if run_guard 0.1.32 sha-new "${SUPP}"; then pass; else fail "$(cat "${WORK}/guard.log")"; fi
|
|
|
|
echo -n " TEST: refuses to overwrite a version tag built from another commit (0.1.24 incident)... "
|
|
if run_guard 0.1.32 sha-other "${SUPP}"; then fail "guard allowed overwrite"; else pass; fi
|
|
|
|
echo -n " TEST: refuses when only a supplemental image collides... "
|
|
FAKE_REGISTRY="${WORK}/reg2"
|
|
put_image ghcr.io/koftwentytwo/website-backend-migrations:0.2.0 sha-old
|
|
if run_guard 0.2.0 sha-new "${SUPP}"; then fail "guard allowed supplemental overwrite"; else pass; fi
|
|
|
|
echo -n " TEST: refuses when an existing tag has no provenance label... "
|
|
FAKE_REGISTRY="${WORK}/reg3"
|
|
put_image ghcr.io/koftwentytwo/website-backend:0.3.0 ""
|
|
if run_guard 0.3.0 sha-new; then fail "unlabeled image overwritten"; else pass; fi
|
|
|
|
echo -n " TEST: registry errors fail closed... "
|
|
if FAKE_REGISTRY_ERROR=1 run_guard 0.4.0 sha-new; then fail "error treated as free"; else pass; fi
|
|
|
|
echo -n " TEST: docker_build labels images with the commit revision... "
|
|
if grep -q 'org.opencontainers.image.revision=${CIRCLE_SHA1}' "${SRC_SCRIPTS}/docker_build.sh" &&
|
|
grep -q 'org.opencontainers.image.revision=${CIRCLE_SHA1}' "${SRC_SCRIPTS}/build_push_supplemental.sh"; then
|
|
pass
|
|
else
|
|
fail "revision label missing"
|
|
fi
|
|
|
|
echo -n " TEST: docker_build_push guards tags before building and pushing... "
|
|
JOB="${PROJECT_DIR}/src/jobs/docker_build_push.yml"
|
|
g=$(grep -n 'guard_image_tags' "${JOB}" | head -1 | cut -d: -f1)
|
|
p=$(grep -n 'docker_push_ghcr' "${JOB}" | head -1 | cut -d: -f1)
|
|
if [[ -n "${g}" && -n "${p}" && ${g} -lt ${p} ]]; then pass; else fail "guard not before push"; fi
|
|
|
|
echo -n " TEST: build jobs reserve the version before persisting it... "
|
|
ok=true
|
|
for j in mvn gradle node; do
|
|
f="${PROJECT_DIR}/src/jobs/${j}_build_and_test.yml"
|
|
a=$(grep -n 'allocate_version' "${f}" | head -1 | cut -d: -f1)
|
|
e=$(grep -n 'export_version_vars' "${f}" | head -1 | cut -d: -f1)
|
|
m=$(grep -n 'persist_version_metadata' "${f}" | head -1 | cut -d: -f1)
|
|
[[ -n "${a}" && ${a} -gt ${e} && ${a} -lt ${m} ]] || ok=false
|
|
done
|
|
if ${ok}; then pass; else fail "allocate_version misplaced"; fi
|
|
|
|
echo -n " TEST: github_release targets the built commit, not the branch head... "
|
|
if grep -q 'TARGET="${CIRCLE_SHA1:-main}"' "${SRC_SCRIPTS}/github_release.sh" &&
|
|
grep -q 'Refusing to release' "${SRC_SCRIPTS}/github_release.sh"; then
|
|
pass
|
|
else
|
|
fail
|
|
fi
|
|
|
|
echo -n " TEST: update_cd_repo verifies the release tag belongs to this commit... "
|
|
if grep -q 'Refusing to bump the GitOps repo' "${SRC_SCRIPTS}/update_cd_repo.sh"; then pass; else fail; fi
|
|
|
|
echo ""
|
|
echo "=== cd_path emission (orb version compatibility) ==="
|
|
|
|
render() {
|
|
local fixture="$1" out="$2"
|
|
MUNITOR_CONFIG="${fixture}" MUNITOR_OUTPUT_FILE="${out}" \
|
|
bash "${SRC_SCRIPTS}/generate_config.sh" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
echo -n " TEST: manifest without cd.path emits no cd_path (older orbs reject it)... "
|
|
render "${SCRIPT_DIR}/fixtures/java-webapp.munitor.yml" "${WORK}/java.yml"
|
|
if [[ -s "${WORK}/java.yml" ]] && grep -q 'munitor/update_cd_repo' "${WORK}/java.yml" && ! grep -q 'cd_path' "${WORK}/java.yml"; then
|
|
pass
|
|
else
|
|
fail "cd_path present or render failed"
|
|
fi
|
|
|
|
echo -n " TEST: manifest with cd.path still emits cd_path... "
|
|
render "${SCRIPT_DIR}/fixtures/node-webapp-consolidated.munitor.yml" "${WORK}/node.yml"
|
|
if grep -q 'cd_path: tenants/investinginchester/website/kustomization.yaml' "${WORK}/node.yml"; then pass; else fail; fi
|
|
|
|
echo ""
|
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
|
[[ ${FAIL} -eq 0 ]]
|