build-hub.sh v1.24.0: refuse a stale or dirty tree (R-1021)
gates / gates (push) Successful in 6m44s

Fetch, pull --ff-only, then refuse unless clean and HEAD == origin/main; the
commit label is read after the pull. Test with a stub docker: 6 cases; the
old script gets 5 wrong. Evidence for the catalog gate unclean-boot-images.
This commit is contained in:
2026-10-14 19:11:17 +02:00
parent 8e7dce3211
commit 621783b3b0
7 changed files with 409 additions and 12 deletions
+12
View File
@@ -1,3 +1,15 @@
## 2026-10-14 — build-hub.sh v1.24.0: refuses a stale or dirty tree (R-1021)
- Fetches, then REFUSES (exit 1, naming the leftovers' home and the own-clone route) unless the tree is clean, the
`git pull --ff-only` succeeds, and `HEAD == origin/main`. `GIT_COMMIT` is read AFTER the pull. Until now a failed
pull only warned and the commit was read before it — on 2026-10-14 the shared checkout was 44 commits behind and the
script would have built old hub code under the new version label.
- `FELHOM_HUB_REPO_DIR` builds from another clone (same rule). No `--allow-dirty`: no current runbook needs one.
- `test_build_hub_tree_gate.sh` (new): a scratch origin + clone, a STUB `docker` first on PATH (nothing is built or
pushed). Six cases — untracked file, modified file, diverged (pull blocked), unpushed commit → refused; behind with a
clean pull → passes and the Commit line names the NEW head; clean → passes. Red-proof: the old script (only
`REPO_DIR` made overridable) gets 5 of 6 wrong (`documentation/audits/immich-repair-2026-10-14/E/bh-old.txt`).
## 2026-10-14 — host install: the uninstall removes the new root helper felhom-vzdump-partial-clean (R-1006)
- `felhom-host-install.sh` step 4d removes `/usr/local/sbin/felhom-vzdump-partial-clean` (agent v0.156.1's bundle installs
+33 -12
View File
@@ -19,10 +19,12 @@
# =============================================================================
set -euo pipefail
SCRIPT_VERSION="1.23.0"
SCRIPT_VERSION="1.24.0"
# --- Configuration (edit these if your paths differ) ---
REPO_DIR="/mnt/5_hdd/felhom.eu/git/felhom.eu"
# FELHOM_HUB_REPO_DIR builds from another clone (a session's own clean clone; the script's own test) — the same
# clean-tree rule applies to it.
REPO_DIR="${FELHOM_HUB_REPO_DIR:-/mnt/5_hdd/felhom.eu/git/felhom.eu}"
HUB_SRC="${REPO_DIR}/hub"
REGISTRY="gitea.dooplex.hu/admin"
IMAGE="${REGISTRY}/felhom-hub"
@@ -54,8 +56,35 @@ if ! command -v docker &>/dev/null; then
exit 1
fi
# Get git commit for build metadata
GIT_COMMIT=$(cd "${REPO_DIR}" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
# =========================================================================
# Step 1: the clean-tree gate (R-1021) — fetch, pull, and REFUSE a stale or dirty tree
# =========================================================================
# Until v1.24.0 a failed pull only warned and the commit was read BEFORE it: on 2026-10-14 the shared checkout was
# 44 commits behind, its pull aborted on untracked leftovers, and this script would have shipped old hub code under a
# new version label. An unpushed or stale change does not exist (workspace CLAUDE.md, „Clean-tree gate"). There is
# no override: no current runbook builds the hub from a dirty tree. Test: scripts/test_build_hub_tree_gate.sh.
step "1/3 — Pulling latest code (the tree must be clean and at origin/main)..."
cd "${REPO_DIR}"
refuse() {
error "$1"
error "Refusing to build from ${REPO_DIR}: the hub image must be built from a clean tree at origin/main."
error "Leftover files in the shared checkout move to /mnt/5_hdd/felhom.eu/leftovers/<date>/ (never deleted; R-1021),"
error "or build from your own clean clone: FELHOM_HUB_REPO_DIR=<clone> ./build.sh <VER> --push"
exit 1
}
DIRTY=$(git status --porcelain 2>&1) || refuse "git status failed: ${DIRTY}"
[[ -z "${DIRTY}" ]] || refuse "the tree is not clean:
${DIRTY}"
git fetch -q origin 2>/dev/null || refuse "git fetch failed (offline?) — origin/main cannot be checked"
git pull -q --ff-only 2>/dev/null || refuse "git pull --ff-only failed (local commits, or files in the way)"
DIRTY=$(git status --porcelain 2>&1) || refuse "git status failed: ${DIRTY}"
[[ -z "${DIRTY}" ]] || refuse "the tree is not clean after the pull:
${DIRTY}"
HEAD_SHA=$(git rev-parse HEAD) ; MAIN_SHA=$(git rev-parse origin/main)
[[ "${HEAD_SHA}" == "${MAIN_SHA}" ]] || refuse "HEAD ${HEAD_SHA:0:10} is not origin/main ${MAIN_SHA:0:10} (unpushed commits?)"
# Read AFTER the pull: the label names the commit that is built.
GIT_COMMIT=$(git rev-parse --short HEAD)
echo ""
info "╔══════════════════════════════════════╗"
@@ -68,14 +97,6 @@ info "Build: ${BUILD_DIR}"
info "Image: ${IMAGE}:${VERSION}"
echo ""
# =========================================================================
# Step 1: Pull latest code
# =========================================================================
step "1/3 — Pulling latest code..."
cd "${REPO_DIR}"
git pull --ff-only 2>/dev/null || warn "git pull failed (offline or dirty tree?)"
# =========================================================================
# Step 2: Sync source to build workspace
# =========================================================================
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# test_build_hub_tree_gate.sh — build-hub.sh refuses a stale or dirty tree (R-1021).
#
# Builds a tiny scratch repo (a bare "origin" + a clone holding the hub's directory shape), copies build-hub.sh next
# to it (so its workspace/ is scratch too) and runs it with FELHOM_HUB_REPO_DIR pointing at the clone and a STUB
# `docker` first on PATH: the real Docker is never called, nothing is built or pushed. Each case asserts the exit code
# AND a line of output:
# dirty (an untracked file) -> refused, "not clean"
# dirty (a modified tracked file) -> refused, "not clean"
# behind origin, the pull blocked -> refused (local commit diverged: "pull --ff-only failed")
# ahead of origin (unpushed commit) -> refused, "is not origin/main"
# behind origin, the pull clean -> passes, and the Commit line names the NEW head (read after the pull)
# clean, at origin/main -> passes, the stub docker reached
# Run: bash scripts/test_build_hub_tree_gate.sh (exit 0 all cases right · 1 a case wrong)
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
WS="$(mktemp -d)"
trap 'rm -rf "${WS:?}"' EXIT
fails=0
mkdir -p "$WS/bin" "$WS/build"
printf '#!/bin/sh\necho "STUB docker $*"\n' > "$WS/bin/docker"; chmod +x "$WS/bin/docker"
cp "$HERE/build-hub.sh" "$WS/build/build.sh"
fresh() {
rm -rf "${WS:?}/origin.git" "${WS:?}/clone" "${WS:?}/other" "${WS:?}/build/workspace"
git init -q --bare -b main "$WS/origin.git"
git clone -q "$WS/origin.git" "$WS/clone" 2>/dev/null
(cd "$WS/clone" && mkdir -p hub/cmd/hub hub/internal/api hub/internal/store hub/internal/web \
&& printf 'module x\n\ngo 1.22\n' > hub/go.mod && echo a > hub/cmd/hub/a.txt \
&& git add -A && git -c user.name=t -c user.email=t@t commit -q -m init && git push -q origin main)
git clone -q "$WS/origin.git" "$WS/other" 2>/dev/null
}
upstream_commit() { # a new commit on origin, made from another clone
(cd "$WS/other" && git pull -q && echo "$1" > "hub/cmd/hub/$1.txt" && git add -A \
&& git -c user.name=t -c user.email=t@t commit -q -m "$1" && git push -q origin main)
}
run() { # name, expected rc, required output fragment
local out rc
out=$(cd "$WS/build" && PATH="$WS/bin:$PATH" FELHOM_HUB_REPO_DIR="$WS/clone" bash ./build.sh 9.9.9 2>&1); rc=$?
if [[ $rc -eq $2 && "$out" == *"$3"* ]]; then
echo " ok $1 (rc=$rc)"
else
echo " XX $1 (rc=$rc, expected $2; wanted: $3)"; echo "$out" | tail -8 | sed 's/^/ /'; fails=$((fails+1))
fi
}
fresh; touch "$WS/clone/leftover.txt"
run "dirty: an untracked file" 1 "the tree is not clean"
fresh; echo b >> "$WS/clone/hub/cmd/hub/a.txt"
run "dirty: a modified tracked file" 1 "the tree is not clean"
fresh; upstream_commit up1; (cd "$WS/clone" && echo l > hub/cmd/hub/local.txt && git add -A && git -c user.name=t -c user.email=t@t commit -q -m local)
run "behind origin and the pull blocked (diverged)" 1 "pull --ff-only failed"
fresh; (cd "$WS/clone" && echo l > hub/cmd/hub/local.txt && git add -A && git -c user.name=t -c user.email=t@t commit -q -m local)
run "ahead of origin (an unpushed commit)" 1 "is not origin/main"
fresh; upstream_commit up2; NEW=$(git -C "$WS/origin.git" rev-parse --short main)
run "behind origin, clean pull: Commit is the NEW head" 0 "Commit: ${NEW}"
fresh
run "clean, at origin/main: builds (stub docker)" 0 "STUB docker build"
if [[ $fails -gt 0 ]]; then echo "build-hub tree gate: $fails case(s) WRONG"; exit 1; fi
echo "build-hub tree gate OK — 6 case(s)"