CI / Shell-Lint (bash -n, source-check, Validierungs-Tests) (pull_request) Successful in 2s
actions/cache -- and actions/setup-go, which builds on it -- packs its archive with zstdmt when zstd is present and falls back to single-threaded gzip when it is not. The archive name says which happened: cache.tzst against cache.tgz. Measured in l.kirchner/patchmgr, CI run 1957. The cache is GOMODCACHE plus GOCACHE and runs to 2-5 GB. runner-gpu has zstd and writes cache.tzst; runner-01, -02 and -03 write cache.tgz, and go test -race (stable) spent 605 seconds packing it for a job with 41 seconds of work. Across all eight jobs of that run, 2312 of 3146 seconds went into this step. This does not settle whether the cache is wanted at all -- in host mode with a persistent home both directories survive between jobs anyway, and patchmgr is switching it off for that reason. But as long as any repository on the instance uses it, it should not be compressed on one core. zstd is a package for the post step, not for jobs, so it sits with the others rather than in a workflow: this runner installs nothing at job time, by design.
241 lines
11 KiB
Bash
241 lines
11 KiB
Bash
#!/usr/bin/env bash
|
||
# Runner installer — runs inside the LXC, called by ct/runner.sh
|
||
#
|
||
# Allgemeiner, INSTANZWEITER Gitea-Actions-Runner ohne Deploy-Rechte:
|
||
# - Node.js (für actions/checkout im Host-Mode) + git + rsync
|
||
# - Docker (für Wegwerf-Test-Container in CI, z. B. pgvector)
|
||
# - act_runner als unprivilegierter User "runner" in der docker-Gruppe
|
||
# - systemd-Unit act-runner.service (Requires=docker.service)
|
||
#
|
||
# SICHERHEITSMODELL (Review-Finding 88, bewusst akzeptiert & dokumentiert):
|
||
# docker-Gruppe == de-facto root IN DIESEM LXC. Da der Runner instanzweit
|
||
# ist, kann jedes Repo der Gitea-Instanz via Workflow den Runner-Container
|
||
# vollständig kontrollieren. Das ist hier akzeptiert, weil (a) alle Repos
|
||
# der Instanz vom selben Admin (Lutz) stammen — kein Multi-Tenant — und
|
||
# (b) dieser LXC GENAU DESHALB nichts besitzt: keine sudoers, keine
|
||
# Produktions-Mounts, keine Secrets außer dem (geshredderten) Reg-Token.
|
||
# Bei Öffnung der Instanz für Dritte: Docker rootless oder eigener Runner
|
||
# pro Vertrauenszone. NICHT auf LXCs mit Deploy-Rechten übertragen.
|
||
#
|
||
# Idempotent: erneuter Lauf (auch OHNE /root/runner.deploy.env) überspringt
|
||
# Vorhandenes und provisioniert nur nach — Registrierung braucht die env-Datei
|
||
# nur beim Erstlauf (Finding 90).
|
||
|
||
set -euo pipefail
|
||
|
||
LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}"
|
||
source <(curl -fsSL "$LIB_URL/install.func")
|
||
|
||
[[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; }
|
||
|
||
APP_USER="runner"
|
||
APP_HOME="/opt/runner"
|
||
RUNNER_DIR="$APP_HOME/data"
|
||
|
||
# ── Konfiguration laden (optional bei Re-Run, Finding 90) ─────────────────────
|
||
CONF="/root/runner.deploy.env"
|
||
if [[ -f "$CONF" ]]; then
|
||
set -a; . "$CONF"; set +a
|
||
fi
|
||
RUNNER_NAME="${RUNNER_NAME:-$(hostname)}"
|
||
RUNNER_LABELS="${RUNNER_LABELS:-homelab:host}"
|
||
RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}"
|
||
NODE_MAJOR="${NODE_MAJOR:-22}"
|
||
|
||
# ── Validierung (Finding 80): Werte landen in Shell-Fragmenten/systemd —
|
||
# strikte Zeichenklassen statt Vertrauen. Re-Runs ohne CONF validieren
|
||
# nur, was gesetzt ist; Registrierungs-Pflichtwerte prüft der Reg-Block. ──
|
||
valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$ ]]; }
|
||
valid_token() { [[ "$1" =~ ^[A-Za-z0-9_-]{16,128}$ ]]; }
|
||
valid_token_word() { [[ "$1" =~ ^[A-Za-z0-9._:,-]+$ ]]; }
|
||
|
||
[[ -z "${GITEA_INSTANCE_URL:-}" ]] || valid_url "$GITEA_INSTANCE_URL" \
|
||
|| { msg_err "GITEA_INSTANCE_URL ungültig: nur http(s)://host[:port]"; exit 1; }
|
||
[[ -z "${RUNNER_TOKEN:-}" ]] || valid_token "$RUNNER_TOKEN" \
|
||
|| { msg_err "RUNNER_TOKEN ungültig (erwartet 16–128 Zeichen [A-Za-z0-9_-])"; exit 1; }
|
||
valid_token_word "$RUNNER_NAME" || { msg_err "RUNNER_NAME enthält unzulässige Zeichen"; exit 1; }
|
||
valid_token_word "$RUNNER_LABELS" || { msg_err "RUNNER_LABELS enthält unzulässige Zeichen"; exit 1; }
|
||
[[ "$RUNNER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { msg_err "RUNNER_VERSION ungültig"; exit 1; }
|
||
[[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || { msg_err "NODE_MAJOR ungültig"; exit 1; }
|
||
|
||
# Finding 85 (Re-Review): act_runner schreibt .runner ins CWD — run_user wechselt
|
||
# deshalb hart ins RUNNER_DIR (env -C), sonst landet die Registrierung in / und
|
||
# der unprivilegierte User darf dort nicht schreiben.
|
||
run_user() { runuser -u "$APP_USER" -- env -C "$RUNNER_DIR" HOME="$APP_HOME" "$@"; }
|
||
|
||
# ── Pakete: git/rsync, Node (checkout-Action), Docker (Test-Container),
|
||
# Build-Werkzeuge (Compiler-Sprachen in CI) ─────────────────────────────────
|
||
#
|
||
# make/gcc/protobuf: Der Runner faehrt Host-Mode, also gibt es kein Image, das
|
||
# Werkzeuge mitbringt — was hier nicht liegt, hat kein Job. Konkret gemessen an
|
||
# l.kirchner/patchmgr (Go):
|
||
# make "make: command not found" in jedem Gate-Job
|
||
# gcc ohne gefundenen C-Compiler bleibt cgo aus (go env
|
||
# meldet dann CGO_ENABLED=0), und "go test -race" ist
|
||
# nicht baubar
|
||
# protobuf-compiler protoc fuer die Codegenerierung
|
||
# libprotobuf-dev liefert /usr/include/google/protobuf/*.proto; ohne die
|
||
# Includes scheitert protoc trotz vorhandenem Binary
|
||
#
|
||
# ACHTUNG protoc: Das bindet jedes Repo der Instanz an die protoc-Fassung der
|
||
# Distribution (Debian 12: 3.21.x). Anders als make/gcc ist protoc ein
|
||
# Codegenerator — ein Distro-Upgrade aendert erzeugten Code fuer alle Nutzer
|
||
# gleichzeitig. Wer eine eigene Fassung braucht, pinnt sie im Projekt
|
||
# (Release-Tarball, buf, oder Docker — der Runner hat Docker) statt sie hier
|
||
# zu heben.
|
||
#
|
||
# zstd: kein Werkzeug fuer Jobs, sondern fuer den Nachlauf. `actions/cache`
|
||
# — und damit auch `actions/setup-go`, das darauf aufsetzt — packt seinen
|
||
# Cache mit `zstdmt`, wenn zstd vorhanden ist, und faellt sonst auf
|
||
# einkerniges gzip zurueck. Der Unterschied ist am Archivnamen zu sehen:
|
||
# `cache.tzst` gegen `cache.tgz`.
|
||
#
|
||
# Gemessen am 02./03.09.2026 in l.kirchner/patchmgr, CI-Lauf 1957: Der Cache
|
||
# aus GOMODCACHE und GOCACHE ist dort 2 bis 5 GB gross. Auf runner-gpu (hat
|
||
# zstd) heisst er `cache.tzst`; auf runner-01/02/03 `cache.tgz`, und
|
||
# `go test -race (stable)` verbrachte damit **605 Sekunden** im Nachlauf bei
|
||
# 41 Sekunden Arbeit. Ueber alle acht Jobs waren es 2312 von 3146 Sekunden.
|
||
#
|
||
# Das ersetzt nicht die Frage, ob dieser Cache ueberhaupt gebraucht wird — im
|
||
# Host-Mode mit dauerhaftem Zuhause ueberleben beide Verzeichnisse ohnehin
|
||
# zwischen den Jobs, und patchmgr schaltet ihn deshalb ab. Aber solange
|
||
# irgendein Repo der Instanz ihn nutzt, soll er nicht einkernig komprimiert
|
||
# werden. Belege: .specs/reports/ci-laufzeit-und-cache-2026-09-02.md dort.
|
||
#
|
||
# Bewusst NICHT installiert: sudo. Ein Workflow soll auf diesem Runner nichts
|
||
# nachinstallieren koennen — was gebraucht wird, steht hier.
|
||
#
|
||
# Go selbst gehoert nicht hierher: Projekte holen es ueber actions/setup-go,
|
||
# weil CI-Matrizen mehrere Fassungen fahren.
|
||
setup_base_apt git rsync ca-certificates curl \
|
||
make gcc protobuf-compiler libprotobuf-dev zstd
|
||
|
||
NODE_HAVE="$(command -v node >/dev/null 2>&1 && node -v | sed -E 's/^v([0-9]+).*/\1/' || echo 0)"
|
||
if [[ "$NODE_HAVE" != "$NODE_MAJOR" ]]; then
|
||
msg_info "Installing Node.js ${NODE_MAJOR}.x (NodeSource)..."
|
||
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/dev/null
|
||
apt-get install -y -qq nodejs >/dev/null
|
||
msg_ok "Node $(node -v) installed"
|
||
else
|
||
msg_warn "Node $(node -v) already present, skipping"
|
||
fi
|
||
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
msg_info "Installing Docker (get.docker.com)..."
|
||
curl -fsSL https://get.docker.com | sh >/dev/null
|
||
msg_ok "Docker $(docker --version | awk '{print $3}' | tr -d ',')"
|
||
else
|
||
msg_warn "Docker already present, skipping"
|
||
fi
|
||
|
||
# ── act_runner binary ─────────────────────────────────────────────────────────
|
||
if [[ ! -x /usr/local/bin/act_runner ]]; then
|
||
ARCH="$(dpkg --print-architecture)"
|
||
case "$ARCH" in amd64|arm64) ;; *) msg_err "unsupported arch: $ARCH"; exit 1 ;; esac
|
||
msg_info "Downloading act_runner $RUNNER_VERSION ($ARCH)..."
|
||
curl -fsSL "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${ARCH}" \
|
||
-o /usr/local/bin/act_runner
|
||
chmod +x /usr/local/bin/act_runner
|
||
msg_ok "act_runner $(/usr/local/bin/act_runner --version 2>/dev/null | head -n1)"
|
||
else
|
||
msg_warn "act_runner already present, skipping download"
|
||
fi
|
||
|
||
# ── User (docker-Gruppe VOR dem Daemon-Start — Lesson aus nexus K-103:
|
||
# Gruppenmitgliedschaften werden beim Prozessstart eingefroren) ─────────────
|
||
create_system_user "$APP_USER" "$APP_HOME"
|
||
usermod -aG docker "$APP_USER"
|
||
mkdir -p "$RUNNER_DIR"
|
||
chown -R "$APP_USER:$APP_USER" "$APP_HOME"
|
||
|
||
# ── Registrierung (idempotent; braucht CONF-Werte nur beim Erstlauf) ──────────
|
||
if [[ ! -f "$RUNNER_DIR/.runner" ]]; then
|
||
: "${GITEA_INSTANCE_URL:?missing GITEA_INSTANCE_URL (Erstlauf braucht /root/runner.deploy.env)}"
|
||
: "${RUNNER_TOKEN:?missing RUNNER_TOKEN (Erstlauf braucht /root/runner.deploy.env)}"
|
||
msg_info "Registering runner '$RUNNER_NAME' [$RUNNER_LABELS] with $GITEA_INSTANCE_URL (instance scope)..."
|
||
# Werte sind oben strikt validiert (keine Quotes/Whitespace möglich) —
|
||
# Übergabe als argv an runuser, keine erneute Shell-Interpolation (vgl. Finding 76).
|
||
# CWD = RUNNER_DIR via run_user (Finding 85).
|
||
run_user /usr/local/bin/act_runner register \
|
||
--no-interactive \
|
||
--config /dev/null \
|
||
--instance "$GITEA_INSTANCE_URL" \
|
||
--token "$RUNNER_TOKEN" \
|
||
--name "$RUNNER_NAME" \
|
||
--labels "$RUNNER_LABELS" \
|
||
2>&1 | sed "s#$RUNNER_TOKEN#<redacted>#g" || { msg_err "Registrierung fehlgeschlagen"; exit 1; }
|
||
# Belt-and-suspenders: falls eine künftige act_runner-Version doch ins HOME schreibt
|
||
if [[ -f "$APP_HOME/.runner" && ! -f "$RUNNER_DIR/.runner" ]]; then
|
||
mv "$APP_HOME/.runner" "$RUNNER_DIR/.runner"
|
||
fi
|
||
[[ -f "$RUNNER_DIR/.runner" ]] || { msg_err ".runner nach Registrierung nicht gefunden"; exit 1; }
|
||
chown -R "$APP_USER:$APP_USER" "$RUNNER_DIR"
|
||
msg_ok "Runner registered"
|
||
else
|
||
msg_warn "Runner already registered (.runner exists), skipping"
|
||
fi
|
||
|
||
# ── SSH-Root-Login-Policy anwenden, falls vom Host-Script übergeben
|
||
# (Funktion existiert in install.func ab K-114/PR #5 — guarded Aufruf,
|
||
# damit dieser Branch vor und nach dem Merge funktioniert) ────────────────
|
||
if declare -F configure_ssh_root_login >/dev/null 2>&1; then
|
||
configure_ssh_root_login
|
||
fi
|
||
|
||
# ── systemd-Unit (bewusst KEINE sudoers-Datei; Findings 82 + 70) ──────────────
|
||
cat >/etc/systemd/system/act-runner.service <<EOF
|
||
[Unit]
|
||
Description=Gitea Actions runner (instance-wide, label ${RUNNER_LABELS%%:*}, no deploy rights)
|
||
After=network-online.target docker.service
|
||
Wants=network-online.target
|
||
# Finding 82: ohne Docker keine Jobs annehmen — harte Kopplung
|
||
Requires=docker.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$APP_USER
|
||
Group=$APP_USER
|
||
WorkingDirectory=$RUNNER_DIR
|
||
Environment=HOME=$APP_HOME
|
||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||
ExecStart=/usr/local/bin/act_runner daemon
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
# Finding 70: Basis-Härtung (docker-CLI über Socket bleibt funktional)
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable --now act-runner.service
|
||
msg_ok "act-runner.service installed + started"
|
||
|
||
CRED_FILE="/root/runner.credentials"
|
||
cat >"$CRED_FILE" <<EOF
|
||
Allgemeiner Gitea-Actions-Runner (instanzweit)
|
||
|
||
Instance: ${GITEA_INSTANCE_URL:-<bestehende Registrierung, siehe $RUNNER_DIR/.runner>}
|
||
Name/Label: $RUNNER_NAME [$RUNNER_LABELS]
|
||
User: $APP_USER (docker-Gruppe, KEIN sudo)
|
||
Workdir: $RUNNER_DIR
|
||
Logs: journalctl -u act-runner -f
|
||
|
||
Repos nutzen ihn mit: runs-on: ${RUNNER_LABELS%%:*}
|
||
|
||
SICHERHEITSMODELL: docker-Gruppe == de-facto root in DIESEM LXC; instanzweit
|
||
heißt: jedes Repo der Instanz kann den Runner-LXC kontrollieren. Akzeptiert,
|
||
weil Single-Admin-Instanz und dieser LXC nichts besitzt (keine sudoers, keine
|
||
Produktions-Mounts). Deploy-Jobs gehören NICHT hierher — die bleiben auf dem
|
||
repo-scoped nexus-Runner. Trennung beibehalten.
|
||
EOF
|
||
chmod 600 "$CRED_FILE"
|
||
|
||
if [[ -f "$CONF" ]]; then
|
||
shred -u "$CONF" 2>/dev/null || rm -f "$CONF"
|
||
fi
|
||
apt_cleanup
|
||
msg_ok "runner installation finished"
|