feat: Forgejo-native CI — a ci-box tenant and a forgejo-runner command family #110

Merged
andres merged 9 commits from build/109-forgejo-ci-box into main 2026-07-28 19:58:37 +00:00
6 changed files with 374 additions and 50 deletions
Showing only changes of commit 1933b07fd4 - Show all commits

View file

@ -1094,12 +1094,28 @@ the machine. Here it is a disposable guest. Same trade, different box, opposite
answer — which is why these are two commands and not one with a `--forge` flag.
- `--version <pin>``forgejo-runner` release (default: latest at install
time). The published `.sha256` is **verified before the binary is
installed**; a mismatch refuses.
time). **Convergent, and it replaces an existing binary** — including
downward, because a pin is an instruction rather than a floor. Without a
pin, a binary already on the box is left alone: chasing "latest" on every
converge would make a plain re-run an unrequested upgrade.
The published `.sha256` is **verified before the binary is installed**, and
a missing or unreadable checksum **refuses** just as a mismatch does — this
binary runs as root under a systemd unit, so the gate does not fail open.
> Unlike `rig runner install`, presence alone is not enough to skip the
> download here. That command can skip because `actions/runner`
> **self-updates**; `forgejo-runner` does not, so nothing else would ever
> move the version — and a ci-box's template preinstalls the binary at mint,
> which would leave `--version` doing nothing on the exact path this command
> is for.
- `--name <name>` — runner name (default: this host's hostname)
- `--labels <csv>` — replaces the default map:
`ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm`,
so `runs-on: ubuntu-latest` works in a workflow written for GitHub
so `runs-on: ubuntu-latest` works in a workflow written for GitHub.
**Applied at registration only.** Forgejo owns a runner's labels from the
moment it registers, so passing `--labels` to a re-run cannot change them —
rig says so rather than letting the request evaporate, and changing labels
means `remove` then `install` again
- `--user <name>` — service user (default: `ci` when it exists, else
`forgejo-runner`)

View file

@ -79,6 +79,12 @@ INSTANCE=""
VERSION=""
RUNNER_NAME="$(hostname)"
LABELS="$DEFAULT_LABELS"
# Whether --labels was ASKED FOR, distinct from what it resolved to. A rerun
# cannot apply labels (Forgejo owns them from registration time), and the
# difference between "operator requested a change" and "operator passed
# nothing" is what separates a warning worth printing from noise on every
# converge.
LABELS_EXPLICIT=0
RUNNER_USER=""
while [ $# -gt 0 ]; do
case "$1" in
@ -93,7 +99,7 @@ while [ $# -gt 0 ]; do
RUNNER_NAME="$2"; shift 2 ;;
--labels)
[ $# -ge 2 ] || die "--labels needs a value" 2
LABELS="$2"; shift 2 ;;
LABELS="$2"; LABELS_EXPLICIT=1; shift 2 ;;
--user)
[ $# -ge 2 ] || die "--user needs a value" 2
RUNNER_USER="$2"; shift 2 ;;
@ -207,9 +213,52 @@ fi
# Forgejo publishes BARE BINARIES (not a tarball) with a .sha256 beside each
# one. Taking that checksum is nearly free and makes the install auditable —
# the same instinct as `coolify install`'s mandatory version pin.
#
# "Already present" is NOT enough to skip here, and this is where the GitHub
# sibling's shape must not be copied. Its skip is justified by "self-update
# owns upgrades" — actions/runner updates itself, and GitHub refuses jobs from
# stale runners, so freezing it would be pointless. **forgejo-runner does not
# self-update.** Nothing else ever moves the version, so a bare presence check
# would mean the binary a box first happened to get is the binary it keeps
# forever.
#
# That lands hardest on the path this command is FOR: a ci-box's template
# install.sh preinstalls /usr/local/bin/forgejo-runner at mint, so the
# executable always exists before an operator ever runs this — and --version,
# documented as the deterministic-pin lever, would silently do nothing on
# every ci-box in the fleet.
#
# So: converge toward --version when it is given, exactly as this command
# converges toward --instance. A pin is not a trust boundary the way an
# instance is (that one refuses), it is an instruction — including downward,
# which is what a pin is for. Absent a pin, an existing binary is left alone:
# chasing "latest" on every converge would make a re-run an unrequested
# upgrade, and convergence must not be a moving target.
# runner_version_of / runner_download_decision live in the lib, so the rule can
# be driven by test/cli.sh without root — see there for the full reasoning.
PRESENT_VER=""
HAVE_BIN=no
if [ -x "$BIN" ]; then
log "forgejo-runner binary already present at ${BIN}; skipping download"
else
HAVE_BIN=yes
PRESENT_VER="$(runner_version_of "$BIN")"
fi
case "$(runner_download_decision "$HAVE_BIN" "$PRESENT_VER" "$VERSION")" in
skip)
NEED_DOWNLOAD=0
if [ -n "$VERSION" ]; then
log "forgejo-runner ${VERSION} already installed; skipping download"
else
log "forgejo-runner ${PRESENT_VER:-(version unreadable)} already present at ${BIN}; skipping download (pass --version <pin> to converge to a specific release)"
fi ;;
converge)
NEED_DOWNLOAD=1
log "converging ${BIN}: ${PRESENT_VER:-unreadable} -> ${VERSION} (--version)" ;;
*)
NEED_DOWNLOAD=1 ;;
esac
if [ "$NEED_DOWNLOAD" -eq 1 ]; then
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
@ -237,27 +286,54 @@ else
log "downloading forgejo-runner ${VERSION} (${ARCH})"
curl -fsSL "$URL" -o "$WORKDIR/forgejo-runner" \
|| die "could not download ${URL}"
if curl -fsSL "${URL}.sha256" -o "$WORKDIR/forgejo-runner.sha256" 2>/dev/null; then
# 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})"
else
# A warning, not a refusal: rig should not become unable to install because
# upstream changed its asset layout. But it must be LOUD — an operator who
# needs a verified install has to know this one was not.
warn "no published .sha256 for ${ASSET} — installing WITHOUT checksum verification"
fi
install -m 0755 -o root -g root "$WORKDIR/forgejo-runner" "$BIN"
# 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})"
# Staged beside the target and RENAMED into place, never written over.
# Replacing a running executable in place fails with ETXTBSY, and this path
# now runs on boxes where the daemon is live (a --version converge). A
# rename is atomic and leaves the running process on the old inode until the
# restart below picks up the new one.
install -m 0755 -o root -g root "$WORKDIR/forgejo-runner" "$BIN.rig-new"
mv -f "$BIN.rig-new" "$BIN"
log "installed ${BIN}"
fi
INSTALLED_VER="$("$BIN" --version 2>/dev/null | head -n1)"
[ -n "$INSTALLED_VER" ] || die "${BIN} does not answer --version — the download landed but cannot run"
# The converge actually took — asserted, not assumed. A pin that silently did
# not land is exactly the failure --version exists to make impossible.
if [ -n "$VERSION" ]; then
EFFECTIVE_VER="$(runner_version_of "$BIN")"
[ "$EFFECTIVE_VER" = "$VERSION" ] \
|| die "asked for forgejo-runner ${VERSION} but ${BIN} reports ${EFFECTIVE_VER:-nothing} after install"
fi
# --- register ----------------------------------------------------------------
# UPSTREAM MARKS `register` DEPRECATED (measured on v12.13.2: both `register`
@ -285,6 +361,17 @@ INSTALLED_VER="$("$BIN" --version 2>/dev/null | head -n1)"
install -d -m 0755 -o "$RUNNER_USER" -g "$RUNNER_GROUP" "$RUNNER_DIR"
if [ -e "$RUNNER_DIR/.runner" ]; then
log "already registered; skipping registration"
# Registration was skipped, so the labels on the instance are the ones it was
# registered with — NOT whatever this invocation was passed. Say so when the
# operator explicitly asked for different ones, rather than letting the
# request evaporate. Only when EXPLICIT: comparing the default against a
# runner registered with custom labels would warn on every plain converge.
if [ "$LABELS_EXPLICIT" -eq 1 ] && [ -r "$RUNNER_DIR/.rig-labels" ]; then
RECORDED="$(cat "$RUNNER_DIR/.rig-labels")"
if [ "$RECORDED" != "$LABELS" ]; then
warn "--labels was not applied: this runner is already registered, and Forgejo owns its labels from registration time. It still has: ${RECORDED}. Labels are what 'runs-on' matches, so changing them means re-registering: 'rig forgejo-runner remove' then install again with the labels you want."
fi
fi
else
log "registering runner ${RUNNER_NAME} against ${INSTANCE}"
(cd "$RUNNER_DIR" && runuser -u "$RUNNER_USER" -- env HOME="$USER_HOME" \
@ -294,17 +381,21 @@ else
|| die "registration failed — check the token is a RUNNER registration token from ${INSTANCE} and has not been used already"
[ -e "$RUNNER_DIR/.runner" ] \
|| die "register reported success but wrote no ${RUNNER_DIR}/.runner"
# INSIDE the registration branch, where runner-install.sh keeps its copy and
# for the same reason: this file records what rig ACTUALLY registered with,
# so `status` has something to read back. Writing it unconditionally — as an
# earlier draft did — makes a plain re-run stamp this invocation's labels
# over a registration that used different ones, and `status` then reports
# confidently wrong labels while Forgejo still holds the originals. A
# metadata file that can disagree with the thing it describes is worse than
# no metadata file.
printf '%s\n' "$LABELS" > "$RUNNER_DIR/.rig-labels"
chown "$RUNNER_USER:$RUNNER_GROUP" "$RUNNER_DIR/.rig-labels"
fi
# EVERY run, registration or not: .runner holds the runner's own long-lived
# token, and a mode that drifted leaks it silently. See the lib.
forgejo_runner_secure "$RUNNER_DIR" "$RUNNER_USER" "$RUNNER_GROUP"
# rig records the labels beside the registration for `status` to read back.
# Box-local metadata, never a credential — the same note runner-install.sh
# writes, for the same reason.
printf '%s\n' "$LABELS" > "$RUNNER_DIR/.rig-labels"
chown "$RUNNER_USER:$RUNNER_GROUP" "$RUNNER_DIR/.rig-labels"
# --- service -------------------------------------------------------------
# Written by rig rather than shipped by upstream: forgejo-runner has no
# svc.sh, so there is no vendor unit to defer to (the GitHub sibling defers to

View file

@ -55,6 +55,51 @@ forgejo_runner_secure() {
chown "$user:$group" "$dir/.runner"
}
# runner_version_of <bin> — the bare version number ("12.13.2") the binary
# reports, empty when it cannot answer. `forgejo-runner --version` prints
# "forgejo-runner version v12.13.2"; the leading v is stripped so this compares
# against a --version argument, which has its own v stripped at parse.
runner_version_of() {
"$1" --version 2>/dev/null | head -n1 \
| sed -nE 's/.*[Vv]ersion[[:space:]]+v?([0-9][0-9A-Za-z.+-]*).*/\1/p'
}
# runner_download_decision <have-binary yes|no> <present-ver> <wanted-ver>
# -> "install" | "skip" | "converge"
#
# A PURE function, and pure on purpose: this is the decision review !110 caught
# being wrong, and it was wrong in a way no grep could see. Lifting it out of
# the root-only install path is what makes "a pre-existing binary plus
# --version" a real test rather than a string match.
#
# The rule, and why it is not the GitHub sibling's:
#
# no binary -> install. Nothing to reason about.
# binary, no --version -> skip. Chasing "latest" on every converge would make
# a plain re-run an unrequested upgrade, and a
# convergent verb must not be a moving target.
# binary, pin matches -> skip.
# binary, pin differs -> CONVERGE, including downward. A pin is an
# instruction, not a floor.
#
# runner-install.sh skips on mere presence because actions/runner SELF-UPDATES,
# so its version moves regardless and freezing it would only make GitHub refuse
# the runner's jobs. forgejo-runner does not self-update: nothing else ever
# moves this version, and a ci-box's template preinstalls the binary at mint —
# so mere-presence here would leave --version dead on the one path this whole
# command exists to serve.
#
# An unreadable present version (empty) with a pin asked for falls to
# "converge", which is the right direction: a binary that cannot say what it is
# should be replaced by one that can.
runner_download_decision() {
local have="$1" present="$2" want="$3"
[ "$have" = yes ] || { printf 'install\n'; return 0; }
[ -n "$want" ] || { printf 'skip\n'; return 0; }
[ "$present" = "$want" ] && { printf 'skip\n'; return 0; }
printf 'converge\n'
}
# assert_runner_instance <runner_dir> <instance-url>
#
# Returns 0 when the box has no runner, or has one already registered to

View file

@ -215,6 +215,37 @@ registered to re-uses the binary and skips registration; a **different**
instance refuses and names both. Same trust-boundary reasoning as
`assert_runner_repo`, asked about the axis Forgejo actually has.
### Three corrections from review !110
The first draft carried three defects that review caught, all of the same
family — a stated contract that the code did not actually keep:
1. **`--version` was swallowed on the primary path.** The download block
skipped on mere presence, copying `rig runner install`'s shape without its
justification: `actions/runner` self-updates, `forgejo-runner` does not. And
a ci-box's template *preinstalls* the binary at mint, so the flag documented
as the deterministic-pin lever could never fire on a ci-box. The decision is
now `runner_download_decision` in the lib — a pure function, so the rule is
driven by tests instead of asserted by grep — and the binary is renamed into
place rather than written over, since the converge path now runs while the
daemon is live (in-place would be `ETXTBSY`).
2. **`.rig-labels` outlived the registration it described.** The write had
escaped the registration branch, where `runner-install.sh` correctly keeps
its copy. A plain re-run stamped that invocation's labels over a
registration made with different ones, and `status` then reported
confidently wrong labels while Forgejo held the originals. It is scoped
again, and an *explicit* `--labels` on a re-run now warns that Forgejo owns
labels from registration time.
3. **The checksum gate failed open.** A missing `.sha256` warned and installed
anyway — contradicting both the README and this file's own comment about
unverified root downloads. The original reasoning (do not let an upstream
layout change break installs) reasons about the wrong failure: a layout
change breaks the *binary* URL too, so "binary yes, checksum no" is the
shape of an interfered fetch, which is what the checksum is for. Both paths
refuse now, with no bypass flag.
### `.runner` holds a credential here
GitHub's `.runner` names a repo. Forgejo's holds `address`, `name`, `labels`

View file

@ -58,21 +58,29 @@ 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.
if curl -fsSL "${URL}.sha256" -o "$WORKDIR/forgejo-runner.sha256" 2>/dev/null; then
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})"
else
echo "ci-box install: WARNING: no published .sha256 for ${ASSET} — installing WITHOUT checksum verification" >&2
# 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})"
install -m 0755 -o root -g root "$WORKDIR/forgejo-runner" "$BIN"
echo "ci-box install: installed ${BIN}"

View file

@ -3266,13 +3266,146 @@ check "forgejo-runner: status warns on a drifted mode" 0 "FORGEJO_RUNNER_FILE_MO
grep -o "FORGEJO_RUNNER_FILE_MODE" "$ROOT/commands/forgejo-runner-status.sh"
rm -rf "$FRW"
# The download must be verified. This binary is installed as root and executed
# by a systemd unit; an unverified fetch is the one step here that silently
# turns a network compromise into root on the box.
check "forgejo-runner: install verifies the published checksum" 0 "checksum mismatch" \
grep -o "checksum mismatch" "$FR"
check "ci-box: its install.sh verifies the published checksum too" 0 "checksum mismatch" \
grep -o "checksum mismatch" "$ROOT/docs/templates/ci-box/install.sh"
# --- the checksum gate, DRIVEN not grepped (review !110) --------------------
# This binary is installed as root and executed by a systemd unit, so the
# verification is the one step here that silently turns a network compromise
# into root on the box. A grep for the mismatch string proved only that a
# string existed — it could not have caught the fail-open branch that used to
# sit beside it. So drive the real script against a stub curl, the
# test/release.sh idiom, and assert on what it DOES.
CIBOX="$ROOT/docs/templates/ci-box/install.sh"
CBW="$(mktemp -d)"; CBSTUB="$CBW/stub"; mkdir -p "$CBSTUB"
cat > "$CBSTUB/curl" <<'CBCURL'
#!/usr/bin/env bash
# Scripted curl — never the network.
# CB_REDIRECT what -w %{url_effective} answers (the latest-release probe)
# CB_PAYLOAD file copied to -o for the BINARY url
# CB_SUM text written to -o for the .sha256 url; unset => that url 404s
set -u
out="" url="" probe=0
while [ $# -gt 0 ]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-w) probe=1; shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
if [ "$probe" -eq 1 ]; then printf '%s' "${CB_REDIRECT:-}"; exit 0; fi
case "$url" in
*.sha256)
[ -n "${CB_SUM:-}" ] || exit 22
# A fetch that SUCCEEDS but yields nothing usable is its own case, distinct
# from a 404 — a truncated proxy response looks exactly like this.
if [ "$CB_SUM" = EMPTY ]; then : > "${out:?}"; exit 0; fi
printf '%s asset\n' "$CB_SUM" > "${out:?}"; exit 0 ;;
*)
cp "${CB_PAYLOAD:?}" "${out:?}"; exit 0 ;;
esac
CBCURL
chmod +x "$CBSTUB/curl"
printf 'not-really-a-binary\n' > "$CBW/payload"
CB_GOOD="$(sha256sum "$CBW/payload" | awk '{print $1}')"
# install(1) must not touch the real /usr/local/bin, and the script runs as a
# non-root user here — stub it to a writable target so the test reaches the
# checksum logic rather than dying on permissions.
cat > "$CBSTUB/install" <<CBINST
#!/usr/bin/env bash
exec /usr/bin/install -m 0755 "\${@: -2:1}" "$CBW/installed"
CBINST
chmod +x "$CBSTUB/install"
cibox_run() { # cibox_run [VAR=val ...] — the REAL template install.sh, stubbed
rm -f "$CBW/installed"
env PATH="$CBSTUB:$PATH" \
CB_REDIRECT=https://code.forgejo.org/forgejo/runner/releases/tag/v9.9.9 \
CB_PAYLOAD="$CBW/payload" "$@" bash "$CIBOX"
}
cibox_installed() { [ -e "$CBW/installed" ]; }
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" \
cibox_run
check "ci-box: ...and installed NOTHING" 1 "" cibox_installed
check "ci-box: a MISMATCHED checksum refuses" 1 "checksum mismatch" \
cibox_run CB_SUM=0000000000000000000000000000000000000000000000000000000000000000
check "ci-box: ...and installed nothing either" 1 "" cibox_installed
check "ci-box: an EMPTY checksum file refuses" 1 "unreadable" cibox_run CB_SUM=EMPTY
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"
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"
# --- --version must converge, not be swallowed (review !110) ----------------
# forgejo-runner does NOT self-update, and a ci-box's template preinstalls the
# binary at mint — so a bare presence check would make --version dead on the
# exact path this command exists for.
FRLIB="$ROOT/commands/lib/forgejo-runner-config.sh"
vparse() { bash -c ". '$FRLIB'; runner_version_of '$1'"; }
VSTUB="$(mktemp -d)"
printf '#!/bin/sh\necho "forgejo-runner version v12.13.2"\n' > "$VSTUB/fr"; chmod +x "$VSTUB/fr"
printf '#!/bin/sh\necho "garbage"\n' > "$VSTUB/bad"; chmod +x "$VSTUB/bad"
check "forgejo-runner: the version reader strips the leading v" 0 "12.13.2" vparse "$VSTUB/fr"
check "forgejo-runner: an unreadable version yields empty, not garbage" 0 "" vparse "$VSTUB/bad"
rm -rf "$VSTUB"
# The decision itself, driven — this is the case review !110 caught, and a
# grep could not have caught it. Each row is a real lifecycle situation.
dec() { bash -c ". '$FRLIB'; runner_download_decision \"\$1\" \"\$2\" \"\$3\"" _ "$@"; }
check "version: nothing installed -> install" 0 "install" dec no "" ""
check "version: nothing installed, pin asked -> install" 0 "install" dec no "" 12.13.2
check "version: binary present, no pin -> skip (no surprise upgrade)" 0 "skip" dec yes 12.13.2 ""
check "version: binary present, pin MATCHES -> skip" 0 "skip" dec yes 12.13.2 12.13.2
# THE ci-box CASE: the template preinstalled a binary at mint, and the operator
# then pins. Mere presence used to swallow this entirely.
check "version: binary present, pin DIFFERS -> converge" 0 "converge" dec yes 12.13.2 12.13.0
check "version: a pin may converge DOWNWARD (a pin is not a floor)" 0 "converge" dec yes 12.13.2 9.0.0
check "version: an unreadable present version + pin -> converge" 0 "converge" dec yes "" 12.13.2
# ...and the command must actually consult it rather than re-deciding inline.
check "forgejo-runner: install routes the decision through the shared rule" 0 "runner_download_decision" \
grep -o "runner_download_decision" "$FR"
# shellcheck disable=SC2016 # the '${BIN}' is the LITERAL text being grepped for
check "forgejo-runner: the pin is asserted to have LANDED, not assumed" 0 "reports" \
grep -o 'but ${BIN} reports' "$FR"
# Replacing a live executable in place is ETXTBSY; the converge path now runs
# on boxes where the daemon is up, so it must rename into place.
# shellcheck disable=SC2016 # the '$BIN' is the LITERAL text being grepped for
check "forgejo-runner: the binary is renamed into place, never written over" 0 "mv -f" \
grep -o 'mv -f "\$BIN.rig-new" "\$BIN"' "$FR"
# --- .rig-labels must not outlive the registration it describes (review !110)
# A plain re-run used to stamp this invocation's labels over a registration
# made with different ones — status then reported confidently wrong labels
# while Forgejo still held the originals.
labels_write_is_scoped() {
# The write must sit INSIDE the else-branch that actually registers, the way
# runner-install.sh keeps its copy.
awk '/^else$/,/^fi$/' "$FR" | grep -q 'rig-labels'
}
check "forgejo-runner: .rig-labels is written only where registration happens" 0 "" \
labels_write_is_scoped
check "forgejo-runner: an explicit --labels on a rerun warns it was not applied" 0 "was not applied" \
grep -o -- "--labels was not applied" "$FR"
check "forgejo-runner: that warning is gated on --labels being EXPLICIT" 0 "LABELS_EXPLICIT" \
grep -o "LABELS_EXPLICIT" "$FR"
# The GitHub sibling is the precedent this restores — pin that it still scopes
# its own write, so the two cannot drift apart again.
gh_labels_write_is_scoped() {
awk '/^else$/,/^fi$/' "$ROOT/commands/runner-install.sh" | grep -q 'rig-labels'
}
check "rig runner: the sibling still scopes ITS .rig-labels write too" 0 "" \
gh_labels_write_is_scoped
# undo must refuse under a live Forgejo runner for a STRONGER reason than the
# GitHub one: Forgejo has no deregistration endpoint, so the ghost it strands