From 7022f20b76fadbf953edafa3819715b69ca00000 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 19 Jul 2026 00:22:27 +0200 Subject: [PATCH 1/8] [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 d8fcf43a..3f08504c 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 @@ -72,8 +74,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 223a64e1..e39535e3 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 86f4f292..b882f8a3 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 chromium \ && 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"] -- 2.55.0 From 8fd1761688461c420ca8469c39546936c80f1f86 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 19 Jul 2026 13:06:43 +0200 Subject: [PATCH 2/8] [chore](trx-rs): use nested SDK image path trx-rs/sdk Match the image name that was pushed to the registry (git.haxx.space/sjg/trx-rs/sdk) across the workflow, devcontainer and README. Assisted-By: Claude Code (claude-opus-4) Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV Signed-off-by: Stan Grams --- .devcontainer/devcontainer.json | 2 +- .gitea/workflows/ci.yml | 4 ++-- container/README.md | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e11972cc..ed8cbc3f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "trx-rs SDK", - "image": "git.haxx.space/sjg/trx-rs-sdk:latest", + "image": "git.haxx.space/sjg/trx-rs/sdk:latest", "workspaceFolder": "/work", "workspaceMount": "source=${localWorkspaceFolder},target=/work,type=bind", "customizations": { diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3f08504c..ff587761 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -20,7 +20,7 @@ env: jobs: lint: runs-on: ubuntu-latest - container: git.haxx.space/sjg/trx-rs-sdk:latest + container: git.haxx.space/sjg/trx-rs/sdk:latest steps: - uses: actions/checkout@v4 - name: rustfmt @@ -30,7 +30,7 @@ jobs: test: runs-on: ubuntu-latest - container: git.haxx.space/sjg/trx-rs-sdk:latest + container: git.haxx.space/sjg/trx-rs/sdk:latest steps: - uses: actions/checkout@v4 - name: Build diff --git a/container/README.md b/container/README.md index df75998b..1fd15f16 100644 --- a/container/README.md +++ b/container/README.md @@ -19,9 +19,9 @@ and `clippy` are identical everywhere — no "works on my machine". ```bash # from the repo root -podman build -t git.haxx.space/sjg/trx-rs-sdk:latest container +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 +podman push git.haxx.space/sjg/trx-rs/sdk:latest ``` Tag with the Rust version too (e.g. `:1.97.1`) if you want reproducible pins. @@ -36,7 +36,7 @@ Reproducible one-off build, no local toolchain needed: ```bash podman run --rm -it -v "$PWD":/work -w /work \ - git.haxx.space/sjg/trx-rs-sdk:latest \ + git.haxx.space/sjg/trx-rs/sdk:latest \ cargo build --release ``` -- 2.55.0 From f64032dcbecb9f28e7c74f50282160c57775ffed Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 19 Jul 2026 13:14:17 +0200 Subject: [PATCH 3/8] [chore](trx-rs): add OpenRC service for act_runner on Alpine The runner host is Alpine (OpenRC, no systemd). Add an OpenRC init script for act_runner (supervise-daemon, depends on docker) plus a conf.d example for running one instance per project, and rewrite the runner section of the README with Alpine setup steps (apk docker, dedicated user in the docker group, register, service install). Assisted-By: Claude Code (claude-opus-4) Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV Signed-off-by: Stan Grams --- container/README.md | 72 ++++++++++++++++++++++-------- container/act_runner.confd.example | 14 ++++++ container/act_runner.openrc | 44 ++++++++++++++++++ 3 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 container/act_runner.confd.example create mode 100644 container/act_runner.openrc diff --git a/container/README.md b/container/README.md index 1fd15f16..8937d7a9 100644 --- a/container/README.md +++ b/container/README.md @@ -54,25 +54,61 @@ 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. -## CI runner (VM) +## CI runner (Alpine / OpenRC) -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. +The runner uses the **Docker executor** (not the host executor): per-job +container isolation and standard `ubuntu-latest` semantics. `act_runner` runs +as an OpenRC service. Files provided: -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): +| File | Purpose | +|------|---------| +| `act_runner.openrc` | OpenRC init script (`supervise-daemon`, depends on docker). | +| `act_runner.confd.example` | Per-instance `conf.d` settings for multi-runner hosts. | - ```xml - 2 - - - - - ``` +**Cap the thread budget.** In a VM, pin its vCPUs to specific host threads +(libvirt/KVM): -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. +```xml +2 + + + + +``` + +On bare metal, the `container.options: "--cpus=2"` and `capacity: 1` in +`runner-config.example.yaml` already bound each runner. + +**Set it up:** + +```bash +# 1. Docker + a dedicated user with socket access +apk add docker docker-cli +rc-update add docker default && rc-service docker start +adduser -S -D -H -h /var/lib/act_runner act +addgroup act docker + +# 2. act_runner binary (static Go build, works on musl) +curl -fsSL -o /usr/local/bin/act_runner \ + https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64 +chmod +x /usr/local/bin/act_runner + +# 3. Config + register one runner per project (scope keeps their jobs apart) +install -Dm644 container/runner-config.example.yaml /etc/act_runner/trx-rs.yaml +install -d -o act /var/lib/act_runner/trx-rs +su act -s /bin/sh -c 'cd /var/lib/act_runner/trx-rs && \ + act_runner register --no-interactive \ + --instance https://git.haxx.space --token \ + --name trx-rs-ci \ + --labels "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"' + +# 4. OpenRC service (repeat the symlink+conf.d for the second project) +install -m755 container/act_runner.openrc /etc/init.d/act_runner +ln -s act_runner /etc/init.d/act_runner.trx-rs +install -m644 container/act_runner.confd.example /etc/conf.d/act_runner.trx-rs +rc-update add act_runner.trx-rs default +rc-service act_runner.trx-rs start +``` + +Check it with `rc-service act_runner.trx-rs status` and +`tail -f /var/log/act_runner.trx-rs.log`. diff --git a/container/act_runner.confd.example b/container/act_runner.confd.example new file mode 100644 index 00000000..b3ee409e --- /dev/null +++ b/container/act_runner.confd.example @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 Stan Grams +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Per-instance settings for an act_runner OpenRC service. +# Copy to /etc/conf.d/, e.g. /etc/conf.d/act_runner.trx-rs +# (the name must match the /etc/init.d/ symlink). + +# User that runs the daemon. Must be a member of the `docker` group. +runner_user="act" + +# Per-instance state dir (holds the .runner registration) and config file, +# so two runners on one host stay independent. +runner_dir="/var/lib/act_runner/trx-rs" +runner_config="/etc/act_runner/trx-rs.yaml" diff --git a/container/act_runner.openrc b/container/act_runner.openrc new file mode 100644 index 00000000..5c87aff3 --- /dev/null +++ b/container/act_runner.openrc @@ -0,0 +1,44 @@ +#!/sbin/openrc-run +# SPDX-FileCopyrightText: 2026 Stan Grams +# SPDX-License-Identifier: GPL-2.0-or-later +# +# OpenRC service for a Gitea act_runner (Docker executor) on Alpine. +# +# Install as /etc/init.d/act_runner (chmod +x). Single instance uses +# /etc/act_runner/config.yaml. For one runner per project, symlink this script +# and add a matching conf.d file: +# +# ln -s act_runner /etc/init.d/act_runner.trx-rs +# cp container/act_runner.confd.example /etc/conf.d/act_runner.trx-rs +# $EDITOR /etc/conf.d/act_runner.trx-rs # set runner_dir / runner_config +# rc-update add act_runner.trx-rs default +# rc-service act_runner.trx-rs start + +description="Gitea Actions runner" + +: "${runner_user:=act}" +: "${runner_dir:=/var/lib/act_runner}" +: "${runner_config:=/etc/act_runner/config.yaml}" + +command="/usr/local/bin/act_runner" +command_args="daemon --config ${runner_config}" +# No group given, so supplementary groups (incl. docker) are initialised. +command_user="${runner_user}" +directory="${runner_dir}" + +supervisor="supervise-daemon" +respawn_delay=5 +respawn_max=0 +pidfile="/run/${RC_SVCNAME}.pid" +output_log="/var/log/${RC_SVCNAME}.log" +error_log="/var/log/${RC_SVCNAME}.log" + +depend() { + need docker + use net dns +} + +start_pre() { + checkpath -d -m 0750 -o "${runner_user}" "${runner_dir}" + checkpath -f -m 0640 -o "${runner_user}" "${output_log}" +} -- 2.55.0 From 8b725735219fdbc1049df334ef574a265d163508 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 19 Jul 2026 16:43:59 +0200 Subject: [PATCH 4/8] [chore](trx-rs): add sccache compilation cache Bake sccache into the SDK image and enable it via RUSTC_WRAPPER in CI and the devcontainer (not repo-wide, so non-SDK builds are unaffected). - container/Containerfile: install the sccache musl binary. - ci.yml: RUSTC_WRAPPER=sccache, CARGO_INCREMENTAL=0, SCCACHE_DIR=/sccache, cache size cap, plus a `sccache --show-stats` step per job. - runner-config.example.yaml: bind-mount /var/cache/sccache into job containers so the cache persists across runs and is shared between jobs. - .devcontainer: enable sccache with a named cache volume. Assisted-By: Claude Code (claude-opus-4) Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV Signed-off-by: Stan Grams --- .devcontainer/devcontainer.json | 8 ++++++++ .gitea/workflows/ci.yml | 13 +++++++++++++ container/Containerfile | 9 +++++++++ container/README.md | 16 ++++++++++++++++ container/runner-config.example.yaml | 6 ++++-- 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ed8cbc3f..bc185b54 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,6 +3,14 @@ "image": "git.haxx.space/sjg/trx-rs/sdk:latest", "workspaceFolder": "/work", "workspaceMount": "source=${localWorkspaceFolder},target=/work,type=bind", + "mounts": [ + "source=trx-rs-sccache,target=/sccache,type=volume" + ], + "containerEnv": { + "RUSTC_WRAPPER": "sccache", + "CARGO_INCREMENTAL": "0", + "SCCACHE_DIR": "/sccache" + }, "customizations": { "vscode": { "extensions": [ diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ff587761..be432c25 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -16,6 +16,13 @@ on: env: CARGO_TERM_COLOR: always + # sccache: shared compilation cache persisted on the runner host (see the + # -v mount in runner-config.example.yaml). CARGO_INCREMENTAL=0 because + # sccache cannot cache incremental artifacts. + RUSTC_WRAPPER: sccache + CARGO_INCREMENTAL: "0" + SCCACHE_DIR: /sccache + SCCACHE_CACHE_SIZE: "20G" jobs: lint: @@ -27,6 +34,9 @@ jobs: run: cargo fmt --all -- --check - name: clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: sccache stats + if: always() + run: sccache --show-stats test: runs-on: ubuntu-latest @@ -37,6 +47,9 @@ jobs: run: cargo build --workspace --all-targets --locked - name: Test run: cargo test --workspace --locked + - name: sccache stats + if: always() + run: sccache --show-stats frontend: runs-on: ubuntu-latest diff --git a/container/Containerfile b/container/Containerfile index b882f8a3..4634b52f 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -43,4 +43,13 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ --component rustfmt --component clippy \ && chmod -R a+rwX "$RUSTUP_HOME" "$CARGO_HOME" +# sccache — shared compilation cache. Enabled at build time via +# RUSTC_WRAPPER (see the CI workflow and .devcontainer), not repo-wide, so +# non-SDK builds are unaffected. musl build is static and runs anywhere. +ARG SCCACHE_VERSION=0.8.2 +RUN curl -fsSL "https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ + | tar -xz -C /tmp \ + && install -m755 "/tmp/sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache" /usr/local/bin/sccache \ + && rm -rf /tmp/sccache-* + WORKDIR /work diff --git a/container/README.md b/container/README.md index 8937d7a9..dac690f9 100644 --- a/container/README.md +++ b/container/README.md @@ -54,6 +54,22 @@ 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. +## Compilation cache (sccache) + +The SDK image ships [`sccache`](https://github.com/mozilla/sccache). It is +enabled via `RUSTC_WRAPPER=sccache` in CI and the devcontainer (not repo-wide, +so plain `cargo` builds outside the SDK are unaffected). + +- **CI** persists the cache on the runner host — create the dir once: + `mkdir -p /var/cache/sccache`. It is bind-mounted into each job container at + `/sccache` (see `runner-config.example.yaml`), so cache survives across runs + and is shared between the lint/test jobs and both projects. +- **Devcontainer** uses a named volume (`trx-rs-sccache`). +- Check effectiveness with `sccache --show-stats` (the CI jobs print it). + +`CARGO_INCREMENTAL=0` is set wherever sccache is on, since sccache cannot cache +incremental artifacts. + ## CI runner (Alpine / OpenRC) The runner uses the **Docker executor** (not the host executor): per-job diff --git a/container/runner-config.example.yaml b/container/runner-config.example.yaml index 03f6cb76..5d49bf8e 100644 --- a/container/runner-config.example.yaml +++ b/container/runner-config.example.yaml @@ -27,7 +27,9 @@ cache: container: # Cap every job container's CPU so CI stays within the 2-thread budget even - # if capacity is raised later. - options: "--cpus=2" + # if capacity is raised later. The -v mount persists the sccache cache on the + # host (create it first: `mkdir -p /var/cache/sccache`), matching SCCACHE_DIR + # in the workflow. + options: "--cpus=2 -v /var/cache/sccache:/sccache" # Reuse the host VM's Docker network for the built-in cache/artifact server. network: "host" -- 2.55.0 From 22ff1349f313f107ad62f71c05565e73afa37b3f Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 11:33:02 +0200 Subject: [PATCH 5/8] [chore](trx-rs): run the frontend job in the SDK image The frontend job was added while CI still targeted host-executor runners, so it never gained the `container:` key the lint and test jobs use. On the Docker executor it lands on a bare job container and fails the same way the Rust jobs did before this branch: `npm` is missing, the Chromium install shells out to `sudo apt-get`, and `npm run verify-generated` regenerates the Rust wire contracts, so it needs `cargo` too. Run it in the SDK image, which already ships Node.js, Chromium at the path the browser smoke test defaults to, and the pinned Rust toolchain. Installing Chromium per run is then redundant. Drop the job's trailing `reuse lint`. The SDK image deliberately carries nothing REUSE-related, and the separate `reuse` job lints the whole repository with the upstream action, generated assets included. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- .gitea/workflows/ci.yml | 17 ++++++++--------- container/README.md | 12 +++++++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index be432c25..89c07d8f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,10 +2,11 @@ # # SPDX-License-Identifier: GPL-2.0-or-later -# 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. +# CI for the Docker-executor runner (VM). The lint, test and frontend jobs run +# inside the shared trx-rs SDK image (container/Containerfile), which bakes in +# the pinned Rust toolchain, Node.js, Chromium and all build dependencies. The +# reuse job uses the upstream Docker action, which the Docker executor launches +# as a sibling container. name: CI @@ -53,6 +54,7 @@ jobs: frontend: runs-on: ubuntu-latest + container: git.haxx.space/sjg/trx-rs/sdk:latest defaults: run: working-directory: src/trx-client/trx-frontend/trx-frontend-http/frontend @@ -73,15 +75,12 @@ jobs: run: npm run lint - name: Test run: npm test - - name: Install browser smoke dependency - run: command -v chromium >/dev/null || (sudo apt-get update && sudo apt-get install -y --no-install-recommends chromium) + # Chromium comes from the SDK image at the path the smoke test defaults + # to, so there is nothing to install here. - name: Browser smoke test run: npm run test:browser - name: Verify generated assets run: npm run verify-generated - - name: Verify generated-file licensing - working-directory: . - run: reuse lint reuse: runs-on: ubuntu-latest diff --git a/container/README.md b/container/README.md index dac690f9..76c8cf91 100644 --- a/container/README.md +++ b/container/README.md @@ -48,11 +48,17 @@ Building outside the container? `rust-toolchain.toml` pins the same rustc, so ## CI use -`.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`. +`.gitea/workflows/ci.yml` runs the `lint`, `test` and `frontend` jobs *inside* +this image via the `container:` key, so they skip all setup and go straight to +`cargo` and `npm`. The frontend job needs three things from the image beyond +Rust: Node.js for the toolchain, Chromium at `/usr/bin/chromium` for the +browser smoke test, and `cargo` — `npm run verify-generated` regenerates the +Rust wire contracts before checking for drift. + 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. +baked into the SDK, and it lints the whole repository, so no job runs its own +licence check. ## Compilation cache (sccache) -- 2.55.0 From c2455bb08c92015ea8420b5ba538cad119213a32 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 12:14:44 +0200 Subject: [PATCH 6/8] [chore](trx-rs): build the SDK image natively on x86_64 and arm64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sccache release asset is per-architecture and the Containerfile hardcoded the x86_64 triple, so an arm64 build produced an image whose sccache binary could not execute. Everything else in the image — the Debian base, the build dependencies, Node.js and rustup — already resolves per architecture, so that one URL was what pinned the image to amd64 and forced Rosetta or qemu on Apple Silicon. Resolve the triple from `uname -m`, which reflects the build platform under plain docker/podman build as well as buildx, unlike the BuildKit-only TARGETARCH. Document publishing `:latest` as a manifest list built natively on a host of each architecture, since a single-architecture tag sends the other side back to emulation, and note that Apple's `container` CLI needs Rosetta for its BuildKit helper VM regardless of the target. Pick the act_runner download by architecture for the same reason. Verified on arm64: the case arm selects sccache-v0.8.2-aarch64-unknown-linux-musl, and the installed binary reports `sccache 0.8.2` running natively. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- container/Containerfile | 21 ++++++++++++++---- container/README.md | 47 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/container/Containerfile b/container/Containerfile index 4634b52f..740b0417 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -46,10 +46,23 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ # sccache — shared compilation cache. Enabled at build time via # RUSTC_WRAPPER (see the CI workflow and .devcontainer), not repo-wide, so # non-SDK builds are unaffected. musl build is static and runs anywhere. +# +# The release asset is per-architecture, so resolve it from `uname -m` rather +# than hardcoding one triple: everything else in this image is arch-agnostic, +# and a pinned x86_64 URL is what forces an amd64 build (and Rosetta or qemu) +# on an arm64 host. `uname -m` reflects the build platform under plain +# docker/podman build as well as buildx, unlike the BuildKit-only TARGETARCH. ARG SCCACHE_VERSION=0.8.2 -RUN curl -fsSL "https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ - | tar -xz -C /tmp \ - && install -m755 "/tmp/sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache" /usr/local/bin/sccache \ - && rm -rf /tmp/sccache-* +RUN set -eux; \ + case "$(uname -m)" in \ + x86_64) sccache_arch=x86_64 ;; \ + aarch64|arm64) sccache_arch=aarch64 ;; \ + *) echo "unsupported architecture for sccache: $(uname -m)" >&2; exit 1 ;; \ + esac; \ + sccache_dist="sccache-v${SCCACHE_VERSION}-${sccache_arch}-unknown-linux-musl"; \ + curl -fsSL "https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/${sccache_dist}.tar.gz" \ + | tar -xz -C /tmp; \ + install -m755 "/tmp/${sccache_dist}/sccache" /usr/local/bin/sccache; \ + rm -rf /tmp/sccache-* WORKDIR /work diff --git a/container/README.md b/container/README.md index 76c8cf91..fce6d67f 100644 --- a/container/README.md +++ b/container/README.md @@ -17,6 +17,13 @@ and `clippy` are identical everywhere — no "works on my machine". ## Build and publish +Nothing in the image is architecture-specific: the base image, the Debian build +dependencies, Node.js, `rustup` and the `sccache` release all resolve per +architecture, so the same `Containerfile` builds natively on x86_64 and arm64. + +Single architecture — the tag then only works on the architecture you built it +on: + ```bash # from the repo root podman build -t git.haxx.space/sjg/trx-rs/sdk:latest container @@ -24,12 +31,48 @@ podman login git.haxx.space podman push git.haxx.space/sjg/trx-rs/sdk:latest ``` +**Both architectures without emulation.** The CI runner is x86_64 and Apple +Silicon developer machines are arm64, so `:latest` has to be a manifest list — +a single-architecture tag makes the other side fall back to Rosetta or qemu. +Build each half natively on a host of that architecture, then join them: + +```bash +# on an x86_64 host +podman build --platform linux/amd64 -t git.haxx.space/sjg/trx-rs/sdk:latest-amd64 container +podman push git.haxx.space/sjg/trx-rs/sdk:latest-amd64 + +# on an arm64 host +podman build --platform linux/arm64 -t git.haxx.space/sjg/trx-rs/sdk:latest-arm64 container +podman push git.haxx.space/sjg/trx-rs/sdk:latest-arm64 + +# from either, once both are pushed +podman manifest create git.haxx.space/sjg/trx-rs/sdk:latest \ + git.haxx.space/sjg/trx-rs/sdk:latest-amd64 \ + git.haxx.space/sjg/trx-rs/sdk:latest-arm64 +podman manifest push --all git.haxx.space/sjg/trx-rs/sdk:latest +``` + +Building both from one machine is a single command +(`podman build --platform linux/amd64,linux/arm64 --manifest ...`), but the +foreign half runs under emulation and is slow — the two-host flow above is +what keeps every build native. + 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. +### macOS note + +Apple's `container` CLI builds through a BuildKit helper VM that is configured +with Rosetta whether or not the target is x86_64, so `container build` fails +with *"Rosetta is not installed"* on a clean machine. That is a property of the +builder, not of this image — `container run` works natively without it. Either +install Rosetta once (`softwareupdate --install-rosetta`, after which an arm64 +build still produces a native arm64 image), or build with Podman, whose arm64 +BuildKit needs no emulation. + ## Developer use Reproducible one-off build, no local toolchain needed: @@ -111,8 +154,10 @@ adduser -S -D -H -h /var/lib/act_runner act addgroup act docker # 2. act_runner binary (static Go build, works on musl) +# Upstream publishes per-architecture builds; pick the host's. +case "$(uname -m)" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac curl -fsSL -o /usr/local/bin/act_runner \ - https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64 + "https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-${arch}" chmod +x /usr/local/bin/act_runner # 3. Config + register one runner per project (scope keeps their jobs apart) -- 2.55.0 From 26b00608b29a395a01b3450e04a9d4be5e140b27 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 16:49:46 +0200 Subject: [PATCH 7/8] [chore](trx-rs): force-pull the SDK image on the CI runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow references the SDK image by the moving `:latest` tag, and act_runner skips the pull when a local copy of that tag already exists: the job log reports `docker pull ... forcePull=false` followed by `Image exists? true`. Pushing a rebuilt image therefore changes nothing until someone pulls on the VM by hand, and the run fails as though the image never gained the tool that was added to the Containerfile — `sccache` resolving as "No such file or directory" while the pinned toolchain from an earlier build of the same tag resolves fine. Set `force_pull: true` so a pushed image is what actually runs, and document the manual refresh for runners configured before this change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- container/README.md | 12 ++++++++++++ container/runner-config.example.yaml | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/container/README.md b/container/README.md index fce6d67f..0ccca5c4 100644 --- a/container/README.md +++ b/container/README.md @@ -63,6 +63,18 @@ 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. +Pushing a rebuilt image is not enough on its own: `:latest` is a moving tag, and +act_runner reuses whatever it cached the first time unless `force_pull: true` is +set (see `runner-config.example.yaml`). Without it the job log says +`Image exists? true` and the run behaves as though the image were never +rebuilt — a tool added to the `Containerfile` reads as missing from the image. +Either set `force_pull`, or refresh the VM's copy by hand: + +```bash +docker pull git.haxx.space/sjg/trx-rs/sdk:latest +docker run --rm git.haxx.space/sjg/trx-rs/sdk:latest sccache --version +``` + ### macOS note Apple's `container` CLI builds through a BuildKit helper VM that is configured diff --git a/container/runner-config.example.yaml b/container/runner-config.example.yaml index 5d49bf8e..ece17576 100644 --- a/container/runner-config.example.yaml +++ b/container/runner-config.example.yaml @@ -33,3 +33,9 @@ container: options: "--cpus=2 -v /var/cache/sccache:/sccache" # Reuse the host VM's Docker network for the built-in cache/artifact server. network: "host" + # The workflow pulls the SDK image by the moving `:latest` tag. Without this + # the runner logs "Image exists? true" and reuses whatever it cached the + # first time, so pushing a rebuilt image has no effect until someone pulls + # on the VM by hand — which looks like the image is missing a tool it in + # fact has. The extra registry round-trip per job is nothing next to a build. + force_pull: true -- 2.55.0 From 2f4973ed704941091b6b909662639b7e29e3f2ab Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 16:54:16 +0200 Subject: [PATCH 8/8] [chore](trx-rs): allow the sccache bind mount on the CI runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit act_runner validates every bind mount against `valid_volumes`, which defaults to an empty allowlist, so the `-v /var/cache/sccache:/sccache` in `container.options` was dropped on every job. The only trace is one line in the job log — "[/var/cache/sccache] is not a valid volume, will be ignored" — after which SCCACHE_DIR points at a path that does not outlive the container, so the shared compilation cache never persisted. Allow that one path rather than the `**` wildcard: the runner is the only thing mounting host directories here, and a narrow allowlist keeps a workflow from mounting arbitrary host paths into a job container. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- container/runner-config.example.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/container/runner-config.example.yaml b/container/runner-config.example.yaml index ece17576..86d78bd5 100644 --- a/container/runner-config.example.yaml +++ b/container/runner-config.example.yaml @@ -31,6 +31,12 @@ container: # host (create it first: `mkdir -p /var/cache/sccache`), matching SCCACHE_DIR # in the workflow. options: "--cpus=2 -v /var/cache/sccache:/sccache" + # act_runner rejects every bind mount unless it is listed here — the default + # is an empty allowlist, so the -v above is dropped with only a + # "[...] is not a valid volume, will be ignored" line in the job log, and + # SCCACHE_DIR then points at a directory that does not outlive the job. + valid_volumes: + - /var/cache/sccache # Reuse the host VM's Docker network for the built-in cache/artifact server. network: "host" # The workflow pulls the SDK image by the moving `:latest` tag. Without this -- 2.55.0