feat: Forgejo-native CI — a ci-box tenant and a forgejo-runner command family #110
6 changed files with 374 additions and 50 deletions
22
README.md
22
README.md
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
36
docs/templates/ci-box/install.sh
vendored
36
docs/templates/ci-box/install.sh
vendored
|
|
@ -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}"
|
||||
|
|
|
|||
147
test/cli.sh
147
test/cli.sh
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue