fix: one checksum policy, labeler coverage, orphaned-unit removal
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled

Net-new review findings from grok and kimi on !110. Their items 1-3 were
codex's, already fixed in 1933b07; these are the ones only they raised.

grok #4 — the two downloaders would drift. docs/templates/ci-box/install.sh
and the download block in forgejo-runner-install.sh were near-copies, and grok
named the exact consequence with the exact evidence: fail-open survived in BOTH
while a grep for "checksum mismatch" passed against both, because the string it
looked for sat right beside the branch it could not see. The whole policy —
fetch, unreadable, mismatch — is now fetch_and_verify_sha256, byte-identical in
both files and diffed by test/cli.sh. They cannot share a lib: the command
sources commands/lib/, and the template is a registry definition that runs
standalone inside a mint with rig's tree nowhere in reach, which is the same
situation valid_version faces between bin/rig and install.sh. Mutation-checked
by drifting one copy's message and confirming the diff goes red.

kimi #2 — the labeler could not see this family. scope:runner matched
commands/runner-*.sh only, so forgejo-runner-*.sh and the staged ci-box
definition scored no scope at all. Globs extended and the label's description
now says either forge rather than GitHub.

kimi #4 — remove stranded a unit whose user was gone. The missing-user check
exited 0 before the unit was ever looked at, so a deleted account with a
leftover forgejo-runner.service reported "nothing to remove" while the
absence-assert that never ran implied the opposite. The unit is now checked
independently. Auditing that fix surfaced a hazard kimi did not mention: with
the user gone RUNNER_DIR is "", and the later unguarded "$RUNNER_DIR/.rig-labels"
would have expanded to "/.rig-labels" — an rm at the filesystem root, as root.
Every RUNNER_DIR path is now gated, and a test pins that none is unguarded.

kimi #1 — the README handed out a config that breaks rig's own gates.
DEFAULT_ACTIONS_URL is a single fallback and rig's workflows need two origins;
measured: code.forgejo.org serves actions/checkout (200) but not
heavy-duty/ceremony (404), which lives on the Forgejo instance. With the value
the README recommended, all eight ceremony references fail to resolve. The
section now states the conflict with the counts, says which references would
break, and explicitly does NOT pick a side — that is an infra decision, and
rig's CI running on Forgejo is not something rig forgejo-runner depends on.
Asked the maintainer for direction.

746/31/43 pass, shellcheck clean, labeler.yml parses.

forgejo#109

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
cluade-reviewer-andresmgsl 2026-07-27 21:19:33 +00:00
parent 1933b07fd4
commit cf5858bb60
7 changed files with 207 additions and 72 deletions

4
.github/labeler.yml vendored
View file

@ -11,7 +11,9 @@
"scope:runner":
- changed-files:
- any-glob-to-any-file:
["commands/runner-*.sh", "commands/lib/runner-config.sh"]
["commands/runner-*.sh", "commands/lib/runner-config.sh",
"commands/forgejo-runner-*.sh", "commands/lib/forgejo-runner-config.sh",
"docs/templates/ci-box/**"]
"scope:coolify":
- changed-files:
- any-glob-to-any-file: ["commands/coolify-*.sh"]

2
.github/labels.conf vendored
View file

@ -2,7 +2,7 @@ panel=claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl kimi-bot-an
triage-actors=dan-claude-bot
scope:bootstrap|C5DEF5|bootstrap — hardening a pristine server into a node
scope:users|C5DEF5|users-* — class model, apply/status, close-root
scope:runner|C5DEF5|runner-* — GitHub runner lifecycle
scope:runner|C5DEF5|runner-* / forgejo-runner-* — CI runner lifecycle, either forge
scope:coolify|C5DEF5|coolify-* — Coolify and backup install
scope:db|C5DEF5|db.sh — dump/restore
scope:installer|C5DEF5|install.sh — how rig lands on a machine

View file

@ -1174,6 +1174,28 @@ FORGEJO__actions__ENABLED=true
FORGEJO__actions__DEFAULT_ACTIONS_URL=https://code.forgejo.org
```
> **`DEFAULT_ACTIONS_URL` is a single fallback, and rig's own workflows need
> two origins.** It decides where a *bare* `uses: owner/repo@ref` resolves.
> Measured:
>
> | reference | count in `.github/workflows/` | `code.forgejo.org` |
> |---|---|---|
> | `actions/checkout@v4` | 3 | **200** — mirrored |
> | `heavy-duty/ceremony/...@0.3.0` | 8 | **404** — lives on this Forgejo instead |
>
> So with the value above, every ceremony guard (`changelog-armed`,
> `changelog-monotonic`, `changelog-assembled`, `drill-recorded`,
> `runner-isolated`, `docs-sync`, and the release/labels callers) fails to
> resolve the moment rig's CI actually runs on the forge.
>
> Forgejo accepts an absolute URL per step, so one side has to be explicit —
> which one is a **decision this PR does not make**. Either set the default to
> this instance and make the three `actions/*` references absolute
> (`uses: https://code.forgejo.org/actions/checkout@v4`), or keep the value
> above and make the eight ceremony references absolute. Until that lands,
> **rig's CI is not expected to run on Forgejo**`rig forgejo-runner` is for
> running *your* repositories' workflows, and does not depend on this.
> **A registry served from Forgejo needs one more.** `RIG_TEMPLATES_HOST`
> (below) lets the template registry live on any forge, but the mint-time fetch
> is **unauthenticated by contract** — box auto-runs `rig bootstrap <role>-box`

View file

@ -33,6 +33,55 @@ die() { printf 'rig-forgejo-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
# from a shared CI server, and inside a box that boundary is already paid for.
DEFAULT_LABELS='ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm'
# fetch_and_verify_sha256 <asset-url> <file> <sumfile> <label>
#
# The whole checksum POLICY, in one place: fetch the published .sha256 beside
# an asset and prove the download matches it. Prints the reason on stderr and
# returns 1 on any failure; the caller supplies the refusal in its own voice.
#
# BYTE-IDENTICAL to the copy in docs/templates/ci-box/install.sh, diffed by
# test/cli.sh — the valid_version / templates_archive_urls precedent. The two
# downloaders cannot share a lib: this one sources commands/lib/, and that one
# is a REGISTRY DEFINITION that runs standalone inside a mint from a fetched
# tarball, with rig's tree nowhere in reach. So the pin is the only mechanism
# that keeps one policy from becoming two.
#
# Review !110 is the evidence for why that matters: a fail-open branch lived in
# BOTH copies while a grep for "checksum mismatch" passed against both, because
# the string it looked for sat right beside the branch it could not see. The
# next checksum-policy change must not be able to land in one file only.
#
# AN UNFETCHABLE CHECKSUM REFUSES — it is a gate, not a courtesy. The earlier
# reasoning ("do not let an upstream layout change break installs") reasons
# about the wrong failure: a layout change moves the BINARY url too, so the
# download would already have died. "Binary yes, checksum no" is not what a
# layout change looks like — it is what an interfered fetch looks like, which
# is precisely what a checksum exists to catch. Failing open would hand an
# unverified root install to anyone able to block a single URL. There is
# deliberately no bypass flag: if upstream really does move its assets, that is
# a rig PR editing the URL, not an operator improvising past a security gate.
fetch_and_verify_sha256() {
local url="$1" file="$2" sumfile="$3" label="$4" want got
if ! curl -fsSL "${url}.sha256" -o "$sumfile" 2>/dev/null; then
printf 'no published .sha256 for %s at %s.sha256 — the binary itself downloaded, so this is not an upstream layout change; check what is intercepting the fetch\n' "$label" "$url" >&2
return 1
fi
# The published .sha256 names the asset, not our temp path. Compare the
# digest itself rather than rewriting the file into sha256sum -c's format:
# one comparison, no parsing of a file we did not write.
want="$(tr -d '\r' < "$sumfile" 2>/dev/null | awk '{print $1}' | head -n1)"
got="$(sha256sum "$file" | awk '{print $1}')"
if [ -z "$want" ]; then
printf 'the published checksum for %s is unreadable — a fetch that succeeds but returns nothing usable is not a verified download\n' "$label" >&2
return 1
fi
if [ "$want" != "$got" ]; then
printf 'checksum mismatch for %s: published %s, downloaded %s\n' "$label" "$want" "$got" >&2
return 1
fi
printf 'checksum verified (%s)\n' "$got"
}
usage() {
cat <<'EOF'
usage: rig forgejo-runner install --instance <url> [options]
@ -286,35 +335,8 @@ if [ "$NEED_DOWNLOAD" -eq 1 ]; then
log "downloading forgejo-runner ${VERSION} (${ARCH})"
curl -fsSL "$URL" -o "$WORKDIR/forgejo-runner" \
|| die "could not download ${URL}"
# THE CHECKSUM IS A GATE, NOT A COURTESY — an unfetchable one refuses.
#
# This previously warned and installed anyway, reasoning that rig should not
# become unable to install if upstream changed its asset layout. That reasons
# about the wrong failure. The binary lands as root and is executed by a
# systemd unit, and the two ways the checksum can go missing are:
#
# - upstream moved the assets — in which case the BINARY url moved too, and
# the download above would already have died. A layout change does not
# present as "binary yes, checksum no".
# - something is interfering with the fetch — which is precisely the case
# the checksum exists to catch.
#
# So the asymmetry is itself the signal: same origin, same release tag, one
# answers and one does not. Failing open there hands an unverified root
# install to anyone who can block a single URL. There is deliberately no
# bypass flag: if upstream really does change layout, that is a rig PR
# editing the URL above, not an operator improvising past a security gate.
curl -fsSL "${URL}.sha256" -o "$WORKDIR/forgejo-runner.sha256" 2>/dev/null \
|| die "no published .sha256 for ${ASSET} at ${URL}.sha256 — refusing to install an unverified binary that runs as root. The binary itself downloaded, so this is not an upstream layout change; check what is intercepting the fetch."
# The published .sha256 names the asset, not our temp path. Compare the
# digest itself rather than rewriting the file into sha256sum -c's format:
# one comparison, no parsing of a file we did not write.
WANT="$(tr -d '\r' < "$WORKDIR/forgejo-runner.sha256" | awk '{print $1}' | head -n1)"
GOT="$(sha256sum "$WORKDIR/forgejo-runner" | awk '{print $1}')"
[ -n "$WANT" ] || die "the published checksum for ${ASSET} is unreadable — refusing to install an unverified binary"
[ "$WANT" = "$GOT" ] \
|| die "checksum mismatch for ${ASSET}: published ${WANT}, downloaded ${GOT} — refusing to install"
log "checksum verified (${GOT})"
fetch_and_verify_sha256 "$URL" "$WORKDIR/forgejo-runner" "$WORKDIR/forgejo-runner.sha256" "$ASSET" \
|| die "refusing to install an unverified ${ASSET} — it lands as root and runs under a systemd unit. See the checksum failure above."
# Staged beside the target and RENAMED into place, never written over.
# Replacing a running executable in place fails with ETXTBSY, and this path

View file

@ -67,19 +67,36 @@ fi
UNIT=/etc/systemd/system/forgejo-runner.service
# --- nothing to remove? -----------------------------------------------------
if ! id -u "$RUNNER_USER" >/dev/null 2>&1; then
log "no ${RUNNER_USER} user on this box; nothing to remove"
# The unit is checked INDEPENDENTLY of the user, and that ordering is the whole
# point. A missing user used to exit 0 here before the unit was ever looked at,
# so a deleted account with a leftover forgejo-runner.service reported "nothing
# to remove" and left the unit behind — while the absence-assert at the end,
# which never ran, implied removal had been complete. `bootstrap --undo`'s own
# unit check would still have caught it, but a verb that claims to have removed
# everything must not be the thing that lies about it.
RUNNER_DIR=""
if id -u "$RUNNER_USER" >/dev/null 2>&1; then
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
RUNNER_DIR="$USER_HOME/forgejo-runner"
else
log "no ${RUNNER_USER} user on this box"
fi
if [ -z "$RUNNER_DIR" ] && [ ! -e "$UNIT" ]; then
log "no runner user and no unit on this box; nothing to remove"
exit 0
fi
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
RUNNER_DIR="$USER_HOME/forgejo-runner"
if [ ! -e "$RUNNER_DIR/.runner" ] && [ ! -e "$UNIT" ]; then
if [ -n "$RUNNER_DIR" ] && [ ! -e "$RUNNER_DIR/.runner" ] && [ ! -e "$UNIT" ]; then
log "no runner registered in ${RUNNER_DIR}; nothing to remove"
exit 0
fi
[ -n "$RUNNER_DIR" ] || warn "the ${RUNNER_USER} user is gone but ${UNIT} is still here — removing the orphaned unit"
INSTANCE="$(forgejo_runner_instance "$RUNNER_DIR")"
RUNNER_NAME="$(forgejo_runner_name "$RUNNER_DIR")"
INSTANCE=""
RUNNER_NAME=""
if [ -n "$RUNNER_DIR" ]; then
INSTANCE="$(forgejo_runner_instance "$RUNNER_DIR")"
RUNNER_NAME="$(forgejo_runner_name "$RUNNER_DIR")"
fi
# --- service ---------------------------------------------------------------
# First, in both paths: stopping after the registration is wiped would strand a
@ -95,16 +112,23 @@ else
fi
# --- registration -----------------------------------------------------------
if [ -e "$RUNNER_DIR/.runner" ]; then
rm -f "$RUNNER_DIR/.runner"
log "wiped the local registration"
# Every path below is gated on RUNNER_DIR being non-empty. With the user gone
# it is "", and an unguarded "$RUNNER_DIR/.rig-labels" would expand to
# "/.rig-labels" — an rm at the filesystem root, as root. The repo already
# treats this class of expansion as a hazard worth spelling out (`rm -rf
# "${ir:?}/versions/$ver"` in bin/rig); same discipline here.
if [ -n "$RUNNER_DIR" ]; then
if [ -e "$RUNNER_DIR/.runner" ]; then
rm -f "$RUNNER_DIR/.runner"
log "wiped the local registration"
fi
rm -f "$RUNNER_DIR/.rig-labels"
fi
rm -f "$RUNNER_DIR/.rig-labels"
# END WITH THE ABSENCE ASSERT: "removed" is a claim, and claims get verified
# (the `rig uninstall` precedent).
leftover=""
[ -e "$RUNNER_DIR/.runner" ] && leftover="$leftover $RUNNER_DIR/.runner"
[ -n "$RUNNER_DIR" ] && [ -e "$RUNNER_DIR/.runner" ] && leftover="$leftover $RUNNER_DIR/.runner"
[ -e "$UNIT" ] && leftover="$leftover $UNIT"
if [ -n "$leftover" ]; then
printf 'rig-forgejo-runner: remove INCOMPLETE — still present:%s\n' "$leftover" >&2

View file

@ -18,6 +18,55 @@
# inside the box.
set -euo pipefail
# fetch_and_verify_sha256 <asset-url> <file> <sumfile> <label>
#
# The whole checksum POLICY, in one place: fetch the published .sha256 beside
# an asset and prove the download matches it. Prints the reason on stderr and
# returns 1 on any failure; the caller supplies the refusal in its own voice.
#
# BYTE-IDENTICAL to the copy in commands/forgejo-runner-install.sh, diffed by
# test/cli.sh — the valid_version / templates_archive_urls precedent. The two
# downloaders cannot share a lib: that one sources commands/lib/, and this one
# is a REGISTRY DEFINITION that runs standalone inside a mint from a fetched
# tarball, with rig's tree nowhere in reach. So the pin is the only mechanism
# that keeps one policy from becoming two.
#
# Review !110 is the evidence for why that matters: a fail-open branch lived in
# BOTH copies while a grep for "checksum mismatch" passed against both, because
# the string it looked for sat right beside the branch it could not see. The
# next checksum-policy change must not be able to land in one file only.
#
# AN UNFETCHABLE CHECKSUM REFUSES — it is a gate, not a courtesy. The earlier
# reasoning ("do not let an upstream layout change break installs") reasons
# about the wrong failure: a layout change moves the BINARY url too, so the
# download would already have died. "Binary yes, checksum no" is not what a
# layout change looks like — it is what an interfered fetch looks like, which
# is precisely what a checksum exists to catch. Failing open would hand an
# unverified root install to anyone able to block a single URL. There is
# deliberately no bypass flag: if upstream really does move its assets, that is
# a rig PR editing the URL, not an operator improvising past a security gate.
fetch_and_verify_sha256() {
local url="$1" file="$2" sumfile="$3" label="$4" want got
if ! curl -fsSL "${url}.sha256" -o "$sumfile" 2>/dev/null; then
printf 'no published .sha256 for %s at %s.sha256 — the binary itself downloaded, so this is not an upstream layout change; check what is intercepting the fetch\n' "$label" "$url" >&2
return 1
fi
# The published .sha256 names the asset, not our temp path. Compare the
# digest itself rather than rewriting the file into sha256sum -c's format:
# one comparison, no parsing of a file we did not write.
want="$(tr -d '\r' < "$sumfile" 2>/dev/null | awk '{print $1}' | head -n1)"
got="$(sha256sum "$file" | awk '{print $1}')"
if [ -z "$want" ]; then
printf 'the published checksum for %s is unreadable — a fetch that succeeds but returns nothing usable is not a verified download\n' "$label" >&2
return 1
fi
if [ "$want" != "$got" ]; then
printf 'checksum mismatch for %s: published %s, downloaded %s\n' "$label" "$want" "$got" >&2
return 1
fi
printf 'checksum verified (%s)\n' "$got"
}
BIN=/usr/local/bin/forgejo-runner
if [ -x "$BIN" ]; then
@ -58,29 +107,8 @@ curl -fsSL "$URL" -o "$WORKDIR/forgejo-runner" \
# Forgejo publishes a .sha256 beside each binary. Verifying it costs one
# request and makes the install auditable; this file executes as root inside
# every future mint, so an unverified download is the last thing it should do.
# An unfetchable checksum REFUSES — it does not warn and continue. The comment
# above says an unverified root download is the last thing this file should do,
# and an earlier draft then did exactly that whenever the .sha256 404'd. The
# binary downloading while its checksum does not is not an upstream layout
# change (that would break both URLs); it is the shape of an interfered fetch,
# which is the case the checksum exists to catch. Failing open here would hand
# an unverified root install, inside every future mint, to anyone who can block
# one URL.
if ! curl -fsSL "${URL}.sha256" -o "$WORKDIR/forgejo-runner.sha256" 2>/dev/null; then
echo "ci-box install: no published .sha256 for ${ASSET} — refusing to install an unverified binary that runs as root. The binary itself downloaded, so this is not an upstream layout change; check what is intercepting the fetch." >&2
exit 1
fi
WANT="$(tr -d '\r' < "$WORKDIR/forgejo-runner.sha256" | awk '{print $1}' | head -n1)"
GOT="$(sha256sum "$WORKDIR/forgejo-runner" | awk '{print $1}')"
if [ -z "$WANT" ]; then
echo "ci-box install: the published checksum for ${ASSET} is unreadable — refusing to install an unverified binary" >&2
exit 1
fi
if [ "$WANT" != "$GOT" ]; then
echo "ci-box install: checksum mismatch for ${ASSET}: published ${WANT}, downloaded ${GOT} — refusing to install" >&2
exit 1
fi
echo "ci-box install: checksum verified (${GOT})"
fetch_and_verify_sha256 "$URL" "$WORKDIR/forgejo-runner" "$WORKDIR/forgejo-runner.sha256" "$ASSET" \
|| { echo "ci-box install: refusing to install an unverified ${ASSET} — it lands as root inside every mint. See the checksum failure above." >&2; exit 1; }
install -m 0755 -o root -g root "$WORKDIR/forgejo-runner" "$BIN"
echo "ci-box install: installed ${BIN}"

View file

@ -3207,6 +3207,26 @@ check "forgejo-runner: remove --local explains why it is not a flag" 2 "always l
"$ROOT/commands/forgejo-runner-remove.sh" --local
check "forgejo-runner: repoint explains why it cannot exist" 2 "no deregistration endpoint" \
"$ROOT/bin/rig" forgejo-runner repoint --instance https://f.example.com
# remove used to exit 0 on a missing user BEFORE looking at the unit, so a
# deleted account with a leftover forgejo-runner.service reported "nothing to
# remove" and stranded it — while the absence-assert that never ran implied the
# opposite (review !110). The unit check must not sit behind the user check.
remove_checks_unit_independently() {
# The early-exit must require BOTH the user to be absent AND the unit to be
# missing; a user-only guard is the regression.
# shellcheck disable=SC2016 # the '$RUNNER_DIR'/'$UNIT' are LITERAL text being grepped for
grep -q 'if \[ -z "\$RUNNER_DIR" \] && \[ ! -e "\$UNIT" \]' "$ROOT/commands/forgejo-runner-remove.sh"
}
check "forgejo-runner: remove checks the unit even when the user is gone" 0 "" \
remove_checks_unit_independently
check "forgejo-runner: remove warns about an orphaned unit" 0 "orphaned unit" \
grep -o "orphaned unit" "$ROOT/commands/forgejo-runner-remove.sh"
# With the user gone RUNNER_DIR is "", and an unguarded "$RUNNER_DIR/.rig-labels"
# would expand to "/.rig-labels" — an rm at the filesystem root, as root.
# shellcheck disable=SC2016 # the '$RUNNER_DIR' is LITERAL text being grepped for
check "forgejo-runner: remove never rm's an unguarded \$RUNNER_DIR path" 1 "" \
grep -qE '^rm -f "\$RUNNER_DIR' "$ROOT/commands/forgejo-runner-remove.sh"
check "forgejo-runner: status --help exits 0" 0 "usage:" "$ROOT/commands/forgejo-runner-status.sh" --help
check "forgejo-runner: remove --help exits 0" 0 "usage:" "$ROOT/commands/forgejo-runner-remove.sh" --help
@ -3327,7 +3347,9 @@ check "ci-box: a matching checksum installs" 0 "checksum verified" \
cibox_run CB_SUM="$CB_GOOD"
check "ci-box: ...and the binary really landed" 0 "" cibox_installed
# The regression this replaces: a 404 on .sha256 used to warn and install.
check "ci-box: a MISSING checksum refuses (exit 1)" 1 "refusing to install an unverified binary" \
check "ci-box: a MISSING checksum refuses (exit 1)" 1 "refusing to install an unverified" \
cibox_run
check "ci-box: ...naming the fetch, not blaming an upstream layout change" 1 "check what is intercepting the fetch" \
cibox_run
check "ci-box: ...and installed NOTHING" 1 "" cibox_installed
check "ci-box: a MISMATCHED checksum refuses" 1 "checksum mismatch" \
@ -3337,15 +3359,30 @@ check "ci-box: an EMPTY checksum file refuses" 1 "unreadable" cibox_run CB_SUM=E
check "ci-box: ...and installed nothing there either" 1 "" cibox_installed
rm -rf "$CBW"
# The rig command's copy of the gate must not drift back to failing open.
# Grep-pinned only for the branch SHAPE (driving it needs root and systemd);
# the behaviour itself is proven above on the byte-equivalent template path.
check "forgejo-runner: install refuses an unfetchable checksum" 0 "refusing to install an unverified binary" \
grep -o "refusing to install an unverified binary" "$FR"
# ONE checksum policy, two files that cannot share a lib — the rig command
# sources commands/lib/, the template is a registry definition that runs
# standalone inside a mint with rig's tree nowhere in reach. So the copies are
# byte-identical and diffed, the valid_version precedent (review !110).
#
# This pin is what the driven tests above cannot give on their own: they
# exercise the TEMPLATE's copy, and without the diff a fix could land there
# while the rig command kept a stale policy. That is exactly how fail-open
# survived in both while a grep for "checksum mismatch" passed against both.
FVLIB="$(mktemp)"; FVTPL="$(mktemp)"
awk '/^fetch_and_verify_sha256\(\) \{/,/^\}/' "$FR" > "$FVLIB"
awk '/^fetch_and_verify_sha256\(\) \{/,/^\}/' "$CIBOX" > "$FVTPL"
check "checksum policy: extracted from the command (guards the awk)" 0 "sha256sum" cat "$FVLIB"
check "checksum policy: extracted from the template (guards the awk)" 0 "sha256sum" cat "$FVTPL"
check "checksum policy: the two copies are byte-identical" 0 "" diff "$FVLIB" "$FVTPL"
rm -f "$FVLIB" "$FVTPL"
# ...and neither may drift back to warn-and-continue.
check "forgejo-runner: install has no warn-and-continue checksum branch" 1 "" \
grep -q "WITHOUT checksum verification" "$FR"
check "ci-box: no warn-and-continue checksum branch either" 1 "" \
grep -q "WITHOUT checksum verification" "$CIBOX"
check "forgejo-runner: install routes through the shared checksum policy" 0 "fetch_and_verify_sha256" \
grep -o "fetch_and_verify_sha256 \"\$URL\"" "$FR"
# --- --version must converge, not be swallowed (review !110) ----------------
# forgejo-runner does NOT self-update, and a ci-box's template preinstalls the