#!/usr/bin/env bash # rig forgejo-runner install — Forgejo Actions runner as a systemd service # under an unprivileged user. Outbound-only (long-poll to the instance), no # inbound ports. Convergent toward --instance: re-running against the instance # the box is already on leaves it alone; a box registered to a DIFFERENT # instance is refused, never silently restarted on the old one. # # The GitHub sibling (runner-install.sh) refuses Docker outright: it converges # a fleet MACHINE, where `docker` group membership is root-equivalent and the # blast radius is the machine. This command's home is a ci-box TENANT, where # bootstrap-tenant.sh has already installed Docker and added the tenant user to # the group, and where the blast radius is a disposable guest with no inbound # path. Same trade, different machine, opposite answer — which is why this is a # separate command and not a flag on that one. set -euo pipefail HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" # shellcheck source=SCRIPTDIR/lib/forgejo-runner-config.sh . "$HERE/lib/forgejo-runner-config.sh" log() { printf 'rig-forgejo-runner: %s\n' "$*"; } warn() { printf 'rig-forgejo-runner: WARNING: %s\n' "$*" >&2; } die() { printf 'rig-forgejo-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } # The default label map. `runs-on: ubuntu-latest` is what a workflow written # for GitHub says, so it must mean something here or every workflow needs # editing to migrate; catthehacker's image is the act/Forgejo ecosystem's # stand-in for GitHub's runner image. `docker` is the lean second option. # # Every entry is `docker://` — jobs run in CONTAINERS on the box's own dockerd, # not on the box itself. No docker-in-docker: the guide this came from stacks a # privileged dind sidecar with a plaintext tcp://…:2375 daemon to isolate jobs # from a shared CI server, and inside a box that boundary is already paid for. # # `ubuntu-latest` is the SLIM act image and does not carry GitHub's tool # surface. That is a deliberate choice, ruled on 2026-08-01 (#144) after rig's # own `ci / check` failed 9 times on `shellcheck: command not found`. The # measurement, so nobody re-litigates it from the tag names: # # image on the wire extracted shellcheck # act-22.04 (this one) 0.55 GB 2.2 GB no # runner-22.04 0.55 GB 2.2 GB no <- not a middle # full-22.04 18.67 GB 54.52 GB yes # # There is no cheap parity image: `runner-22.04` sounds like one and is the # same slim class, tool for tool. Parity is 54.52 GB or nothing, and a # box-class ci tenant does not have it — so mapping `ubuntu-latest` to # `full-22.04` would be a default that cannot land on the host class it is # for. `apt-get install -y shellcheck` takes 7s and yields the SAME # ShellCheck 0.8.0 that `full-22.04` ships, so parity buys no newer tool # either. # # The rule that follows, and the one to state when a workflow surprises # someone: on this forge a workflow must not assume tools from the image. # `ubuntu-latest` means "a Linux container that runs GitHub-shaped # workflows", not "GitHub's runner image" — rig's own .github/workflows/ci.yml # installs what it uses, and that is the pattern to copy. # # `ubuntu-latest-full` is the escape hatch for anyone who does want the whole # GitHub tool surface. A label mapping pulls NOTHING until a job matches it, # so shipping it by default costs a box that never says `runs-on: # ubuntu-latest-full` exactly nothing — and it must ship by default, because # Forgejo freezes labels at registration and a label absent at that moment # cannot be added later without re-registering the runner. Budget ~120 GB of # disk on any box that intends to use it. DEFAULT_LABELS='ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,ubuntu-latest-full:docker://ghcr.io/catthehacker/ubuntu:full-22.04,docker:docker://node:22-bookworm' # Every label map rig has ever shipped as its DEFAULT, oldest first. Append the # outgoing string here whenever DEFAULT_LABELS changes; never edit or remove a # row, because the whole point is to recognise a runner registered long ago. # # This exists so a plain converge can tell "registered under an older rig" from # "the operator chose these labels" — the second must stay silent, and nothing # on disk distinguishes them except the string itself. SUPERSEDED_DEFAULTS=( # pre-#144: no ubuntu-latest-full escape hatch 'ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm' ) # labels_are_a_superseded_default — true when the recorded map is one # rig itself used to ship. Exact match only: a near-miss is an operator's map. labels_are_a_superseded_default() { local recorded="$1" past for past in "${SUPERSEDED_DEFAULTS[@]}"; do [ "$recorded" = "$past" ] && return 0 done return 1 } # fetch_and_verify_sha256