forked from heavy-duty/rig
Merge pull request #128 from claude-bot-andresmgsl/build/106-install-lifecycle
feat: CI drills the install lifecycle — four beats against a real tree
This commit is contained in:
commit
9cc7568ca5
5 changed files with 221 additions and 3 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -64,6 +64,21 @@ jobs:
|
|||
# half-done pin bump goes red here.
|
||||
- uses: heavy-duty/ceremony/actions/docs-sync@0.1.0
|
||||
|
||||
# The install LIFECYCLE against a tree install.sh itself produced — the four
|
||||
# beats box and cast already run in CI (#106): install from this checkout,
|
||||
# assert what landed, a converging re-run proven by an EMPTY DIFF (never an
|
||||
# exit code), uninstall --all ending in the absence assert (`! -e` AND
|
||||
# `! -L` — only the second sees a dangling symlink). Separate from `check`
|
||||
# for the same reason db-integration is: fast feedback first. The runner's
|
||||
# real $HOME is the point — no throwaway roots here; the suite refuses to
|
||||
# run where a rig is already installed, so it cannot eat a real install.
|
||||
install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: the install lifecycle — four beats against a real tree
|
||||
run: bash test/install-lifecycle.sh
|
||||
|
||||
# Kept SEPARATE from `check` on purpose: this job pulls a Postgres image and
|
||||
# stands up throwaway containers, and a slow image pull must never delay the
|
||||
# fast shellcheck + cli.sh feedback above. ubuntu-latest ships Docker running
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ on the way to cutting its first release, and this file starts there.
|
|||
- GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123)
|
||||
- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95)
|
||||
- Platform, drill, docs and labels changes receive dedicated scope labels (#119)
|
||||
- CI drills the install lifecycle against a real tree — install from the checkout, converge to an empty diff, uninstall to proven absence (#106)
|
||||
- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109)
|
||||
- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13)
|
||||
- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ set -euo pipefail
|
|||
# protect, and the operator flipping versions on purpose is the common case.
|
||||
# A pre-versioning flat tree is migrated in place, so upgrading is seamless.
|
||||
#
|
||||
# RIG_INSTALL_SOURCE=<dir-or-tarball> installs from a local tree instead of
|
||||
# downloading — for CI and the test suite, so what lands is the code under
|
||||
# review.
|
||||
# RIG_INSTALL_SOURCE=<dir-or-tarball> is the LOCAL channel, a supported input
|
||||
# like RIG_REF (#106): installs from that tree instead of downloading — CI's
|
||||
# install-lifecycle job and the test suites use it, so what lands is the code
|
||||
# under review. A path that is neither refuses by name, never falls back to
|
||||
# a download.
|
||||
|
||||
REPO="${RIG_REPO:-heavy-duty/rig}"
|
||||
REF="${RIG_REF:-}" # empty = the latest release, resolved below
|
||||
|
|
|
|||
171
test/install-lifecycle.sh
Executable file
171
test/install-lifecycle.sh
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env bash
|
||||
# The install LIFECYCLE, driven end to end against a tree install.sh itself
|
||||
# produced (#106) — the four beats box and cast already run in CI, which rig,
|
||||
# the repo whose headline claim is convergence, ran nowhere:
|
||||
#
|
||||
# 1. install from THIS checkout (RIG_INSTALL_SOURCE — the local channel)
|
||||
# 2. assert what landed (layout, current, the PATH chain)
|
||||
# 3. a converging re-run (an EMPTY DIFF, never an exit code)
|
||||
# 4. uninstall --all (ending in the absence assert)
|
||||
#
|
||||
# test/cli.sh drives the same verbs against throwaway roots; this suite runs
|
||||
# them in the environment cli.sh deliberately fakes — the real default paths
|
||||
# under the runner's own $HOME. Run: bash test/install-lifecycle.sh (CI's
|
||||
# `install:` job). RIG_HOME/RIG_BIN redirect the roots for a local run; the
|
||||
# refusal below explains when you need them.
|
||||
#
|
||||
# Deliberately no `set -e` — a failing beat is data, and the summary is the
|
||||
# verdict (the test/release.sh harness shape).
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT" || exit 1
|
||||
PASS=0 FAIL=0
|
||||
|
||||
# check <desc> <want_exit> <want_substr> <cmd...>
|
||||
# Runs cmd, asserts exit code and (if non-empty) that combined output
|
||||
# contains want_substr.
|
||||
check() {
|
||||
local desc="$1" want="$2" substr="$3"; shift 3
|
||||
local out rc
|
||||
out="$("$@" 2>&1)"; rc=$?
|
||||
if [ "$rc" -ne "$want" ]; then
|
||||
echo "FAIL: $desc — exit $rc, wanted $want"
|
||||
printf '%s\n' "$out" | sed 's/^/ /'
|
||||
FAIL=$((FAIL + 1)); return
|
||||
fi
|
||||
if [ -n "$substr" ] && ! printf '%s' "$out" | grep -qF -e "$substr"; then
|
||||
echo "FAIL: $desc — output missing '$substr'"
|
||||
printf '%s\n' "$out" | sed 's/^/ /'
|
||||
FAIL=$((FAIL + 1)); return
|
||||
fi
|
||||
echo "ok: $desc"; PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
# The roots install.sh will use, computed by ITS rules (install.sh:55-60), so
|
||||
# every assert below points at what the installer actually touched.
|
||||
DEST="${RIG_HOME:-$HOME/.local/share/rig}"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
BINDIR="${RIG_BIN:-/usr/local/bin}"
|
||||
else
|
||||
BINDIR="${RIG_BIN:-$HOME/.local/bin}"
|
||||
fi
|
||||
|
||||
# Beat 4 REMOVES the install at those roots, so a rig that already lives there
|
||||
# is a refusal, not a fixture — this suite must never eat an operator's
|
||||
# install. CI runners are clean; a workstation run points the roots at
|
||||
# something disposable.
|
||||
if [ -e "$DEST" ] || [ -L "$DEST" ] || [ -e "$BINDIR/rig" ] || [ -L "$BINDIR/rig" ]; then
|
||||
echo "install-lifecycle: a rig install already exists ($DEST or $BINDIR/rig)" >&2
|
||||
echo "install-lifecycle: refusing to drive the lifecycle over it — re-run against scratch roots:" >&2
|
||||
echo " W=\$(mktemp -d); RIG_HOME=\$W/rig RIG_BIN=\$W/bin bash test/install-lifecycle.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
VER="$(cat "$ROOT/VERSION")"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# tree_state <root> — what "changed nothing" must mean: every file's bytes,
|
||||
# every path's type and mode, every symlink's target. Beat 3 captures this
|
||||
# before and after the re-run and diffs the two.
|
||||
tree_state() {
|
||||
(cd "$1" || return 1
|
||||
find . -type f -exec sha256sum {} + | LC_ALL=C sort
|
||||
find . -type l -printf '%p -> %l\n' | LC_ALL=C sort
|
||||
find . -printf '%y %m %p\n' | LC_ALL=C sort)
|
||||
}
|
||||
|
||||
diff_state() { # diff_state <capture> <root> — how the tree drifted, by name
|
||||
tree_state "$2" | diff "$1" -
|
||||
}
|
||||
|
||||
no_residue() { # no_residue <path>... — 0 iff every path is GONE: file, dir OR
|
||||
local p bad=0 # symlink. `! -e` alone follows the link and cannot see it
|
||||
for p in "$@"; do # dangling — the residue a broken uninstall actually leaves.
|
||||
if [ -e "$p" ] || [ -L "$p" ]; then echo "still present: $p"; bad=1; fi
|
||||
done
|
||||
return "$bad"
|
||||
}
|
||||
|
||||
# --- instrument honesty ------------------------------------------------------
|
||||
# The diff and the absence assert must be able to FAIL, or beats 3 and 4 prove
|
||||
# nothing — so break each one against a scratch tree first, on every run
|
||||
# (the test/drill.sh doctrine: mechanical, not a one-off claim in a PR).
|
||||
SCR="$WORK/scr"; mkdir -p "$SCR/tree/bin"
|
||||
echo content > "$SCR/tree/bin/rig"
|
||||
ln -s bin/rig "$SCR/tree/link"
|
||||
tree_state "$SCR/tree" > "$SCR/cap"
|
||||
check "honesty: an untouched tree reads as zero drift" 0 "" \
|
||||
diff_state "$SCR/cap" "$SCR/tree"
|
||||
echo drift >> "$SCR/tree/bin/rig"
|
||||
check "honesty: a mutated file is drift, named" 1 "bin/rig" \
|
||||
diff_state "$SCR/cap" "$SCR/tree"
|
||||
tree_state "$SCR/tree" > "$SCR/cap"
|
||||
ln -sfn ../elsewhere "$SCR/tree/link"
|
||||
check "honesty: a retargeted symlink is drift" 1 "elsewhere" \
|
||||
diff_state "$SCR/cap" "$SCR/tree"
|
||||
tree_state "$SCR/tree" > "$SCR/cap"
|
||||
touch "$SCR/tree/leftover"
|
||||
check "honesty: an ADDED file is drift (what a non-convergent installer leaves)" 1 "leftover" \
|
||||
diff_state "$SCR/cap" "$SCR/tree"
|
||||
# The beat-4 distinction, demonstrated: `test ! -e` PASSES on a dangling
|
||||
# symlink (it follows the link), so on its own it would certify a broken
|
||||
# uninstall clean — only `! -L` sees the corpse.
|
||||
ln -s "$SCR/nowhere" "$SCR/dangling-rig"
|
||||
check "honesty: test ! -e cannot see a dangling symlink (the lie)" 0 "" \
|
||||
test ! -e "$SCR/dangling-rig"
|
||||
check "honesty: the absence assert can (! -L is the catch)" 1 "still present" \
|
||||
no_residue "$SCR/dangling-rig"
|
||||
rm "$SCR/dangling-rig"
|
||||
check "honesty: a really-gone path passes the absence assert" 0 "" \
|
||||
no_residue "$SCR/dangling-rig"
|
||||
|
||||
# --- beat 1: install from THIS checkout --------------------------------------
|
||||
# RIG_INSTALL_SOURCE is the supported local channel (its contract — dir,
|
||||
# tarball, loud refusal, no silent download fallback — is test/release.sh's);
|
||||
# in CI $ROOT is $GITHUB_WORKSPACE, so what lands is the code under review.
|
||||
b1() { RIG_INSTALL_SOURCE="$ROOT" bash "$ROOT/install.sh"; }
|
||||
check "beat 1: install.sh installs this checkout" 0 "done" b1
|
||||
|
||||
# --- beat 2: assert what landed ----------------------------------------------
|
||||
check "beat 2: the tree landed in versions/$VER" 0 "" \
|
||||
test -x "$DEST/versions/$VER/bin/rig"
|
||||
check "beat 2: current points at versions/$VER" 0 "versions/$VER" \
|
||||
readlink "$DEST/current"
|
||||
check "beat 2: the PATH symlink rides the chain" 0 "$DEST/current/bin/rig" \
|
||||
readlink "$BINDIR/rig"
|
||||
check "beat 2: ...and resolves into versions/ (cast's assert)" 0 "/versions/$VER/bin/rig" \
|
||||
readlink -f "$BINDIR/rig"
|
||||
check "beat 2: rig --version answers through the whole chain" 0 "rig $VER" \
|
||||
"$BINDIR/rig" --version
|
||||
check "beat 2: INSTALLED_FROM names the local source" 0 "local:$ROOT" \
|
||||
cat "$DEST/versions/$VER/INSTALLED_FROM"
|
||||
|
||||
# --- beat 3: the converging re-run -------------------------------------------
|
||||
# "Ran twice without crashing" is the self-deception this beat exists to
|
||||
# refuse (#106): the assert is an empty diff of captured state, plus current
|
||||
# still pointing where it did.
|
||||
tree_state "$DEST" > "$WORK/before"
|
||||
CUR_BEFORE="$(readlink "$DEST/current")"
|
||||
check "beat 3: the re-run is a no-op that says so" 0 "already installed" b1
|
||||
check "beat 3: ...and changed NOTHING — the diff is the verdict" 0 "" \
|
||||
diff_state "$WORK/before" "$DEST"
|
||||
check "beat 3: current did not move" 0 "" \
|
||||
test "$(readlink "$DEST/current")" = "$CUR_BEFORE"
|
||||
|
||||
# --- beat 4: uninstall --all, ending in the absence assert -------------------
|
||||
check "beat 4: uninstall --all removes the whole install" 0 "uninstalled" \
|
||||
"$BINDIR/rig" uninstall --all --force
|
||||
check "beat 4: zero residue at the install root" 0 "" no_residue "$DEST"
|
||||
check "beat 4: zero residue on PATH — not even a dangling symlink" 0 "" \
|
||||
no_residue "$BINDIR/rig"
|
||||
# The doctrine spelled out as its two distinct asserts (#106): -e for
|
||||
# presence, -L for the dangling link -e cannot see.
|
||||
check "beat 4: test ! -e on the PATH entry" 0 "" test ! -e "$BINDIR/rig"
|
||||
check "beat 4: test ! -L on the PATH entry" 0 "" test ! -L "$BINDIR/rig"
|
||||
check "beat 4: test ! -e on the install root" 0 "" test ! -e "$DEST"
|
||||
check "beat 4: test ! -L on the install root" 0 "" test ! -L "$DEST"
|
||||
|
||||
echo "---"
|
||||
echo "$PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
|
@ -170,6 +170,35 @@ H5="$WORK/h5"; B5="$WORK/b5"
|
|||
check "channel: a ref that is neither tag nor branch dies naming both tries" \
|
||||
1 "not a tag and not a branch" rinst "$H5" "$B5" RIG_REF=no-such-ref
|
||||
|
||||
# --- the local channel: RIG_INSTALL_SOURCE (#106) ----------------------------
|
||||
# A supported input, not test scaffolding — CI's `install:` job and test/cli.sh
|
||||
# both install THIS checkout through it. What release.sh owes is the channel's
|
||||
# contract: a directory installs, a tarball installs, neither touches the
|
||||
# network, and a bad path refuses BY NAME — never a silent fallback to
|
||||
# downloading a release, which would leave a green CI job testing the wrong
|
||||
# tree. The stub curl's log is the network witness: any download, even an
|
||||
# attempted one, would land a URL in it.
|
||||
H6="$WORK/h6"; B6="$WORK/b6"; LOG6="$WORK/log6"
|
||||
check "channel local: a directory installs" 0 "done" \
|
||||
rinst "$H6" "$B6" RIG_INSTALL_SOURCE="$TBDIR/rig-7.7.7-relflow" CURL_STUB_LOG="$LOG6"
|
||||
check "channel local: the tree landed under its VERSION" 0 "" \
|
||||
test -x "$H6/versions/7.7.7-relflow/bin/rig"
|
||||
check "channel local: INSTALLED_FROM records local:<path>" 0 \
|
||||
"local:$TBDIR/rig-7.7.7-relflow" cat "$H6/versions/7.7.7-relflow/INSTALLED_FROM"
|
||||
check "channel local: curl was never consulted" 1 "" test -s "$LOG6"
|
||||
H7="$WORK/h7"; B7="$WORK/b7"; LOG7="$WORK/log7"
|
||||
check "channel local: a tarball installs too" 0 "done" \
|
||||
rinst "$H7" "$B7" RIG_INSTALL_SOURCE="$WORK/release.tgz" CURL_STUB_LOG="$LOG7"
|
||||
check "channel local: the tarball's tree landed" 0 "" \
|
||||
test -x "$H7/versions/7.7.7-relflow/bin/rig"
|
||||
check "channel local: ...also without a download" 1 "" test -s "$LOG7"
|
||||
H8="$WORK/h8"; B8="$WORK/b8"; LOG8="$WORK/log8"
|
||||
check "channel local: a missing path refuses BY NAME" 1 "$WORK/no-such-source" \
|
||||
rinst "$H8" "$B8" RIG_INSTALL_SOURCE="$WORK/no-such-source" CURL_STUB_LOG="$LOG8"
|
||||
check "channel local: the refusal installed NOTHING" 1 "" test -e "$H8"
|
||||
check "channel local: ...and downloaded nothing (no silent fallback)" 1 "" \
|
||||
test -s "$LOG8"
|
||||
|
||||
rm -rf "$WORK"
|
||||
|
||||
echo "---"
|
||||
|
|
|
|||
Loading…
Reference in a new issue