runner: reproducible nix-built job image + compose-based act_runner

This commit is contained in:
2026-05-18 10:13:17 +00:00
parent f8a041f5b7
commit 623fe57683
7 changed files with 226 additions and 42 deletions

View File

@@ -1,19 +0,0 @@
name: auto-merge
on:
pull_request:
types: [labeled]
jobs:
merge:
if: github.event.label.name == 'auto-merge'
runs-on: self-hosted
steps:
- name: merge
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
tea pr merge \
--repo "${{ github.repository }}" \
--style squash \
"${{ github.event.pull_request.number }}"

View File

@@ -62,7 +62,9 @@ jobs:
done
done
# 4. Build smoke — every changed package must build.
# 4. Build smoke — every changed package must build. Cache push
# is intentionally absent for now (no shared binary cache);
# add a step here once cache infra is decided.
- name: build smoke
if: steps.changed.outputs.packages != ''
run: |
@@ -71,20 +73,9 @@ jobs:
.#${pkg} --no-link --print-out-paths
done
# 5. Cache push (only on the validated outputs, before merge).
- name: push to binary cache
if: steps.changed.outputs.packages != ''
env:
NIX_SECRET_KEY_FILE: ${{ secrets.NIX_CACHE_SECRET_KEY_FILE }}
CACHE_URL: ${{ vars.CARGOXX_CACHE_URL }}
run: |
for pkg in ${{ steps.changed.outputs.packages }}; do
nix copy --extra-experimental-features 'nix-command flakes' \
--to "${CACHE_URL}?secret-key=${NIX_SECRET_KEY_FILE}" \
.#${pkg}
done
# 6. Maintainer match.
# 5. Maintainer check — PR must come from someone listed in
# recipes/<pkg>/maintainers.txt (auto-pass for new packages,
# since the PR introduces the file in the same commit).
- name: maintainer check
if: steps.changed.outputs.packages != ''
run: |
@@ -92,18 +83,11 @@ jobs:
for pkg in ${{ steps.changed.outputs.packages }}; do
list="recipes/$pkg/maintainers.txt"
if [[ ! -f "$list" ]]; then
echo "new package $pkg — maintainers.txt will be added by this PR"
echo "new package $pkg — maintainers.txt added by this PR"
continue
fi
if ! grep -E -q "^\s*${author}\s*(\#.*)?$" "$list"; then
echo "PR author '$author' is not in $list"
gh pr edit ${{ github.event.pull_request.number }} \
--add-label needs-human-review
exit 1
fi
done
- name: label auto-merge
if: steps.changed.outputs.packages != ''
run: |
gh pr edit ${{ github.event.pull_request.number }} --add-label auto-merge

4
runner/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.env
data/
flake.lock
result

71
runner/README.md Normal file
View File

@@ -0,0 +1,71 @@
# 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=<paste here>
GITEA_RUNNER_NAME=cargoxx-pkgs-runner
GITEA_RUNNER_LABELS=self-hosted
```
4. **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.

20
runner/compose.yml Normal file
View File

@@ -0,0 +1,20 @@
# Runs the act_runner that listens to Gitea and spawns one job
# container per workflow run. The job image (cargoxx-runner-job:latest)
# is built reproducibly from runner/flake.nix — run `nix run .#load-image`
# in this directory to load it into the host's Docker daemon before
# starting the runner.
version: "3.8"
services:
runner:
image: docker.io/gitea/act_runner:nightly
restart: unless-stopped
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "${GITEA_INSTANCE_URL}"
GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN}"
GITEA_RUNNER_NAME: "${GITEA_RUNNER_NAME:-cargoxx-pkgs-runner}"
GITEA_RUNNER_LABELS: "${GITEA_RUNNER_LABELS:-self-hosted}"
volumes:
- ./config.yaml:/config.yaml:ro
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock

28
runner/config.yaml Normal file
View File

@@ -0,0 +1,28 @@
# act_runner config. The `runner.labels` mapping says: when a workflow
# requests `runs-on: self-hosted`, spawn the cargoxx-runner-job:latest
# image (built from runner/flake.nix). Other labels can be added by
# building additional images and listing them here.
log:
level: info
runner:
file: .runner
capacity: 1
envs: {}
labels:
- "self-hosted:docker://cargoxx-runner-job:latest"
cache:
enabled: false
container:
network: bridge
privileged: false
options: ""
workdir_parent: /workspace
valid_volumes: []
docker_host: "unix:///var/run/docker.sock"
force_pull: false
host:
workdir_parent: ""

96
runner/flake.nix Normal file
View File

@@ -0,0 +1,96 @@
{
description = "OCI image for cargoxx-pkgs CI jobs: nix + tea + git + jq";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
# Single-user nix config — same defaults used by the cargoxx
# distribution wrapper. Avoids the multi-user nixbld group
# requirement; sandbox disabled because the runner container
# itself doesn't usually have user-namespace support.
nixConfig = ''
experimental-features = nix-command flakes
build-users-group =
sandbox = false
accept-flake-config = true
'';
in {
packages.default = pkgs.dockerTools.buildLayeredImage {
name = "cargoxx-runner-job";
tag = "latest";
contents = with pkgs; [
bashInteractive
coreutils
findutils
gawk
gnugrep
gnused
gnutar
gzip
xz
nix
git
curl
jq
tea
cacert
iana-etc
];
# Skeleton filesystem layout: /tmp, /etc/passwd for nix,
# writable nix store, cacert pointer.
extraCommands = ''
mkdir -p tmp etc nix/var/{nix,log/nix} root
chmod 1777 tmp
cat > etc/passwd <<'EOF'
root:x:0:0:root:/root:/bin/bash
nobody:x:65534:65534:nobody:/var/empty:/bin/false
EOF
cat > etc/group <<'EOF'
root:x:0:
nobody:x:65534:
EOF
cat > etc/nix/nix.conf <<'EOF'
${nixConfig}
EOF
'';
config = {
Env = [
"PATH=/bin:/usr/bin"
"NIX_CONFIG=${nixConfig}"
"NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
"SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
"HOME=/root"
"USER=root"
];
Cmd = [ "/bin/bash" ];
WorkingDir = "/root";
};
};
# `nix run .#load-image` builds the image and pipes it into the
# local Docker daemon — no registry needed for single-host
# deployments.
apps.load-image = {
type = "app";
program = toString (pkgs.writeShellScript "load-image" ''
set -euo pipefail
img=$(nix build --no-link --print-out-paths .#default)
echo "loading $img into docker"
${pkgs.docker}/bin/docker load < "$img"
'');
};
});
}