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
3 changed files with 60 additions and 8 deletions
Showing only changes of commit fbdce5284e - Show all commits

View file

@ -1,9 +1,13 @@
- **Creds-free by default.** The box starts with no Forgejo credentials and no
git credentials. The runner binary is installed but **not registered**:
registration needs a short-lived token the operator mints in Forgejo
(Site Administration, org, or repo → Actions → Runners) and hands to
`sudo rig forgejo-runner install --instance <url>`. The token is consumed at
registration and never written to disk by rig. After that, the runner's own
credential lives in `~/forgejo-runner/.runner`, mode 0600 — never copy it,
print it, or commit it. Secrets that CI jobs need belong in Forgejo's repo or
org secrets, injected per job, not on this box.
registration needs a token the operator mints in Forgejo (Site
Administration, org, or repo → Actions → Runners) and hands to
`sudo rig forgejo-runner install --instance <url>`. rig never writes that
token to disk — but it does **not expire, and registering does not spend
it**. A Forgejo registration token stays valid until somebody mints a
replacement at that same scope, and it will register as many runners as it is
shown to. Treat a leaked one as live until it has been replaced. (GitHub's
equivalent expires in about an hour; do not carry that habit across.) After
registration the runner's own credential lives in `~/forgejo-runner/.runner`,
mode 0600 — never copy it, print it, or commit it. Secrets that CI jobs need
belong in Forgejo's repo or org secrets, injected per job, not on this box.

View file

@ -3,10 +3,20 @@
# TENANT_USER/TENANT_HOME/TENANT_GROUP/ROLE exported.
#
# This lands the BINARY ONLY. Registration is deliberately not here: it needs a
# short-lived token from the Forgejo instance, and a tenant install is
# registration token from the Forgejo instance, and a tenant install is
# creds-free by contract — box auto-runs it at mint, holding nothing. The
# operator registers afterwards, out loud:
#
# Deliberately NOT described the way the GitHub sibling describes its own
# registration token, which really does expire in about an hour. That adjective
# must not cross this forge boundary — a test pins its absence from this file
# and from creds.md, so do not reintroduce it by copying from `rig runner`.
# Forgejo's ActionRunnerToken carries no expiry field at all;
# NewRunnerToken invalidates prior tokens only when a replacement is minted at
# the same scope, and Register leaves the one it was handed active. It is
# reusable until replaced, so a leak stays live. See creds.md, which is the
# copy an agent inside the box actually reads.
#
# box shell ci-box
# sudo rig forgejo-runner install --instance https://forgejo.example.com
#

View file

@ -3190,6 +3190,18 @@ check "forgejo-runner: --instance needs a value" 2 "needs a value" "$FR" -
check "forgejo-runner: unknown flag exits 2" 2 "unknown flag" "$FR" --instance https://f.example.com --nope
check "forgejo-runner: empty --labels refused" 2 "must not be empty" "$FR" --instance https://f.example.com --labels ''
check "forgejo-runner: the runner user is never root" 2 "must not be root" "$FR" --instance https://f.example.com --user root
# The pin rig RESOLVES was charset-checked; the pin it is HANDED was not, and
# that one is the one that reaches a URL path. Refused by name at parse time.
check "forgejo-runner: --version refuses a path, not a release number" 2 "release number like" \
"$FR" --instance https://f.example.com --version ../../etc/passwd
check "forgejo-runner: --version refuses a non-numeric pin" 2 "release number like" \
"$FR" --instance https://f.example.com --version latest
# Reaching the root check is the proof a good pin got THROUGH validation: this
# runs as a normal user in CI, so "must run as root" is the next gate down.
check "forgejo-runner: a plain release number passes validation" 1 "must run as root" \
"$FR" --instance https://f.example.com --version 12.13.2
check "forgejo-runner: a leading v is stripped before that check" 1 "must run as root" \
"$FR" --instance https://f.example.com --version v12.13.2
# A schemeless host and a repo URL are the two ways an operator mis-states the
# instance, and only one of them would fail loudly on its own — a repo URL
# registers somewhere subtly wrong instead. Both refuse by name.
@ -3384,6 +3396,32 @@ check "ci-box: no warn-and-continue checksum branch either" 1 "" \
check "forgejo-runner: install routes through the shared checksum policy" 0 "fetch_and_verify_sha256" \
grep -o "fetch_and_verify_sha256 \"\$URL\"" "$FR"
# --- GitHub's token adjective must not cross the forge boundary -------------
# Measured in Forgejo's own source, not inferred: ActionRunnerToken carries NO
# expiry field; NewRunnerToken invalidates prior tokens only when a replacement
# is minted at the same scope; Register leaves the token it was handed active.
# Reusable until replaced — where GitHub's expires in about an hour.
#
# This is pinned rather than merely fixed because creds.md is spliced into the
# ci-box's own CONTEXT.md: it is the text an AGENT INSIDE THE BOX reads about
# its own credentials. "short-lived" there makes a leaked token look
# self-expiring while it is still registering runners. The wording arrived by
# being copied from the GitHub sibling, so the same copy can bring it back.
CICREDS="$ROOT/docs/templates/ci-box/creds.md"
check "ci-box: creds.md never calls the registration token short-lived" 1 "" \
grep -qi "short-lived" "$CICREDS"
# Plain absence, not a phrase match: the wording this replaced wrapped across
# two comment lines, so a phrase pin would have passed against the very text it
# was written to catch. The file explains the ban without spelling the word.
check "ci-box: the install header does not call it short-lived either" 1 "" \
grep -qi "short-lived" "$CIBOX"
# ...and says the true thing, so the pin cannot be satisfied by deleting the
# claim rather than correcting it.
check "ci-box: creds.md states the token does not expire" 0 "not expire" \
grep -o "not expire" "$CICREDS"
check "ci-box: creds.md states a leaked token stays live" 0 "leaked" \
grep -o "leaked" "$CICREDS"
# --- --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