From e9cf5e873935206fa84b5fbc0a76fbfdfb427e7a Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 19 Jul 2026 00:22:27 +0200 Subject: [PATCH] [chore](trx-rs): shared SDK image for CI and developers Rework container/ from a host-executor act_runner image into a single "SDK" build image used everywhere: as the CI job container (Docker executor) and by developers locally / via .devcontainer. It bakes in a pinned Rust toolchain and all build dependencies, so CI and every developer share the exact same rustc/clippy. - container/Containerfile: SDK image (Debian + deps + pinned Rust + Node). - rust-toolchain.toml: pin the toolchain to match the image; also ends the "CI clippy newer than local" version skew. - .gitea/workflows/ci.yml: lint/test run inside the SDK image via `container:`; reuse returns to fsfe/reuse-action (Docker executor runs it as a sibling container, so nothing REUSE-related is baked in). - .devcontainer/devcontainer.json: dev use of the same image. - container/runner-config.example.yaml: Docker-executor runner config for the CI VM, capped for a 2-thread budget. - Drop the obsolete host-executor entrypoint/config/Quadlet units. Assisted-By: Claude Code (claude-opus-4) Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV Signed-off-by: Stan Grams --- .devcontainer/devcontainer.json | 14 +++ .gitea/workflows/ci.yml | 16 ++- REUSE.toml | 2 + container/Containerfile | 63 +++++------- container/README.md | 142 ++++++++++----------------- container/config.yaml | 32 ------ container/entrypoint.sh | 38 ------- container/project2-runner.container | 40 -------- container/runner-config.example.yaml | 33 +++++++ container/trx-rs-runner.container | 41 -------- rust-toolchain.toml | 11 +++ 11 files changed, 142 insertions(+), 290 deletions(-) create mode 100644 .devcontainer/devcontainer.json delete mode 100644 container/config.yaml delete mode 100644 container/entrypoint.sh delete mode 100644 container/project2-runner.container create mode 100644 container/runner-config.example.yaml delete mode 100644 container/trx-rs-runner.container create mode 100644 rust-toolchain.toml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..e11972cc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "trx-rs SDK", + "image": "git.haxx.space/sjg/trx-rs-sdk:latest", + "workspaceFolder": "/work", + "workspaceMount": "source=${localWorkspaceFolder},target=/work,type=bind", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml" + ] + } + } +} diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7cc2a4d0..3609a8ad 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,10 +2,10 @@ # # SPDX-License-Identifier: GPL-2.0-or-later -# CI for the self-hosted, host-executor Podman runners (see container/). -# The runner image bakes in the Rust toolchain and all build dependencies, -# so jobs go straight to cargo — no apt/rustup setup steps (which also -# collided on the dpkg lock when jobs ran concurrently in the same runner). +# CI for the Docker-executor runner (VM). The lint/test jobs run inside the +# shared trx-rs SDK image (container/Containerfile), which bakes in the pinned +# Rust toolchain and all build dependencies. The reuse job uses the upstream +# Docker action, which the Docker executor launches as a sibling container. name: CI @@ -20,6 +20,7 @@ env: jobs: lint: runs-on: ubuntu-latest + container: git.haxx.space/sjg/trx-rs-sdk:latest steps: - uses: actions/checkout@v4 - name: rustfmt @@ -29,6 +30,7 @@ jobs: test: runs-on: ubuntu-latest + container: git.haxx.space/sjg/trx-rs-sdk:latest steps: - uses: actions/checkout@v4 - name: Build @@ -40,8 +42,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: REUSE compliance - # `reuse` CLI instead of fsfe/reuse-action: the latter is a Docker - # action, which the host-executor runners cannot run. `reuse` is baked - # into the runner image (see container/Containerfile). - run: reuse lint + - uses: fsfe/reuse-action@v5 diff --git a/REUSE.toml b/REUSE.toml index f46d1a72..7b454a76 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -12,6 +12,8 @@ path = [ "trx-rs.toml.example", "docs/**", "aidocs/**", + "container/**", + ".devcontainer/**", "src/decoders/trx-ftx/README.md", "src/decoders/trx-wxsat/README.md", "assets/trx-logo.png", diff --git a/container/Containerfile b/container/Containerfile index 1e5b62ca..00f31646 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -2,60 +2,45 @@ # # SPDX-License-Identifier: GPL-2.0-or-later -# Gitea Actions runner image for trx-rs CI (host-executor / "Pattern B"). +# trx-rs SDK / build image. # -# 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. +# Single source of truth for the build environment. Used two ways: +# * CI — as the job container for the lint/test jobs (Docker executor). +# * Dev — run locally or via .devcontainer for a reproducible toolchain. +# +# Pinning the Rust version here (and in rust-toolchain.toml) means CI and every +# developer share the exact same rustc/clippy, so "works locally, fails in CI" +# cannot happen. FROM docker.io/library/debian:bookworm-slim -ARG ACT_RUNNER_VERSION=0.2.11 +# Keep in sync with rust-toolchain.toml. +ARG RUST_VERSION=1.97.1 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 + RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:/usr/local/bin:/usr/bin:/bin -# Base tooling + trx-rs build dependencies (mirrors .gitea/workflows/ci.yml). +# Build dependencies (mirror README's manual instructions). RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl xz-utils git sudo pipx \ + ca-certificates curl git \ 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). +# Node.js — JS-based actions (actions/checkout, actions/cache) run *inside* +# the job container under the Docker executor, so node must be present. 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). -# The [charset-normalizer] extra provides an encoding-detection backend; -# without it (and without libmagic) reuse fails to import at runtime. -RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install 'reuse[charset-normalizer]' - -# Rust stable with rustfmt + clippy, installed system-wide. +# Pinned Rust toolchain, installed world-readable so any UID the runner or a +# devcontainer uses can invoke cargo. 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" + | sh -s -- -y --no-modify-path \ + --default-toolchain "${RUST_VERSION}" --profile minimal \ + --component rustfmt --component clippy \ + && chmod -R a+rwX "$RUSTUP_HOME" "$CARGO_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"] +WORKDIR /work diff --git a/container/README.md b/container/README.md index 36dab96a..df75998b 100644 --- a/container/README.md +++ b/container/README.md @@ -3,116 +3,76 @@ SPDX-FileCopyrightText: 2026 Stan Grams SPDX-License-Identifier: GPL-2.0-or-later --> -# Podman-based Gitea Actions runners +# trx-rs SDK image -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 +A single container image that is the canonical build environment for trx-rs, +used **both** by CI and by developers. It bakes in the pinned Rust toolchain +(matching `rust-toolchain.toml`) and every build dependency, so the compiler +and `clippy` are identical everywhere — no "works on my machine". | 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. | +| `Containerfile` | The SDK image (Debian + build deps + pinned Rust + Node + git). | +| `runner-config.example.yaml` | Example act_runner config for the CI VM (Docker executor). | -## Prerequisites (once per host) - -Rootless Podman with cgroups v2 (default on modern distros). As the unprivileged -user that will own the runners: +## Build and publish ```bash -# Survive logout / start on boot without an interactive session. -loginctl enable-linger "$USER" +# from the repo root +podman build -t git.haxx.space/sjg/trx-rs-sdk:latest container +podman login git.haxx.space +podman push git.haxx.space/sjg/trx-rs-sdk:latest ``` -No `podman.socket` is required for the host executor. +Tag with the Rust version too (e.g. `:1.97.1`) if you want reproducible pins. +Make the package **public** (Gitea → Packages → the image → Settings) so the CI +runner and developers can pull it without credentials. If you keep it private, +add `credentials:` under the workflow's `container:` and log the runner into the +registry. -## 1. Build the image +## Developer use + +Reproducible one-off build, no local toolchain needed: ```bash -cd container -podman build -t trx-rs-ci:latest . +podman run --rm -it -v "$PWD":/work -w /work \ + git.haxx.space/sjg/trx-rs-sdk:latest \ + cargo build --release ``` -## 2. Get a registration token +Or open the repo in the image via VS Code / JetBrains "Reopen in Container" +(`.devcontainer/devcontainer.json` points at the same image). -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.) +Building outside the container? `rust-toolchain.toml` pins the same rustc, so +`rustup` installs the matching toolchain automatically. -## 3. Install and start the runners +## CI use -```bash -mkdir -p ~/.config/containers/systemd -cp trx-rs-runner.container project2-runner.container ~/.config/containers/systemd/ +`.gitea/workflows/ci.yml` runs the `lint` and `test` jobs *inside* this image +via the `container:` key, so they skip all setup and go straight to `cargo`. +The `reuse` job stays on the upstream `fsfe/reuse-action` (a Docker action the +Docker executor launches as a sibling container) — nothing REUSE-related is +baked into the SDK. -# 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 +## CI runner (VM) -systemctl --user daemon-reload -systemctl --user start trx-rs-runner -systemctl --user start project2-runner +The runner runs in a small VM using the **Docker executor** (not the host +executor). This gives per-job container isolation and standard `ubuntu-latest` +semantics, and caps CI resources at the VM boundary. -systemctl --user status trx-rs-runner -podman logs -f gitea-runner-trx-rs -``` +1. **Size the VM to your thread budget.** 2 vCPUs = a hard 2-thread ceiling. + Pin them to specific host threads so CI never touches the rest (libvirt/KVM): -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`. + ```xml + 2 + + + + + ``` -## 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" -``` +2. **Inside the VM:** install Docker (moby) and `act_runner`. +3. **Register one runner per project** (separate tokens from each repo's + *Settings → Actions → Runners*; scope keeps their jobs apart). +4. **Configure** with `runner-config.example.yaml` — `capacity: 1` and + `container.options: "--cpus=2"` keep usage inside the 2-thread budget. diff --git a/container/config.yaml b/container/config.yaml deleted file mode 100644 index 6ffe1c0c..00000000 --- a/container/config.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# 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 deleted file mode 100644 index 72a1dfe3..00000000 --- a/container/entrypoint.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/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 deleted file mode 100644 index 6448f4e2..00000000 --- a/container/project2-runner.container +++ /dev/null @@ -1,40 +0,0 @@ -# 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/runner-config.example.yaml b/container/runner-config.example.yaml new file mode 100644 index 00000000..03f6cb76 --- /dev/null +++ b/container/runner-config.example.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Example act_runner config for the Docker-executor runner that lives in the +# CI VM. This is NOT the SDK image — it configures the runner that launches +# per-job containers (including the trx-rs SDK image referenced by the +# workflow's `container:` key). Copy to the VM and pass with +# `act_runner daemon --config`. + +log: + level: info + +runner: + file: .runner + # One concurrent job. With one runner per project on a 2-vCPU VM this keeps + # total CI usage at ~2 threads. + capacity: 1 + timeout: 3h + # Docker executor: no ":host" suffix. Maps runs-on labels to base images + # (the workflow overrides these per job via `container:`). + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-latest" + +cache: + enabled: true + +container: + # Cap every job container's CPU so CI stays within the 2-thread budget even + # if capacity is raised later. + options: "--cpus=2" + # Reuse the host VM's Docker network for the built-in cache/artifact server. + network: "host" diff --git a/container/trx-rs-runner.container b/container/trx-rs-runner.container deleted file mode 100644 index 53b460e0..00000000 --- a/container/trx-rs-runner.container +++ /dev/null @@ -1,41 +0,0 @@ -# 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 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..323e27b0 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Pins the Rust toolchain for reproducible builds. Keep in sync with the +# SDK image (container/Containerfile, ARG RUST_VERSION). rustup honours this +# automatically for local builds outside the SDK container. + +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy"]