# cargoxx-pkgs runner Self-hosted Gitea Actions runner that validates package PRs. ## Architecture ``` +----------------+ +-------------------+ +--------------------+ | Gitea instance | <----> | act_runner | -----> | job container | | (queues jobs) | poll | (this docker- | docker| cargoxx-runner- | | | | compose service) | spawn | job:latest | +----------------+ +-------------------+ +--------------------+ ``` - `compose.yml` runs the official `gitea/act_runner:nightly` image, which polls Gitea for jobs and spawns one container per workflow run via the host's Docker socket. - `flake.nix` builds the **job container image** — reproducible, declarative, ships `nix`, `git`, `curl`, `jq`, `tea`, and a single-user `NIX_CONFIG`. This image runs the actual workflow steps; act_runner just orchestrates it. - `config.yaml` maps `runs-on: self-hosted` (from `.gitea/workflows/*.yml`) to the job image. ## One-time setup 1. **Build + load the job image** into the host's Docker daemon: ```sh cd runner nix run --extra-experimental-features 'nix-command flakes' .#load-image ``` Re-run whenever you bump nixpkgs or change the tool list. 2. **Mint a runner registration token** in the Gitea UI: `Site Administration → Actions → Runners → Create new Runner`. Copy the token. 3. **Provision the `.env`** alongside `compose.yml`: ```env GITEA_INSTANCE_URL=https://git.amadey.xyz GITEA_RUNNER_REGISTRATION_TOKEN= GITEA_RUNNER_NAME=cargoxx-pkgs-runner GITEA_RUNNER_LABELS=self-hosted ``` 4. **Generate the binary-cache signing key** + cache directory. The workflow's "push to binary cache" step writes here; nginx (or anything you point at it) serves it back over HTTPS to consumers. ```sh mkdir -p cache/store nix-store --generate-binary-cache-key \ cache.cargoxx. \ cache/cache.sec cache/cache.pub chmod 600 cache/cache.sec ``` The `cache/` directory is gitignored. Both keys live alongside `compose.yml`; the named volume binds use `${PWD}/cache/...`. 5. **Pick the Caddy ports.** `compose.yml` runs Caddy alongside the runner to HTTPS-front the cache. Because the router does PAT, the *internal* ports Caddy listens on must equal whatever 80/443 are forwarded to. Add to `.env`: ```env CADDY_HTTP_PORT=8080 CADDY_HTTPS_PORT=8443 ``` Both compose.yml and the Caddyfile pick those up. The Caddyfile already targets `cache.cargoxx.amadey.xyz` and the e-mail `vorontsov@amadey.xyz`; edit if you're deploying somewhere else. ACME provisioning works as long as the router forwards 80 → CADDY_HTTP_PORT and 443 → CADDY_HTTPS_PORT, so Let's Encrypt's HTTP-01 challenge reaches Caddy. Consumers' substituter config (`substituters = https://cache.`, `trusted-public-keys = `) is baked into cargoxx's own wrapper (`cargoxx/flake.nix:cargoxxNixConfig`), so any installed `cargoxx` binary picks them up — no per-user setup needed. 6. **Start the runner**: ```sh docker compose up -d docker compose logs -f runner ``` First boot registers the runner with Gitea; subsequent boots reuse the persisted token in `./data/.runner`. ## Updating - Change a workflow step's tools → edit `flake.nix`, rerun `nix run .#load-image`. - Bump nixpkgs → `nix flake update` in this dir, rebuild the image. - act_runner itself updates by `docker compose pull && docker compose up -d`. ## Why not host mode? Host mode (act_runner running workflows directly on the host) would save the Docker indirection but tightly couples the runner host to the toolchain. Docker mode lets us treat the job image as a deployable artifact — same image on every runner, no drift.