From 406a8f86b192d7257100c2e57cc13cf6e665e173 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 18 Jul 2026 12:43:12 +0200 Subject: [PATCH] [chore](trx-rs): add Podman act_runner CI deployment Add container/: a rootless Podman + systemd (Quadlet) setup to run per-project Gitea Actions runners on one host instead of VMs. Uses the host executor with a purpose-built image that bakes in the Rust toolchain and all build dependencies (opus, alsa, soapysdr, clang), so CI runs skip the per-run install and cold soapysdr-sys build. Includes the runner Containerfile, first-boot registration entrypoint, act_runner config template, two Quadlet units (trx-rs + a second project), and a README covering build, registration, the required host-executor workflow tweak, and tuning. Assisted-By: Claude Code (claude-opus-4) Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV Signed-off-by: Stan Grams --- container/Containerfile | 59 ++++++++++++++ container/README.md | 118 ++++++++++++++++++++++++++++ container/config.yaml | 32 ++++++++ container/entrypoint.sh | 38 +++++++++ container/project2-runner.container | 40 ++++++++++ container/trx-rs-runner.container | 41 ++++++++++ 6 files changed, 328 insertions(+) create mode 100644 container/Containerfile create mode 100644 container/README.md create mode 100644 container/config.yaml create mode 100644 container/entrypoint.sh create mode 100644 container/project2-runner.container create mode 100644 container/trx-rs-runner.container diff --git a/container/Containerfile b/container/Containerfile new file mode 100644 index 00000000..1632a08e --- /dev/null +++ b/container/Containerfile @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later + +# Gitea Actions runner image for trx-rs CI (host-executor / "Pattern B"). +# +# All build dependencies, the Rust toolchain, Node.js (for JS actions such as +# actions/checkout and actions/cache) and the `reuse` tool are baked in, so CI +# runs skip the per-run apt/rustup install cost. `sudo` is present so the +# existing workflow's `sudo apt-get ...` / rustup steps remain valid — they +# just become fast no-ops because everything is already installed. +FROM docker.io/library/debian:bookworm-slim + +ARG ACT_RUNNER_VERSION=0.2.11 +ARG NODE_MAJOR=20 + +ENV DEBIAN_FRONTEND=noninteractive \ + RUSTUP_HOME=/opt/rustup \ + CARGO_HOME=/opt/cargo \ + PATH=/opt/cargo/bin:/usr/local/bin:/usr/bin:/bin + +# Base tooling + trx-rs build dependencies (mirrors .gitea/workflows/ci.yml). +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl xz-utils git sudo pipx \ + build-essential pkg-config cmake clang libclang-dev \ + libopus-dev libasound2-dev libsoapysdr-dev \ + && rm -rf /var/lib/apt/lists/* + +# Node.js (JS-based actions need node in PATH under the host executor). +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# REUSE >= 3 (Debian's packaged reuse is too old for REUSE.toml). +RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install reuse + +# Rust stable with rustfmt + clippy, installed system-wide. +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --profile minimal \ + --component rustfmt --component clippy \ + && chmod -R a+rwX "$CARGO_HOME" "$RUSTUP_HOME" + +# act_runner binary. +RUN arch="$(dpkg --print-architecture)"; \ + case "$arch" in amd64) rarch=amd64;; arm64) rarch=arm64;; *) echo "unsupported arch $arch" >&2; exit 1;; esac; \ + curl -fsSL -o /usr/local/bin/act_runner \ + "https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-linux-${rarch}" \ + && chmod +x /usr/local/bin/act_runner + +# Default config template (seeded into the /data volume on first boot). +COPY config.yaml /etc/act_runner/config.yaml +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# /data holds the .runner registration, cache and workflow workspaces. +VOLUME /data +WORKDIR /data + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/container/README.md b/container/README.md new file mode 100644 index 00000000..36dab96a --- /dev/null +++ b/container/README.md @@ -0,0 +1,118 @@ + + +# Podman-based Gitea Actions runners + +Run two independent Gitea Actions runners on one host as rootless Podman +containers managed by systemd (Quadlet) — one per project — instead of two +VMs. Uses the **host executor**: workflow steps run directly inside a +purpose-built runner image that already has the Rust toolchain and all build +dependencies baked in, so CI runs skip the per-run install cost and no +Docker/Podman socket is needed. + +## Files + +| File | Purpose | +|------|---------| +| `Containerfile` | Runner image: Debian + build deps + clang + Rust + Node + `reuse` + `act_runner`. | +| `entrypoint.sh` | Registers on first boot (if needed), then runs the daemon. | +| `config.yaml` | act_runner config template (seeded into each runner's volume). | +| `trx-rs-runner.container` | Quadlet unit for the trx-rs runner. | +| `project2-runner.container` | Quadlet unit for the second project's runner. | + +## Prerequisites (once per host) + +Rootless Podman with cgroups v2 (default on modern distros). As the unprivileged +user that will own the runners: + +```bash +# Survive logout / start on boot without an interactive session. +loginctl enable-linger "$USER" +``` + +No `podman.socket` is required for the host executor. + +## 1. Build the image + +```bash +cd container +podman build -t trx-rs-ci:latest . +``` + +## 2. Get a registration token + +For **each** repo: *Settings → Actions → Runners → Create new Runner* and copy +the token. (Org- or instance-level tokens work too if you prefer wider scope.) + +## 3. Install and start the runners + +```bash +mkdir -p ~/.config/containers/systemd +cp trx-rs-runner.container project2-runner.container ~/.config/containers/systemd/ + +# Paste each repo's token for the FIRST boot only: +# Environment=GITEA_RUNNER_REGISTRATION_TOKEN=xxxx… +$EDITOR ~/.config/containers/systemd/trx-rs-runner.container +$EDITOR ~/.config/containers/systemd/project2-runner.container + +systemctl --user daemon-reload +systemctl --user start trx-rs-runner +systemctl --user start project2-runner + +systemctl --user status trx-rs-runner +podman logs -f gitea-runner-trx-rs +``` + +Once each runner shows **online** in the repo's runner list, blank out the +`GITEA_RUNNER_REGISTRATION_TOKEN` line again (the registration is persisted in +the `…-data` volume) and `systemctl --user daemon-reload`. + +## Required workflow change: the `reuse` job + +The host executor runs steps directly in the container and therefore **cannot +run Docker-based actions**. The current `reuse` job uses `fsfe/reuse-action@v5`, +which is a Docker action. `reuse` is baked into the image, so replace that job +with a plain command: + +```yaml + reuse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: REUSE compliance + run: reuse lint +``` + +The `lint` and `test` jobs need no changes: their `sudo apt-get …` and rustup +steps still run, but become fast no-ops because the image already has those +packages and the toolchain. (`sudo` is included in the image for exactly this +reason.) + +> If you would rather keep Docker-based actions and per-run images, use the +> **Docker executor** instead: drop the `:host` suffix from the label in +> `config.yaml`, enable `systemctl --user --now enable podman.socket`, mount it +> into the container, and set `container.docker_host` to the socket path. That +> trades the baked-in speed for stronger per-job isolation. + +## Tuning + +- **`capacity`** (in `config.yaml`) — concurrent jobs per runner. Rust builds + are heavy; 1–2 is sensible when two runners share a host. +- **`PodmanArgs=--cpus/--memory`** (in each `.container`) — hard resource caps + so one project cannot starve the other. +- **SELinux** — the `:Z` volume flag is already set; keep it if SELinux is + enforcing. + +## Committing these files + +If you add this directory to a REUSE-checked repo, register the markdown in +`REUSE.toml` (the other files carry inline SPDX headers): + +```toml +[[annotations]] +path = ["container/**"] +SPDX-FileCopyrightText = "2026 Stan Grams " +SPDX-License-Identifier = "GPL-2.0-or-later" +``` diff --git a/container/config.yaml b/container/config.yaml new file mode 100644 index 00000000..6ffe1c0c --- /dev/null +++ b/container/config.yaml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# act_runner configuration template. Seeded into /data/config.yaml on first +# boot; edit the copy inside the volume to change settings per runner. + +log: + level: info + +runner: + # Registration state. Relative to the daemon's working directory (/data). + file: .runner + # Concurrent jobs this runner will pick up. Rust builds are heavy — keep this + # modest, especially if two runners share one host. The trx-rs workflow has + # three parallel jobs (lint, test, reuse); capacity 2 lets two overlap. + capacity: 2 + timeout: 3h + # Map the workflow's `runs-on: ubuntu-latest` to the HOST executor, i.e. run + # steps directly inside THIS container (which already has all the toolchain). + # No Docker/Podman socket is required in this mode. + labels: + - "ubuntu-latest:host" + +cache: + # Built-in actions cache server (used by actions/cache). Stored in the volume. + enabled: true + dir: "/data/cache" + +host: + # Where per-job workspaces are created. + workdir_parent: /data/workflows diff --git a/container/entrypoint.sh b/container/entrypoint.sh new file mode 100644 index 00000000..72a1dfe3 --- /dev/null +++ b/container/entrypoint.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Registers the runner on first boot (if no .runner state exists in /data), +# then runs the act_runner daemon. Idempotent: on subsequent boots it reuses +# the stored registration and ignores the token. +set -euo pipefail + +CONFIG_FILE="${CONFIG_FILE:-/data/config.yaml}" + +cd /data + +# Seed the config from the image's template on first boot so it lives in the +# persistent volume and can be edited there. +if [ ! -f "$CONFIG_FILE" ]; then + cp /etc/act_runner/config.yaml "$CONFIG_FILE" +fi + +# runner.file in config.yaml is ".runner" (relative to this CWD => /data/.runner). +if [ ! -f /data/.runner ]; then + if [ -z "${GITEA_RUNNER_REGISTRATION_TOKEN:-}" ]; then + echo "ERROR: no /data/.runner registration and GITEA_RUNNER_REGISTRATION_TOKEN is empty." >&2 + echo " Grab a token from the repo's Settings -> Actions -> Runners and set it" >&2 + echo " in the Quadlet unit for the first boot only." >&2 + exit 1 + fi + echo "Registering runner '${GITEA_RUNNER_NAME:-podman}' with ${GITEA_INSTANCE_URL} ..." + act_runner register --no-interactive \ + --config "$CONFIG_FILE" \ + --instance "${GITEA_INSTANCE_URL:?set GITEA_INSTANCE_URL}" \ + --token "$GITEA_RUNNER_REGISTRATION_TOKEN" \ + --name "${GITEA_RUNNER_NAME:-podman}" \ + --labels "${GITEA_RUNNER_LABELS:-ubuntu-latest:host}" +fi + +exec act_runner daemon --config "$CONFIG_FILE" diff --git a/container/project2-runner.container b/container/project2-runner.container new file mode 100644 index 00000000..6448f4e2 --- /dev/null +++ b/container/project2-runner.container @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Rootless Podman Quadlet for the SECOND project's Gitea Actions runner, +# co-located on the same host as the trx-rs runner. +# +# It has its own name, its own data volume and its own registration token, so +# the two runners are fully independent. They share the `ubuntu-latest` label, +# but registration SCOPE (which repo each token came from) keeps their jobs +# separate — neither will pick up the other's work. +# +# If project 2 needs different build dependencies, build it its own image from +# an adjusted Containerfile and point Image= at that instead of reusing the +# trx-rs image below. + +[Unit] +Description=Gitea Actions runner — project 2 +After=network-online.target +Wants=network-online.target + +[Container] +Image=localhost/gitea-act-runner:latest +ContainerName=gitea-runner-project2 +Volume=gitea-runner-project2-data:/data:Z + +Environment=CONFIG_FILE=/data/config.yaml +Environment=GITEA_INSTANCE_URL=https://git.haxx.space +Environment=GITEA_RUNNER_NAME=project2-podman +Environment=GITEA_RUNNER_LABELS=ubuntu-latest:host +Environment=GITEA_RUNNER_REGISTRATION_TOKEN= + +PodmanArgs=--cpus=4.0 --memory=6g + +[Service] +Restart=always +TimeoutStartSec=0 + +[Install] +WantedBy=default.target diff --git a/container/trx-rs-runner.container b/container/trx-rs-runner.container new file mode 100644 index 00000000..53b460e0 --- /dev/null +++ b/container/trx-rs-runner.container @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Rootless Podman Quadlet for the trx-rs Gitea Actions runner. +# Install to ~/.config/containers/systemd/trx-rs-runner.container then: +# systemctl --user daemon-reload +# systemctl --user start trx-rs-runner +# +# First boot only: paste a registration token (repo Settings -> Actions -> +# Runners) into GITEA_RUNNER_REGISTRATION_TOKEN. After the runner appears +# online you can blank it again — the registration is persisted in the volume. + +[Unit] +Description=Gitea Actions runner — trx-rs +After=network-online.target +Wants=network-online.target + +[Container] +Image=localhost/trx-rs-ci:latest +ContainerName=gitea-runner-trx-rs +# Persistent state: .runner registration, cache, workspaces. +Volume=gitea-runner-trx-rs-data:/data:Z + +Environment=CONFIG_FILE=/data/config.yaml +Environment=GITEA_INSTANCE_URL=https://git.haxx.space +Environment=GITEA_RUNNER_NAME=trx-rs-podman +Environment=GITEA_RUNNER_LABELS=ubuntu-latest:host +Environment=GITEA_RUNNER_REGISTRATION_TOKEN= + +# Resource caps so a heavy Rust build here cannot starve the other project's +# runner on the same host. Tune to your box. +PodmanArgs=--cpus=4.0 --memory=6g + +[Service] +Restart=always +# A cold Rust build can be slow; don't let systemd consider startup failed. +TimeoutStartSec=0 + +[Install] +WantedBy=default.target -- 2.55.0