Files
felhom-controller/controller/scripts/test_build_push_once.sh
T
admin c6a567122e
gates / gates (push) Failing after 50s
v0.316.4: build.sh refuses a push from a dirty or unpushed tree
An admission and its mechanism. Building 0.316.3 published another session's work: the shared
DooPlex clone held 31 uncommitted files, and the build directory's build.sh is a symlink into that
clone's WORKING COPY — which predated the FELHOM_REPO_DIR override, so the override was ignored and
the build copied the shared tree. The banner printed 'Commit: 0225255' and no 'Repo:' line; the
evidence was on screen and the image was already pushed. Nobody deployed it, which is luck, not a
control.

The clean-tree rule has been a sentence in a runbook all along. A rule without a mechanism is a
wish, so it is now two questions the build asks and refuses on: is every tracked file committed,
and is HEAD what origin/main has. A directory that is not a git checkout is refused too — a build
whose source cannot be identified is not reproducible.

Four new cases in the test (ten in total), each driving the real script; red-proved by deleting the
gate, which fails all four.

0.316.3 must never be deployed. It is not re-pushed or deleted: a version number is pushed once,
and that applies to a number I spoiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-15 21:09:38 +02:00

152 lines
6.6 KiB
Bash

#!/bin/bash
# A VERSION NUMBER IS PUSHED ONCE — the test for build.sh's registry check (v0.316.0).
#
# WHY IT EXISTS: 0.315.0 was built and pushed three times from three different commits. A box that
# pulled between the first and the last runs different code under the same number and will never
# fetch the fix, because to the box that number is already current. The check in build.sh is the
# mechanism; this is the test that it convicts.
#
# HOW IT TESTS WITHOUT A REGISTRY: a fake `docker` is put first on PATH. Every case drives the REAL
# build.sh, so the test cannot pass while the check has been deleted — which is the red-proof.
# Nothing is ever pushed: the fake refuses `build` and `push` loudly, so a build that got past the
# check would fail the test rather than reach the network.
#
# Run from controller/: bash scripts/test_build_push_once.sh
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
BUILD_SH="$(dirname "$HERE")/build.sh"
fails=0; ran=0
[[ -f "$BUILD_SH" ]] || { echo "FAIL: build.sh not found at $BUILD_SH"; exit 1; }
# A fake `docker` whose answer to `manifest inspect` is set by FAKE_MODE.
make_fake_docker() {
local dir="$1"
mkdir -p "$dir"
cat > "$dir/docker" <<'FAKE'
#!/bin/bash
case "$1 $2" in
"manifest inspect")
case "${FAKE_MODE}" in
present) echo '{"schemaVersion":2}'; exit 0 ;;
absent) echo "manifest unknown: manifest unknown" >&2; exit 1 ;;
down) echo "Get \"https://gitea.dooplex.hu/v2/\": dial tcp: connection refused" >&2; exit 1 ;;
denied) echo "unauthorized: authentication required" >&2; exit 1 ;;
esac ;;
"build "*|"buildx "*|"push "*)
echo "FAKE-DOCKER-REACHED: $*" ; exit 0 ;;
esac
# anything else (version checks etc.) succeeds quietly
exit 0
FAKE
chmod +x "$dir/docker"
}
# Run build.sh with the fake docker, from a throwaway cwd, and capture everything.
run_build() {
local mode="$1"; shift
local tmp; tmp="$(mktemp -d)"
make_fake_docker "$tmp/bin"
FAKE_MODE="$mode" PATH="$tmp/bin:$PATH" bash "$BUILD_SH" "$@" >"$tmp/out" 2>&1
rc=$?
out="$(cat "$tmp/out")"
rm -rf "$tmp"
return $rc
}
check() {
local name="$1" want_rc="$2" want_text="$3" forbid_text="${4:-}"
ran=$((ran+1))
if [[ "$rc" != "$want_rc" ]]; then
fails=$((fails+1)); echo " FAIL $name: exit $rc, want $want_rc"
echo "$out" | tail -6 | sed 's/^/ /'; return
fi
if [[ -n "$want_text" ]] && ! grep -qi -- "$want_text" <<<"$out"; then
fails=$((fails+1)); echo " FAIL $name: output does not contain '$want_text'"
echo "$out" | tail -6 | sed 's/^/ /'; return
fi
if [[ -n "$forbid_text" ]] && grep -qi -- "$forbid_text" <<<"$out"; then
fails=$((fails+1)); echo " FAIL $name: output contains '$forbid_text', which must not happen"
echo "$out" | tail -6 | sed 's/^/ /'; return
fi
echo " ok $name"
}
echo "build.sh — a version number is pushed once, from a clean tree"
# 1. The tag is already in the registry -> refuse, before building, and push nothing.
run_build present 0.999.0 --push
check "--push, tag present -> refuses" 1 "already exists in the registry" "FAKE-DOCKER-REACHED"
# 2. The tag is new -> say so and carry on INTO the build.
# The exit code is deliberately not asserted: past the check, build.sh runs the real build steps,
# which cannot complete in a test sandbox. What matters is that the check let it through, so the
# assertion is its verdict line plus the ABSENCE of a refusal.
run_build absent 0.999.0 --push
check "--push, tag absent -> proceeds" "$rc" "is not present in the registry" "already exists in the registry"
# 3. --multiarch gets the same refusal (the task's edge case).
run_build present 0.999.0 --multiarch
check "--multiarch, tag present -> refuses" 1 "already exists in the registry" "FAKE-DOCKER-REACHED"
# 4. The registry cannot be asked -> refuse. "I could not ask" is not proof the tag is new.
run_build down 0.999.0 --push
check "--push, registry unreachable -> refuses" 1 "Could not establish whether" "FAKE-DOCKER-REACHED"
# 5. An auth failure is just as unanswered as a network failure.
run_build denied 0.999.0 --push
check "--push, auth failure -> refuses" 1 "Could not establish whether" "FAKE-DOCKER-REACHED"
# 6. A LOCAL build is untouched — reusing a version while iterating is the normal case, and the
# check must not even run (no registry question is asked, so `present` must not refuse).
run_build present 0.999.0
check "local build, tag present -> not refused" 0 "" "already exists in the registry"
# ── THE SOURCE TREE MUST BE CLEAN AND PUSHED (v0.316.4) ─────────────────────────────────────────
# Earned the same evening: a build from the shared clone, which held 31 files of another session's
# work in progress, published their unreleased code under this repo's version number.
scratch_repo() {
local d; d="$(mktemp -d)"
git -C "$d" init -q 2>/dev/null
git -C "$d" config user.email t@t; git -C "$d" config user.name t
mkdir -p "$d/controller"; echo x > "$d/controller/f"
git -C "$d" add -A >/dev/null; git -C "$d" commit -qm one >/dev/null
git -C "$d" update-ref refs/remotes/origin/main HEAD
echo "$d"
}
run_build_in() { # $1 = mode, $2 = repo dir, rest = args
local mode="$1" repo="$2"; shift 2
local tmp; tmp="$(mktemp -d)"
make_fake_docker "$tmp/bin"
FAKE_MODE="$mode" FELHOM_REPO_DIR="$repo" PATH="$tmp/bin:$PATH" bash "$BUILD_SH" "$@" >"$tmp/out" 2>&1
rc=$?; out="$(cat "$tmp/out")"; rm -rf "$tmp"; return $rc
}
CLEAN_REPO="$(scratch_repo)"
run_build_in absent "$CLEAN_REPO" 0.999.0 --push
check "clean + at origin/main -> proceeds" "$rc" "Source tree is clean and at origin/main" "refusing to push"
echo "dirt" > "$CLEAN_REPO/controller/uncommitted"
run_build_in absent "$CLEAN_REPO" 0.999.0 --push
check "dirty tree -> refuses" 1 "uncommitted change" "FAKE-DOCKER-REACHED"
rm -f "$CLEAN_REPO/controller/uncommitted"
echo y > "$CLEAN_REPO/controller/f2"
git -C "$CLEAN_REPO" add -A >/dev/null; git -C "$CLEAN_REPO" commit -qm two >/dev/null
run_build_in absent "$CLEAN_REPO" 0.999.0 --push
check "ahead of origin/main -> refuses" 1 "not at origin/main" "FAKE-DOCKER-REACHED"
rm -rf "$CLEAN_REPO"
NOT_A_REPO="$(mktemp -d)"; mkdir -p "$NOT_A_REPO/controller"
run_build_in absent "$NOT_A_REPO" 0.999.0 --push
check "not a git checkout -> refuses" 1 "not a git checkout" "FAKE-DOCKER-REACHED"
rm -rf "$NOT_A_REPO"
echo
if [[ $fails -eq 0 ]]; then
echo "all $ran cases behaved — a pushed version cannot be overwritten"
exit 0
fi
echo "$fails of $ran cases FAILED"
exit 1