forked from heavy-duty/rig
Self-review of the whole head after the panel converged. One real defect,
reproduced verbatim before it was touched.
runner_version_of is a pipeline, and every forgejo-runner-* command runs under
set -euo pipefail. When the binary exits non-zero the pipeline fails, and
`PRESENT_VER="$(runner_version_of "$BIN")"` is an ASSIGNMENT — so install died
at that line, exit 1, printing nothing at all. The lib documented the opposite
("empty when it cannot answer"), and the refusal written for exactly this case
twenty lines later — "the download landed but cannot run" — was unreachable
code that could never have fired.
The shape is not hypothetical. `[ -x ]` yes, runnable no is what a truncated or
wrong-arch download leaves behind, and a ci-box's template preinstalls the
binary at mint — so the silent death sat on the one path this command family
exists to serve.
json_field, two files away, already carries `|| true` with this reasoning
written above it: "callers run under set -e with pipefail, where a grep that
matches nothing would otherwise kill the script with no message". The same
guard, for the same reason, at both sites: the lib reader and install's own
read of --version.
The test could not have caught it. vparse drove the lib through `bash -c` with
NO shell options, and its "bad" stub exits 0 — it proved garbage-in-empty-out
in a permissive shell, which is not where the function is used. vparse now runs
under set -euo pipefail, and a stub that exits non-zero is a third row.
Mutation-checked: that row exits 1 against the unpatched lib and 0 against the
fixed one. Install's site is grep-pinned, since reaching it for real needs root
and a downloaded binary.
748 passed (was 746), release 31, drill 43, shellcheck clean.
163 lines
7.9 KiB
Bash
163 lines
7.9 KiB
Bash
#!/usr/bin/env bash
|
|
# Shared reader for the Forgejo runner's own on-disk config ($RUNNER_DIR/.runner).
|
|
# Sourced by the forgejo-runner-* commands; never executed on its own.
|
|
#
|
|
# WHY A SECOND LIB, not an arm inside lib/runner-config.sh: the two files are
|
|
# different documents making different claims, and the sibling's helpers answer
|
|
# questions this one cannot ask. GitHub's .runner names a REPOSITORY
|
|
# (gitHubUrl), so `runner install` converges toward --repo. Forgejo's names an
|
|
# INSTANCE (address) and nothing else about scope — whether a registration is
|
|
# instance-wide, org, or single-repo is a property of the TOKEN, decided in
|
|
# Forgejo's UI before rig ever sees it. There is no repo here to converge
|
|
# toward, and no way to read one back. Sharing a reader would mean a
|
|
# gitHubUrl accessor that returns empty forever on one of the two forges.
|
|
#
|
|
# json_field is deliberately re-used FROM the sibling rather than copied: a
|
|
# rig-bootstrapped box has no jq, both files are flat JSON, and one grep/sed
|
|
# reader for both is the same trade lib/runner-config.sh already argued.
|
|
|
|
HERE_FJ="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=SCRIPTDIR/runner-config.sh
|
|
. "$HERE_FJ/runner-config.sh" # json_field
|
|
|
|
# THE CREDENTIAL FACT that shapes this whole family: Forgejo's .runner holds
|
|
# the runner's own long-lived token — the secret it authenticates every poll
|
|
# with — alongside address/name/labels. GitHub's holds no such thing.
|
|
#
|
|
# So the mode is part of the contract, not hygiene: a registration secret
|
|
# readable by every account on the box is a quiet, permanent credential leak,
|
|
# and it leaks silently — nothing fails, the runner keeps working. Converge is
|
|
# the only moment rig can notice a mode that drifted (an operator's editor, a
|
|
# restore from a tarball that lost modes, a hand-edit to add a label).
|
|
FORGEJO_RUNNER_FILE_MODE=600
|
|
|
|
# forgejo_runner_instance <runner_dir> — the Forgejo instance this box's runner
|
|
# is registered to, empty when nothing is registered there.
|
|
forgejo_runner_instance() {
|
|
[ -e "$1/.runner" ] || return 0
|
|
json_field "$1/.runner" address
|
|
}
|
|
|
|
# forgejo_runner_name <runner_dir> — the runner's name, empty when unregistered.
|
|
forgejo_runner_name() {
|
|
[ -e "$1/.runner" ] || return 0
|
|
json_field "$1/.runner" name
|
|
}
|
|
|
|
# forgejo_runner_secure <runner_dir> <user> <group> — converge .runner to 0600
|
|
# owned by the runner user. Called on every install, not only at registration.
|
|
# Silent on success: this is a mode that should always already be right, and a
|
|
# line saying so on every converge would train the reader to skip it.
|
|
forgejo_runner_secure() {
|
|
local dir="$1" user="$2" group="$3"
|
|
[ -e "$dir/.runner" ] || return 0
|
|
chmod "$FORGEJO_RUNNER_FILE_MODE" "$dir/.runner"
|
|
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.
|
|
#
|
|
# `|| true` for json_field's reason, which bites harder here. Callers run under
|
|
# `set -euo pipefail`, where a pipeline whose FIRST stage exits non-zero fails
|
|
# the whole pipeline — and `PRESENT_VER="$(runner_version_of "$BIN")"` is an
|
|
# assignment, so the script dies AT THAT LINE, with no message. "Empty when it
|
|
# cannot answer" is only true if this says so out loud.
|
|
#
|
|
# That is not a hypothetical shape: the binary is `[ -x ]` but unrunnable
|
|
# exactly when a ci-box's template preinstall landed a truncated or wrong-arch
|
|
# download — the one path this command family exists for. Without this, install
|
|
# exits 1 in silence and the refusal written for that case ("the download
|
|
# landed but cannot run") is unreachable code.
|
|
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' || true
|
|
}
|
|
|
|
# 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
|
|
# <instance-url>: re-running `install` against the instance the box is already
|
|
# on is real convergence — it re-uses the binary, skips registration, exits 0.
|
|
#
|
|
# Returns 1, explaining itself on stderr, when the runner is registered to a
|
|
# DIFFERENT instance. Skipping *that* is not convergence, it is ignoring the
|
|
# argument: `install` would skip its registration step, restart the service
|
|
# against the OLD instance, and report success — leaving the instance you asked
|
|
# for with no runner and its jobs queued against one that will never come.
|
|
#
|
|
# This is assert_runner_repo's reasoning, asked about the axis Forgejo actually
|
|
# has. There is deliberately no `repoint` sibling: Forgejo has no
|
|
# deregistration handshake to perform against the old instance, so moving a
|
|
# runner is `remove` then `install` — two acts that are already honest about
|
|
# leaving a stale entry behind, rather than one verb pretending to be atomic.
|
|
assert_runner_instance() {
|
|
local dir="$1" wanted="$2" current
|
|
[ -e "$dir/.runner" ] || return 0
|
|
|
|
current="$(forgejo_runner_instance "$dir")"
|
|
|
|
if [ -z "$current" ]; then
|
|
printf 'rig-forgejo-runner: ERROR: %s\n' \
|
|
"${dir}/.runner exists but names no instance — this box's registration cannot
|
|
be read, so rig cannot tell whether it is already on ${wanted}.
|
|
Wipe the local registration and install again:
|
|
rig forgejo-runner remove" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Trailing slashes are a spelling difference, not a different instance:
|
|
# forgejo-runner records the URL as given, so `--instance https://f.example/`
|
|
# and `--instance https://f.example` would otherwise read as a move.
|
|
if [ "${current%/}" = "${wanted%/}" ]; then
|
|
return 0
|
|
fi
|
|
|
|
printf 'rig-forgejo-runner: ERROR: %s\n' \
|
|
"this box's runner is already registered to ${current}, not ${wanted}.
|
|
install will not move a runner between instances: it would leave the service
|
|
running against the OLD instance and report success. To move it, take it off
|
|
the old instance first:
|
|
rig forgejo-runner remove
|
|
then install against the new one. Forgejo has no deregistration handshake, so
|
|
the old entry stays listed until you delete it in that instance's admin UI." >&2
|
|
return 1
|
|
}
|