Files
sjgandClaude Opus 5 2f4973ed70
CI / test (pull_request) Successful in 13m51s
CI / frontend (pull_request) Successful in 5m1s
CI / test (push) Successful in 7m43s
CI / frontend (push) Successful in 2m18s
CI / reuse (pull_request) Successful in 4s
CI / lint (pull_request) Successful in 4m22s
CI / lint (push) Successful in 2m23s
CI / reuse (push) Successful in 1m18s
[chore](trx-rs): allow the sccache bind mount on the CI runner
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-02 16:54:16 +02:00
..

trx-rs SDK image

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 The SDK image (Debian + build deps + pinned Rust + Node + git).
runner-config.example.yaml Example act_runner config for the CI VM (Docker executor).

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:

# 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

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:

# 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.

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:

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 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:

podman run --rm -it -v "$PWD":/work -w /work \
  git.haxx.space/sjg/trx-rs/sdk:latest \
  cargo build --release

Or open the repo in the image via VS Code / JetBrains "Reopen in Container" (.devcontainer/devcontainer.json points at the same image).

Building outside the container? rust-toolchain.toml pins the same rustc, so rustup installs the matching toolchain automatically.

CI use

.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 cargonpm 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, and it lints the whole repository, so no job runs its own licence check.

Compilation cache (sccache)

The SDK image ships 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 container isolation and standard ubuntu-latest semantics. act_runner runs as an OpenRC service. Files provided:

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.

Cap the thread budget. In a VM, pin its vCPUs to specific host threads (libvirt/KVM):

<vcpu placement='static'>2</vcpu>
<cputune>
  <vcpupin vcpu='0' cpuset='4'/>
  <vcpupin vcpu='1' cpuset='5'/>
</cputune>

On bare metal, the container.options: "--cpus=2" and capacity: 1 in runner-config.example.yaml already bound each runner.

Set it up:

# 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)
#    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-${arch}"
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 <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.