From 623fe576836ad6622deb255ab621969cf4f429d0 Mon Sep 17 00:00:00 2001 From: Amadey Vorontsov Date: Mon, 18 May 2026 10:13:17 +0000 Subject: [PATCH] runner: reproducible nix-built job image + compose-based act_runner --- .gitea/workflows/auto-merge.yml | 19 ------- .gitea/workflows/validate-pr.yml | 30 +++------- runner/.gitignore | 4 ++ runner/README.md | 71 +++++++++++++++++++++++ runner/compose.yml | 20 +++++++ runner/config.yaml | 28 ++++++++++ runner/flake.nix | 96 ++++++++++++++++++++++++++++++++ 7 files changed, 226 insertions(+), 42 deletions(-) delete mode 100644 .gitea/workflows/auto-merge.yml create mode 100644 runner/.gitignore create mode 100644 runner/README.md create mode 100644 runner/compose.yml create mode 100644 runner/config.yaml create mode 100644 runner/flake.nix diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml deleted file mode 100644 index 093907f..0000000 --- a/.gitea/workflows/auto-merge.yml +++ /dev/null @@ -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 }}" diff --git a/.gitea/workflows/validate-pr.yml b/.gitea/workflows/validate-pr.yml index abad671..14f60ec 100644 --- a/.gitea/workflows/validate-pr.yml +++ b/.gitea/workflows/validate-pr.yml @@ -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//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 diff --git a/runner/.gitignore b/runner/.gitignore new file mode 100644 index 0000000..542bdc9 --- /dev/null +++ b/runner/.gitignore @@ -0,0 +1,4 @@ +.env +data/ +flake.lock +result diff --git a/runner/README.md b/runner/README.md new file mode 100644 index 0000000..6c9bfca --- /dev/null +++ b/runner/README.md @@ -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= + 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. diff --git a/runner/compose.yml b/runner/compose.yml new file mode 100644 index 0000000..a4b0ab8 --- /dev/null +++ b/runner/compose.yml @@ -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 diff --git a/runner/config.yaml b/runner/config.yaml new file mode 100644 index 0000000..c1f68ca --- /dev/null +++ b/runner/config.yaml @@ -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: "" diff --git a/runner/flake.nix b/runner/flake.nix new file mode 100644 index 0000000..af46c70 --- /dev/null +++ b/runner/flake.nix @@ -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" + ''); + }; + }); +}