Compare commits
2 commits
main
...
build/144-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0daa2d0095 | |||
| cc5f478e77 |
9 changed files with 190 additions and 344 deletions
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
|
|
@ -15,6 +15,27 @@ jobs:
|
|||
# a skip (a guard that can quietly stop guarding is the failure
|
||||
# shape these checks exist to refuse).
|
||||
fetch-depth: 0
|
||||
# GitHub's `ubuntu-latest` ships shellcheck. The image this forge maps
|
||||
# that label to does not, and no cheap image does: measured 2026-08-01,
|
||||
# catthehacker's slim `act-22.04` and `runner-22.04` both lack it, and
|
||||
# the parity image that carries it is 54.5 GB extracted — more than a
|
||||
# box-class ci tenant has (#144, the numbers live beside DEFAULT_LABELS
|
||||
# in commands/forgejo-runner-install.sh). So the job equips itself
|
||||
# rather than the label promising a tool surface it cannot deliver.
|
||||
#
|
||||
# The `command -v` short-circuit is what keeps either forge from paying
|
||||
# for the other: on GitHub the tool is already there and no apt runs.
|
||||
# Keep the sudo — it is a no-op on the Forgejo path (act jobs run as
|
||||
# uid 0, and that image has no `runner` account at all) and load-bearing
|
||||
# on the GitHub path, where the job runs as `runner` with passwordless
|
||||
# sudo. It reads like dead weight and is not: it is what keeps this
|
||||
# green on GitHub the day that image stops preinstalling shellcheck.
|
||||
- name: shellcheck — preinstalled on GitHub, installed here
|
||||
run: |
|
||||
command -v shellcheck >/dev/null || {
|
||||
sudo apt-get update && sudo apt-get install -y shellcheck
|
||||
}
|
||||
shellcheck --version
|
||||
- name: shellcheck
|
||||
# -x follows the `source=SCRIPTDIR/...` directives into commands/lib/.
|
||||
# globstar so a script in a new subdirectory is linted without anyone
|
||||
|
|
@ -26,21 +47,7 @@ jobs:
|
|||
# The file list is printed so under-coverage shows up in the log, and
|
||||
# the comm below turns under-coverage into a failure rather than a
|
||||
# thing someone has to notice: every tracked `.sh` must be in the set.
|
||||
#
|
||||
# Install shellcheck when missing (#144). rig's default Forgejo label
|
||||
# maps ubuntu-latest to catthehacker's act-22.04 (slim), which does not
|
||||
# ship shellcheck; GitHub-hosted ubuntu-latest does. The conditional
|
||||
# keeps each forge from paying for the other.
|
||||
#
|
||||
# sudo: load-bearing on GitHub (job runs as `runner` with passwordless
|
||||
# sudo) and a no-op on act-22.04 (jobs run as uid 0; the image has no
|
||||
# `runner` account). Do not delete it as "dead weight" — that breaks
|
||||
# the GitHub half the day that image stops preinstalling shellcheck.
|
||||
run: |
|
||||
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y shellcheck
|
||||
fi
|
||||
shopt -s globstar dotglob
|
||||
files=(bin/* **/*.sh)
|
||||
printf 'shellcheck: %s\n' "${files[@]}"
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
### Fixed
|
||||
|
||||
- `rig forgejo-runner install`, `rig runner install`, `rig users apply` and `rig bootstrap <tenant>` refuse with a named remedy when root's `PATH` carries no `/usr/sbin`, instead of dying on `useradd: command not found` after prompting for a token (#139)
|
||||
- `rig users apply` no longer reports success having silently skipped the sudoers drop-in when `visudo` is off `PATH` (#139)
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
### Added
|
||||
|
||||
- Default Forgejo runner labels include opt-in `ubuntu-latest-full` for the GitHub-parity image (#144)
|
||||
|
||||
### Fixed
|
||||
|
||||
- `ci.yml` installs `shellcheck` when the runner image lacks it, so Forgejo's slim `ubuntu-latest` can run `check` (#144)
|
||||
- Plain `rig forgejo-runner install` warns when recorded labels are a retired rig default, without nagging custom `--labels` (#144)
|
||||
- CI's `check` job installs `shellcheck` where the image lacks it, so a runner on rig's defaults runs rig's own workflows (#144)
|
||||
|
||||
### Added
|
||||
|
||||
- `ubuntu-latest-full` in the default runner label map — GitHub's full tool surface, pulled only when a job asks for it (#144)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|||
. "$HERE/lib/templates.sh" # templates_resolve / template_parse_env / render_tenant_context
|
||||
# shellcheck source=SCRIPTDIR/lib/users-config.sh
|
||||
. "$HERE/lib/users-config.sh" # read_role_marker / root_door_of
|
||||
# shellcheck source=SCRIPTDIR/lib/admin-path.sh
|
||||
. "$HERE/lib/admin-path.sh" # require_admin_bins
|
||||
# shellcheck source=SCRIPTDIR/lib/sshd.sh
|
||||
. "$HERE/lib/sshd.sh" # harden_sshd (the staging-box tenant)
|
||||
# shellcheck source=SCRIPTDIR/lib/manifest.sh
|
||||
|
|
@ -202,15 +200,6 @@ else
|
|||
fi
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
# Root is not enough here either (#139). This mint adds the tenant user to the
|
||||
# docker group with `usermod` far below — AFTER installing docker and node,
|
||||
# which is what makes it the worst-placed of the four call sites: on a
|
||||
# PATH-shorn root it dies mid-convergence with a bare `usermod: command not
|
||||
# found`, having already changed the machine, rather than before touching it.
|
||||
#
|
||||
# Unconditional because the docker block below is unconditional — "every tenant
|
||||
# gets docker" is the stated rule there, so every tenant reaches the usermod.
|
||||
require_admin_bins usermod
|
||||
if [ -r /etc/os-release ]; then
|
||||
# Sourced in a subshell: os-release defines VERSION, NAME, ID, etc. —
|
||||
# sourcing it in the main shell silently clobbers same-named script vars.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ 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"
|
||||
# shellcheck source=SCRIPTDIR/lib/admin-path.sh
|
||||
. "$HERE/lib/admin-path.sh"
|
||||
|
||||
log() { printf 'rig-forgejo-runner: %s\n' "$*"; }
|
||||
warn() { printf 'rig-forgejo-runner: WARNING: %s\n' "$*" >&2; }
|
||||
|
|
@ -29,35 +27,64 @@ die() { printf 'rig-forgejo-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
|||
# 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.
|
||||
#
|
||||
# Both are `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
|
||||
# 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.
|
||||
#
|
||||
# WHY act-22.04 (slim) for ubuntu-latest, not full-22.04 — measured 2026-08-01
|
||||
# against ghcr manifests (#144):
|
||||
# act-22.04: ~0.55 GB compressed / ~2.2 GB on disk — no shellcheck
|
||||
# full-22.04: ~18.67 GB compressed / ~54.5 GB on disk — has shellcheck 0.8.0
|
||||
# A normal box-class ci tenant cannot hold 54.5 GB (typical free space ~34 GB).
|
||||
# So ubuntu-latest stays slim, and workflows must not assume GitHub-image tools
|
||||
# (rig's own ci.yml installs shellcheck when missing). Operators who need the
|
||||
# full tool surface opt in with runs-on: ubuntu-latest-full — that label is
|
||||
# inert until matched, so boxes that never ask pay nothing.
|
||||
# `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'
|
||||
|
||||
# labels_are_a_retired_default <recorded>
|
||||
# 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.
|
||||
#
|
||||
# True when <recorded> is a past DEFAULT_LABELS value rig has shipped — the
|
||||
# only plain-converge case that should warn about re-registration (#144).
|
||||
# Custom operator maps (drill's Leg 3, any --labels) must return false so a
|
||||
# bare re-run stays quiet. One pattern per past default; add when the string
|
||||
# changes. Extracted and driven by test/cli.sh — a grep pin alone cannot prove
|
||||
# the match is exact.
|
||||
labels_are_a_retired_default() {
|
||||
case "${1:-}" in
|
||||
'ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm') return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
# 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 <recorded> — 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 <asset-url> <file> <sumfile> <label>
|
||||
|
|
@ -120,10 +147,8 @@ usage: rig forgejo-runner install --instance <url> [options]
|
|||
time). Pin it for a deterministic, auditable install.
|
||||
--name <name> runner name (default: this host's hostname)
|
||||
--labels <csv> runner labels; replaces the default. The default maps
|
||||
ubuntu-latest (slim act image), ubuntu-latest-full
|
||||
(opt-in parity image), and docker onto containers so
|
||||
a workflow written for GitHub runs; full tools need
|
||||
runs-on: ubuntu-latest-full or an install step.
|
||||
ubuntu-latest and docker onto container images, so a
|
||||
workflow written for GitHub runs unchanged.
|
||||
--user <name> unprivileged service user (default: the tenant user
|
||||
`ci` when it exists, else forgejo-runner; created if
|
||||
absent; never root)
|
||||
|
|
@ -231,17 +256,6 @@ fi
|
|||
|
||||
# --- guards ----------------------------------------------------------------
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
# Root is not enough: the admin binaries must also be reachable (#139). This
|
||||
# sits beside the root check so identity and capability are asserted together,
|
||||
# and BEFORE any prompt or download — a token typed for a doomed run is waste.
|
||||
#
|
||||
# BOTH binaries this command goes on to call: useradd at the user-create below,
|
||||
# usermod at the docker-group add further down. Naming only the first would
|
||||
# still consume the token on a PATH that happened to resolve useradd but not
|
||||
# usermod — the same failure one step later, which is the shape #75 exists to
|
||||
# refuse (a sweep that covers most of its call sites is the hole the next bug
|
||||
# arrives through).
|
||||
require_admin_bins useradd usermod
|
||||
if [ -r /etc/os-release ]; then
|
||||
# Sourced in a subshell: os-release defines VERSION (e.g. "13 (trixie)"),
|
||||
# which would clobber this script's $VERSION.
|
||||
|
|
@ -448,29 +462,30 @@ install -d -m 0755 -o "$RUNNER_USER" -g "$RUNNER_GROUP" "$USER_HOME/.cache"
|
|||
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. Forgejo owns
|
||||
# labels from registration time; a re-run never rewrites them.
|
||||
#
|
||||
# Two warn paths (#144):
|
||||
# EXPLICIT --labels that differs → operator asked and it was not applied.
|
||||
# Plain converge whose recorded labels match a known *retired* default →
|
||||
# rig's default map moved (e.g. added ubuntu-latest-full). Without this
|
||||
# the operator re-runs install, sees "already registered", and believes
|
||||
# they have the new default while Forgejo still holds the old set.
|
||||
#
|
||||
# Do NOT warn on every RECORDED != current default: that fires forever for
|
||||
# any runner the operator deliberately gave custom --labels (drill's Leg 3
|
||||
# registers drill:docker://node:22-bookworm). The old LABELS_EXPLICIT-only
|
||||
# gate existed to avoid that noise; retired-default matching keeps the
|
||||
# silence for intentional maps and still catches silent drift off a past
|
||||
# rig default. Re-register only to pick up labels the old set never had —
|
||||
# nothing matching the recorded set is broken by the map change alone.
|
||||
# registered with — NOT whatever this invocation was passed. Say so when the
|
||||
# operator explicitly asked for different ones, rather than letting the
|
||||
# request evaporate.
|
||||
if [ -r "$RUNNER_DIR/.rig-labels" ]; then
|
||||
RECORDED="$(cat "$RUNNER_DIR/.rig-labels")"
|
||||
if [ "$LABELS_EXPLICIT" -eq 1 ] && [ "$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."
|
||||
elif [ "$LABELS_EXPLICIT" -eq 0 ] && labels_are_a_retired_default "$RECORDED"; then
|
||||
warn "this runner was registered with an older rig default label set. The current default adds ubuntu-latest-full (the GitHub-parity image). Labels are fixed at registration, so picking it up means re-registering: 'rig forgejo-runner remove' then install again. Nothing you run today is affected — re-register only if you want the new label."
|
||||
if [ "$LABELS_EXPLICIT" -eq 1 ]; then
|
||||
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
|
||||
elif labels_are_a_superseded_default "$RECORDED"; then
|
||||
# A plain converge on a runner registered before the defaults moved (the
|
||||
# #144 mapping change is the first). Silence here is what let a runner
|
||||
# keep serving a stale map while every re-run reported success.
|
||||
#
|
||||
# It warns on a SUPERSEDED DEFAULT rather than on "anything that is not
|
||||
# the current default", which would fire on every converge of a runner
|
||||
# the operator deliberately gave --labels — noise, not drift, and the
|
||||
# reason the old code only spoke when EXPLICIT. Matching known past
|
||||
# defaults exactly is what tells those two apart.
|
||||
#
|
||||
# Deliberately not an error and deliberately not alarming about CI: a
|
||||
# runner on the old default still runs jobs, and under #144's ruling it
|
||||
# still runs them GREEN. Re-registering buys the new LABELS, nothing else.
|
||||
warn "this runner registered with an older rig default label map, and Forgejo owns labels from registration time — it still has: ${RECORDED}. Nothing is broken: jobs keep running, and 'runs-on: ubuntu-latest' still works. Re-register only if you want the labels added since: 'rig forgejo-runner remove' then install again. Current default: ${DEFAULT_LABELS}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# admin-path.sh — assert the admin binaries are REACHABLE, not merely that we
|
||||
# are root.
|
||||
#
|
||||
# Being uid 0 and being able to find useradd are different facts, and rig
|
||||
# asserted only the first. `su` without `-`, sudo with a sanitised secure_path,
|
||||
# and several container images all hand you a root shell whose PATH carries no
|
||||
# /usr/sbin — which is where useradd, usermod and groupadd live on Debian. The
|
||||
# result was a bare `useradd: command not found` naming a line number inside a
|
||||
# versioned install root, emitted AFTER a registration token had been read off
|
||||
# the operator's terminal (#139).
|
||||
#
|
||||
# Which binaries this covers is measured, not assumed (Debian 13, 2026-08-01):
|
||||
#
|
||||
# useradd usermod groupadd userdel groupdel /usr/sbin package: passwd
|
||||
# visudo /usr/sbin package: sudo
|
||||
# gpasswd /usr/bin package: passwd
|
||||
#
|
||||
# Two consequences worth keeping written down. `gpasswd` is in the same PACKAGE
|
||||
# as useradd but a different DIRECTORY, so it is reachable on a PATH-shorn root
|
||||
# and does not belong in any of these preflights — do not add it for symmetry.
|
||||
# And `visudo` shares the directory but not the package, so its absence has a
|
||||
# second, innocent cause (sudo simply not installed) that the others do not, so
|
||||
# `rig users apply` checks it separately, after the point where that cause is
|
||||
# ruled out. See the comment there.
|
||||
#
|
||||
# (Spelled without the `.sh` on purpose: test/cli.sh pins that exactly one file
|
||||
# under commands/ names that script, to catch a second caller appearing. A
|
||||
# comment is not a caller, but the pin is deliberately blunt and cheap.)
|
||||
#
|
||||
# It REFUSES rather than repairing PATH itself. A command that quietly prepends
|
||||
# /usr/sbin teaches the operator nothing and leaves a misconfigured host
|
||||
# misconfigured; the same reason bootstrap refuses rather than guessing. The
|
||||
# message carries the fix so the refusal costs one paste, not an investigation.
|
||||
|
||||
# require_admin_bins <bin>... — die unless every one resolves on PATH.
|
||||
require_admin_bins() {
|
||||
local missing=() b
|
||||
for b in "$@"; do
|
||||
command -v "$b" >/dev/null 2>&1 || missing+=("$b")
|
||||
done
|
||||
[ "${#missing[@]}" -eq 0 ] && return 0
|
||||
# Names the REMEDY, not this script: the operator typed a `rig ...` command,
|
||||
# and echoing the internal path back at them is the unhelpful half of the
|
||||
# original `useradd: command not found`.
|
||||
die "cannot find ${missing[*]} on PATH — it lives in /usr/sbin, which this root shell does not carry (a 'su' without '-' does this, and so do some container images). Re-run the same rig command with: PATH=/usr/sbin:/sbin:\$PATH"
|
||||
}
|
||||
|
|
@ -9,8 +9,6 @@ set -euo pipefail
|
|||
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||
# shellcheck source=SCRIPTDIR/lib/runner-config.sh
|
||||
. "$HERE/lib/runner-config.sh"
|
||||
# shellcheck source=SCRIPTDIR/lib/admin-path.sh
|
||||
. "$HERE/lib/admin-path.sh"
|
||||
|
||||
log() { printf 'rig-runner: %s\n' "$*"; }
|
||||
warn() { printf 'rig-runner: WARNING: %s\n' "$*" >&2; }
|
||||
|
|
@ -87,10 +85,6 @@ VERSION="${VERSION#v}"
|
|||
|
||||
# --- guards ----------------------------------------------------------------
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
# Root is not enough: the admin binaries must also be reachable (#139). This
|
||||
# sits beside the root check so identity and capability are asserted together,
|
||||
# and BEFORE any prompt or download — a token typed for a doomed run is waste.
|
||||
require_admin_bins useradd
|
||||
if [ -r /etc/os-release ]; then
|
||||
# Sourced in a subshell: os-release defines VERSION (e.g. "13 (trixie)"),
|
||||
# which would clobber this script's $VERSION.
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ set -euo pipefail
|
|||
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||
# shellcheck source=SCRIPTDIR/lib/users-config.sh
|
||||
. "$HERE/lib/users-config.sh"
|
||||
# shellcheck source=SCRIPTDIR/lib/admin-path.sh
|
||||
. "$HERE/lib/admin-path.sh"
|
||||
|
||||
log() { printf 'rig-users: %s\n' "$*"; }
|
||||
warn() { printf 'rig-users: WARNING: %s\n' "$*" >&2; }
|
||||
|
|
@ -138,10 +136,6 @@ done <<< "$PARSED"
|
|||
|
||||
# --- guards ------------------------------------------------------------------
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
# Root is not enough: the admin binaries must also be reachable (#139). This
|
||||
# sits beside the root check so identity and capability are asserted together,
|
||||
# and BEFORE any prompt or download — a token typed for a doomed run is waste.
|
||||
require_admin_bins useradd usermod groupadd
|
||||
|
||||
# Identity management gates its INVOKER, not just its uid: %rig's sudoers rule
|
||||
# is binary-scoped but not argument-scoped, so without this gate a rig-role
|
||||
|
|
@ -201,26 +195,6 @@ if [ "$NEED_SUDO" -eq 1 ] && ! command -v sudo >/dev/null 2>&1; then
|
|||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sudo
|
||||
CHANGED=1
|
||||
fi
|
||||
# visudo is checked HERE and not beside the root check, because until the block
|
||||
# above has run there is a legitimate reason for it to be absent: sudo is not
|
||||
# installed yet, and apply is what installs it. Above, a missing visudo would
|
||||
# be indistinguishable from that, so the refusal would fire on a healthy box.
|
||||
#
|
||||
# Below, it is unambiguous. sudo is present, so `visudo` missing means only one
|
||||
# thing: /usr/sbin is off PATH. And it MUST refuse here rather than be left to
|
||||
# the sudoers block further down, because that block asks `command -v visudo`
|
||||
# and treats false as "no sudo on the box means no role needed it" — which on a
|
||||
# PATH-shorn root is FALSE TWICE. A role does need it, sudo is installed, and
|
||||
# apply would finish reporting success having silently never written the
|
||||
# sudoers drop-in: the users get their roles and not the escalation the roles
|
||||
# are FOR. That is the failure this whole issue is about (a wrong effective
|
||||
# state reported as success, #12), in its quietest form — the other three sites
|
||||
# at least crash. Refusing before the first mutation is what keeps it loud.
|
||||
#
|
||||
# It sits before `groupadd` below, so nothing has been converged when it fires.
|
||||
if [ "$NEED_SUDO" -eq 1 ]; then
|
||||
require_admin_bins visudo
|
||||
fi
|
||||
|
||||
# --- groups ------------------------------------------------------------------
|
||||
groupadd -f rig-admin
|
||||
|
|
|
|||
257
test/cli.sh
257
test/cli.sh
|
|
@ -3334,121 +3334,6 @@ check "ci-box: its install.sh does not register" 1 "" \
|
|||
check "ci-box: bootstrap-tenant does not read the staging dir" 1 "" \
|
||||
grep -q 'docs/templates' "$ROOT/commands/bootstrap-tenant.sh"
|
||||
|
||||
# --- admin binaries must be reachable, not just root (#139) ------------------
|
||||
# Being root and being able to FIND the admin binaries are different facts, and
|
||||
# rig asserted only the first. `su` without `-`, sudo with a sanitised
|
||||
# secure_path, and several container images all give a root shell whose PATH
|
||||
# carries no /usr/sbin — where useradd lives. Reported from a real ci-box:
|
||||
#
|
||||
# root@ci-forgejo-box:/home/dev# rig forgejo-runner install --instance …
|
||||
# forgejo runner registration token:
|
||||
# …/forgejo-runner-install.sh: line 250: useradd: command not found
|
||||
#
|
||||
# Note where it died: AFTER reading a registration token off the operator's
|
||||
# terminal. A secret typed for a run that could never succeed is the avoidable
|
||||
# half of the bug, so the refusal has to come before the prompt.
|
||||
#
|
||||
# The root check fires first and correctly, so these stub `id -u` to 0 — the
|
||||
# idiom the bootstrap --undo block above already uses — to reach the preflight.
|
||||
ADMPATH_DIR="$(mktemp -d)"
|
||||
mkdir -p "$ADMPATH_DIR/bin"
|
||||
# shellcheck disable=SC2016 # the body is shell source being written, not expanded
|
||||
printf '#!/usr/bin/env bash\nif [ "${1:-}" = -u ]; then printf "0\\n"; else exec /usr/bin/id "$@"; fi\n' \
|
||||
> "$ADMPATH_DIR/bin/id"
|
||||
chmod +x "$ADMPATH_DIR/bin/id"
|
||||
# A PATH with the stub and the ordinary bindirs, but deliberately no /usr/sbin.
|
||||
SBINLESS="$ADMPATH_DIR/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
adm_run() { env PATH="$SBINLESS" "$@" 2>&1; }
|
||||
adm_prompted() { # did it read a token before refusing?
|
||||
adm_run "$@" | grep -qi 'registration token:'
|
||||
}
|
||||
check "preflight: forgejo-runner install refuses a PATH with no /usr/sbin" 1 "useradd" \
|
||||
adm_run "$ROOT/commands/forgejo-runner-install.sh" --instance https://f.example.com
|
||||
check "preflight: …and names PATH as the cause, not just the missing binary" 1 "PATH" \
|
||||
adm_run "$ROOT/commands/forgejo-runner-install.sh" --instance https://f.example.com
|
||||
check "preflight: …and refuses BEFORE prompting for a token" 1 "" \
|
||||
adm_prompted "$ROOT/commands/forgejo-runner-install.sh" --instance https://f.example.com
|
||||
check "preflight: the GitHub runner installer refuses too" 1 "useradd" \
|
||||
adm_run "$ROOT/commands/runner-install.sh" --repo o/r
|
||||
check "preflight: users apply refuses before it converges anything" 1 "useradd" \
|
||||
adm_run "$ROOT/commands/users-apply.sh" --file /dev/null
|
||||
|
||||
# A sbin-less PATH loses ALL of /usr/sbin at once, so the checks above can only
|
||||
# ever prove the FIRST binary is named — `useradd` wins every race and would
|
||||
# hide a preflight that forgot the others. These fixtures resolve the earlier
|
||||
# binaries and withhold exactly one, which is the only way to show the sweep
|
||||
# covers what each command actually calls (the #75 lesson: a sweep that misses
|
||||
# one call site is how the next bug gets in).
|
||||
#
|
||||
# The withheld binary is real: these are PATHs, not stubs of the tools
|
||||
# themselves — a stub that silently succeeded would let convergence run on.
|
||||
admstub() { # admstub <dir> <bin>... — a bindir resolving id(0) plus <bin>...
|
||||
local d="$1"; shift
|
||||
mkdir -p "$d"
|
||||
# shellcheck disable=SC2016 # shell source being written, not expanded
|
||||
printf '#!/usr/bin/env bash\nif [ "${1:-}" = -u ]; then printf "0\\n"; else exec /usr/bin/id "$@"; fi\n' > "$d/id"
|
||||
chmod +x "$d/id"
|
||||
local b
|
||||
for b in "$@"; do printf '#!/usr/bin/env bash\nexit 0\n' > "$d/$b"; chmod +x "$d/$b"; done
|
||||
}
|
||||
adm_with() { # adm_with <bindir> <cmd>...
|
||||
local d="$1"; shift
|
||||
env PATH="$d:/usr/local/bin:/usr/bin:/bin" "$@" 2>&1
|
||||
}
|
||||
# adm_saw <bindir> <pattern> <cmd>... — exit 0 if the run printed <pattern>.
|
||||
# Used with `check ... 1 ""` to assert a thing did NOT happen (a token prompt,
|
||||
# a group creation), the same shape as adm_prompted above.
|
||||
adm_saw() {
|
||||
local d="$1" pat="$2"; shift 2
|
||||
adm_with "$d" "$@" | grep -qi -- "$pat"
|
||||
}
|
||||
|
||||
# useradd resolves, usermod does not — the forgejo installer calls both, and
|
||||
# only reaches usermod after the token has been spent.
|
||||
admstub "$ADMPATH_DIR/no-usermod" useradd
|
||||
check "preflight: forgejo-runner install names usermod when only useradd resolves" 1 "usermod" \
|
||||
adm_with "$ADMPATH_DIR/no-usermod" "$ROOT/commands/forgejo-runner-install.sh" --instance https://f.example.com
|
||||
check "preflight: …and still refuses before the token prompt" 1 "" \
|
||||
adm_saw "$ADMPATH_DIR/no-usermod" 'registration token:' \
|
||||
"$ROOT/commands/forgejo-runner-install.sh" --instance https://f.example.com
|
||||
# users apply calls groupadd unconditionally, two lines into its convergence.
|
||||
admstub "$ADMPATH_DIR/no-groupadd" useradd usermod
|
||||
check "preflight: users apply names groupadd when useradd and usermod resolve" 1 "groupadd" \
|
||||
adm_with "$ADMPATH_DIR/no-groupadd" "$ROOT/commands/users-apply.sh" --file /dev/null
|
||||
|
||||
# visudo is the quiet one. With sudo INSTALLED and /usr/sbin off PATH, the
|
||||
# sudoers block reads `command -v visudo` as "no sudo on the box" and skips the
|
||||
# drop-in — apply then reports success having granted roles without the
|
||||
# escalation those roles exist for. So this fixture needs a role that wants
|
||||
# sudo, and asserts the refusal by name rather than a silent success.
|
||||
ADM_USERS="$ADMPATH_DIR/users"
|
||||
printf '%s\n' 'dan admin ssh-ed25519 AAAAC3fixture dan@laptop' > "$ADM_USERS"
|
||||
admstub "$ADMPATH_DIR/no-visudo" useradd usermod groupadd
|
||||
check "preflight: users apply refuses a sudo-needing role when visudo is unreachable" 1 "visudo" \
|
||||
adm_with "$ADMPATH_DIR/no-visudo" "$ROOT/commands/users-apply.sh" --file "$ADM_USERS" --yes
|
||||
# …and the refusal must land BEFORE the groups are converged, or the machine is
|
||||
# already half-changed when the operator reads it.
|
||||
check "preflight: …before any group is created" 1 "" \
|
||||
adm_saw "$ADMPATH_DIR/no-visudo" 'rig-admin' \
|
||||
"$ROOT/commands/users-apply.sh" --file "$ADM_USERS" --yes
|
||||
# A users file needing no sudo must NOT be refused for a missing visudo: the
|
||||
# guard has to be as narrow as the need, or it refuses healthy boxes.
|
||||
printf '%s\n' 'maria ops ssh-ed25519 AAAAC3fixture maria@mac' > "$ADMPATH_DIR/users-nosudo"
|
||||
check "preflight: …but a file with no sudo-backed role is not refused for visudo" 1 "" \
|
||||
adm_saw "$ADMPATH_DIR/no-visudo" 'visudo' \
|
||||
"$ROOT/commands/users-apply.sh" --file "$ADMPATH_DIR/users-nosudo" --yes
|
||||
|
||||
# The fourth call site (#139 review): bootstrap-tenant adds the tenant user to
|
||||
# the docker group AFTER installing docker and node, so an unguarded PATH fails
|
||||
# it mid-convergence on a machine it has already changed.
|
||||
# staging-box because it is the one tenant role defined in rig's own tree: the
|
||||
# others resolve through the template registry, and a preflight test must not
|
||||
# depend on the network to reach the guard it is testing.
|
||||
admstub "$ADMPATH_DIR/no-any"
|
||||
check "preflight: bootstrap-tenant refuses a sbin-less PATH before it converges" 1 "usermod" \
|
||||
adm_with "$ADMPATH_DIR/no-any" "$ROOT/commands/bootstrap-tenant.sh" staging-box
|
||||
rm -rf "$ADMPATH_DIR"
|
||||
|
||||
# --- rig forgejo-runner (#109) ----------------------------------------------
|
||||
FR="$ROOT/commands/forgejo-runner-install.sh"
|
||||
check "forgejo-runner: bare subcommand shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" forgejo-runner
|
||||
|
|
@ -3464,22 +3349,25 @@ check "forgejo-runner: --version refuses a path, not a release number" 2 "releas
|
|||
"$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 a gate AFTER --version parsing is the proof a good pin got THROUGH
|
||||
# validation. Which gate depends on the uid: non-root hits "must run as root";
|
||||
# act/Forgejo jobs run as uid 0 (no `runner` account — #144), so they sail past
|
||||
# the root check and hit the unattended-token refuse instead. Both prove the
|
||||
# same thing. GitHub-hosted ubuntu-latest is non-root and takes the first arm.
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
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
|
||||
else
|
||||
check "forgejo-runner: a plain release number passes validation" 1 "FORGEJO_RUNNER_TOKEN is unset" \
|
||||
env -u FORGEJO_RUNNER_TOKEN "$FR" --instance https://f.example.com --version 12.13.2
|
||||
check "forgejo-runner: a leading v is stripped before that check" 1 "FORGEJO_RUNNER_TOKEN is unset" \
|
||||
env -u FORGEJO_RUNNER_TOKEN "$FR" --instance https://f.example.com --version v12.13.2
|
||||
fi
|
||||
# Reaching the gate BELOW validation is the proof a good pin got THROUGH it.
|
||||
# Which gate that is depends on who runs the suite, and both readings are
|
||||
# equally good evidence: a normal user hits "must run as root", while root
|
||||
# itself gets further, to the token prompt. Asserting only the first made these
|
||||
# two checks depend on the account the suite happened to run under — #136's
|
||||
# class again, surfaced by the Forgejo runner, which runs job containers as
|
||||
# uid 0 where GitHub's runner is the unprivileged `runner` user. Either way the
|
||||
# exit is 1 and validation is behind us, which is the whole claim (#144).
|
||||
past_version_validation() {
|
||||
local out rc
|
||||
out="$("$FR" --instance https://f.example.com --version "$1" 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 1 ] || { printf 'exit %s, wanted 1\n' "$rc"; return 1; }
|
||||
printf '%s' "$out" | grep -qE 'must run as root|FORGEJO_RUNNER_TOKEN is unset' \
|
||||
|| { printf 'neither gate below validation was reached:\n%s\n' "$out"; return 1; }
|
||||
}
|
||||
check "forgejo-runner: a plain release number passes validation" 0 "" \
|
||||
past_version_validation 12.13.2
|
||||
check "forgejo-runner: a leading v is stripped before that check" 0 "" \
|
||||
past_version_validation 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.
|
||||
|
|
@ -3538,6 +3426,75 @@ check "forgejo-runner: …and the install creates it, owned by the runner user"
|
|||
check "forgejo-runner: ProtectHome stays read-only (the cache is not an excuse to widen it)" 0 "" \
|
||||
grep -qF 'ProtectHome=read-only' "$FRI"
|
||||
|
||||
# #144: rig's own `ci / check` failed 9 times on `shellcheck: command not found`
|
||||
# because `ubuntu-latest` maps to the SLIM act image. The ruling kept the slim
|
||||
# image and made workflows equip themselves, so what has to stay pinned is the
|
||||
# mapping AND the reason — the next reader who sees "ubuntu-latest → slim" with
|
||||
# no explanation re-opens the same question and re-measures the same images.
|
||||
check "forgejo-runner: ubuntu-latest maps to the slim act image" 0 "" \
|
||||
grep -qF 'ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04' "$FRI"
|
||||
# The escape hatch must ship BY DEFAULT or it cannot be had at all: Forgejo
|
||||
# freezes labels at registration, so a label absent then needs a re-register.
|
||||
check "forgejo-runner: …and ubuntu-latest-full ships beside it as the escape hatch" 0 "" \
|
||||
grep -qF 'ubuntu-latest-full:docker://ghcr.io/catthehacker/ubuntu:full-22.04' "$FRI"
|
||||
# The measured cost is the whole argument for the slim default. Pinned so a
|
||||
# tidy-up cannot delete the evidence and leave the choice looking arbitrary.
|
||||
check "forgejo-runner: the extracted-size measurement is recorded beside the map" 0 "" \
|
||||
grep -qF '54.52 GB' "$FRI"
|
||||
check "forgejo-runner: …including that runner-22.04 is not a cheaper middle" 0 "" \
|
||||
grep -qF 'runner-22.04' "$FRI"
|
||||
# The rule a consumer needs, in the file that sets the expectation.
|
||||
check "forgejo-runner: the map states workflows must not assume image tools" 0 "" \
|
||||
grep -qF 'must not assume tools from the image' "$FRI"
|
||||
# The stale-default warning fires on a SUPERSEDED default, never on labels the
|
||||
# operator chose — that distinction is the whole reason the old code stayed
|
||||
# silent on plain converges, and losing it turns the warning into noise.
|
||||
check "forgejo-runner: past defaults are remembered so drift can be recognised" 0 "" \
|
||||
grep -qF 'SUPERSEDED_DEFAULTS=(' "$FRI"
|
||||
check "forgejo-runner: the pre-#144 default is one of them" 0 "" \
|
||||
grep -qF "'ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm'" "$FRI"
|
||||
# It must not read as "your CI is broken": under #144's ruling a runner on the
|
||||
# old map still runs jobs, and still runs them green.
|
||||
check "forgejo-runner: the stale-default warning says nothing is broken" 0 "" \
|
||||
grep -qF 'Nothing is broken' "$FRI"
|
||||
|
||||
# …and the recogniser is DRIVEN, not grepped — a list of past defaults is only
|
||||
# worth having if the match is exact. Extraction pattern is test/drill.sh's:
|
||||
# the shipped bytes are awk-extracted, with a guard on the boundary.
|
||||
SD_FNS="$(mktemp -d)/superseded.sh"
|
||||
awk '/^SUPERSEDED_DEFAULTS=\(/,/^\)/' "$FRI" > "$SD_FNS"
|
||||
awk '/^labels_are_a_superseded_default\(\) \{/,/^\}/' "$FRI" >> "$SD_FNS"
|
||||
check "extraction guards the awk: SUPERSEDED_DEFAULTS landed" 0 "SUPERSEDED_DEFAULTS=(" \
|
||||
grep -F 'SUPERSEDED_DEFAULTS=(' "$SD_FNS"
|
||||
check "extraction guards the awk: the recogniser landed" 0 "labels_are_a_superseded_default() {" \
|
||||
grep -F 'labels_are_a_superseded_default() {' "$SD_FNS"
|
||||
# shellcheck source=/dev/null
|
||||
. "$SD_FNS"
|
||||
PRE_144='ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm'
|
||||
CURRENT='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'
|
||||
check "superseded: a runner on the pre-#144 default is recognised" 0 "" \
|
||||
labels_are_a_superseded_default "$PRE_144"
|
||||
# The current default is not drift — warning on it would fire on every converge
|
||||
# of every up-to-date runner.
|
||||
check "superseded: the CURRENT default is not drift" 1 "" \
|
||||
labels_are_a_superseded_default "$CURRENT"
|
||||
# The whole reason this is a list of exact strings rather than "≠ the default":
|
||||
# a map the operator chose must stay silent forever.
|
||||
check "superseded: an operator's own --labels map is never called drift" 1 "" \
|
||||
labels_are_a_superseded_default 'drill:docker://node:22-bookworm'
|
||||
check "superseded: a near-miss of a past default is not a match either" 1 "" \
|
||||
labels_are_a_superseded_default "${PRE_144} "
|
||||
|
||||
# rig's own ci.yml is the worked example of that rule: it installs shellcheck
|
||||
# only when the image lacks it. Both halves are pinned — drop the `command -v`
|
||||
# and every GitHub run pays an apt round-trip; drop the sudo and the GitHub
|
||||
# path breaks the day that image stops preinstalling the tool.
|
||||
CIYML="$ROOT/.github/workflows/ci.yml"
|
||||
check "ci.yml: check installs shellcheck when the image does not ship it" 0 "" \
|
||||
grep -qF 'sudo apt-get install -y shellcheck' "$CIYML"
|
||||
check "ci.yml: …only when absent, so GitHub pays nothing for it" 0 "" \
|
||||
grep -qF 'command -v shellcheck >/dev/null ||' "$CIYML"
|
||||
|
||||
check "forgejo-runner: status --help exits 0" 0 "usage:" "$ROOT/commands/forgejo-runner-status.sh" --help
|
||||
check "forgejo-runner: remove --help exits 0" 0 "usage:" "$ROOT/commands/forgejo-runner-remove.sh" --help
|
||||
|
||||
|
|
@ -3829,44 +3786,6 @@ check "forgejo-runner: an explicit --labels on a rerun warns it 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"
|
||||
# #144 option B: plain converge warns only for known *retired* defaults — not
|
||||
# for every RECORDED != current default (that would noise custom --labels
|
||||
# forever, including drill's Leg 3). Drive the recogniser against fixtures
|
||||
# (test/drill.sh extraction pattern) so a dead matcher cannot greppen green.
|
||||
check "forgejo-runner: plain converge warns on a known retired default label set" 0 "registered with an older rig default label set" \
|
||||
grep -o "registered with an older rig default label set" "$FR"
|
||||
check "forgejo-runner: that warn says re-register only if you want the new label" 0 "re-register only if you want the new label" \
|
||||
grep -o "re-register only if you want the new label" "$FR"
|
||||
PRE_144_LABELS='ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm'
|
||||
CURRENT_LABELS="$(sed -n "s/^DEFAULT_LABELS='\\(.*\\)'$/\\1/p" "$FR")"
|
||||
RETIRED_FNS="$(mktemp)"
|
||||
awk '/^labels_are_a_retired_default\(\) \{/,/^\}/' "$FR" > "$RETIRED_FNS"
|
||||
check "extraction guards the awk: labels_are_a_retired_default() landed" 0 "labels_are_a_retired_default() {" \
|
||||
grep -F 'labels_are_a_retired_default() {' "$RETIRED_FNS"
|
||||
# shellcheck source=/dev/null
|
||||
. "$RETIRED_FNS"
|
||||
check "retired-default: the pre-#144 default is recognised" 0 "" \
|
||||
labels_are_a_retired_default "$PRE_144_LABELS"
|
||||
check "retired-default: the CURRENT default is not drift" 1 "" \
|
||||
labels_are_a_retired_default "$CURRENT_LABELS"
|
||||
check "retired-default: an operator's own --labels map is never drift" 1 "" \
|
||||
labels_are_a_retired_default 'drill:docker://node:22-bookworm'
|
||||
check "retired-default: a near-miss of a past default is not a match" 1 "" \
|
||||
labels_are_a_retired_default "${PRE_144_LABELS} "
|
||||
rm -f "$RETIRED_FNS"
|
||||
# Default map: slim ubuntu-latest (act), opt-in full, docker — pin the three
|
||||
# so a silent drop of the full rider or a flip back to full-as-default fails.
|
||||
check "forgejo-runner: DEFAULT_LABELS maps ubuntu-latest to act-22.04 (slim)" 0 "ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04" \
|
||||
grep -o "ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04" "$FR"
|
||||
check "forgejo-runner: DEFAULT_LABELS offers ubuntu-latest-full as opt-in" 0 "ubuntu-latest-full:docker://ghcr.io/catthehacker/ubuntu:full-22.04" \
|
||||
grep -o "ubuntu-latest-full:docker://ghcr.io/catthehacker/ubuntu:full-22.04" "$FR"
|
||||
check "forgejo-runner: DEFAULT_LABELS comment records the slim/full size measurement" 0 "54.5 GB" \
|
||||
grep -o "54.5 GB" "$FR"
|
||||
# ci.yml must install shellcheck when the image lacks it (#144 option B).
|
||||
check "ci.yml: shellcheck step installs the tool when missing" 0 "command -v shellcheck" \
|
||||
grep -o "command -v shellcheck" "$ROOT/.github/workflows/ci.yml"
|
||||
check "ci.yml: shellcheck install uses sudo (GitHub path; no-op on act as root)" 0 "sudo apt-get install -y shellcheck" \
|
||||
grep -o "sudo apt-get install -y shellcheck" "$ROOT/.github/workflows/ci.yml"
|
||||
# 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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue