Files
cargoxx-pkgs/runner/README.md

3.8 KiB

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:

    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:

    GITEA_INSTANCE_URL=https://git.amadey.xyz
    GITEA_RUNNER_REGISTRATION_TOKEN=<paste here>
    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.

    mkdir -p cache/store
    nix-store --generate-binary-cache-key \
        cache.cargoxx.<your-domain> \
        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:

    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.<domain>, trusted-public-keys = <cache.pub>) 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:

    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.