runner: reproducible nix-built job image + compose-based act_runner
This commit is contained in:
@@ -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 }}"
|
|
||||||
@@ -62,7 +62,9 @@ jobs:
|
|||||||
done
|
done
|
||||||
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
|
- name: build smoke
|
||||||
if: steps.changed.outputs.packages != ''
|
if: steps.changed.outputs.packages != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -71,20 +73,9 @@ jobs:
|
|||||||
.#${pkg} --no-link --print-out-paths
|
.#${pkg} --no-link --print-out-paths
|
||||||
done
|
done
|
||||||
|
|
||||||
# 5. Cache push (only on the validated outputs, before merge).
|
# 5. Maintainer check — PR must come from someone listed in
|
||||||
- name: push to binary cache
|
# recipes/<pkg>/maintainers.txt (auto-pass for new packages,
|
||||||
if: steps.changed.outputs.packages != ''
|
# since the PR introduces the file in the same commit).
|
||||||
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.
|
|
||||||
- name: maintainer check
|
- name: maintainer check
|
||||||
if: steps.changed.outputs.packages != ''
|
if: steps.changed.outputs.packages != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -92,18 +83,11 @@ jobs:
|
|||||||
for pkg in ${{ steps.changed.outputs.packages }}; do
|
for pkg in ${{ steps.changed.outputs.packages }}; do
|
||||||
list="recipes/$pkg/maintainers.txt"
|
list="recipes/$pkg/maintainers.txt"
|
||||||
if [[ ! -f "$list" ]]; then
|
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
|
continue
|
||||||
fi
|
fi
|
||||||
if ! grep -E -q "^\s*${author}\s*(\#.*)?$" "$list"; then
|
if ! grep -E -q "^\s*${author}\s*(\#.*)?$" "$list"; then
|
||||||
echo "PR author '$author' is not in $list"
|
echo "PR author '$author' is not in $list"
|
||||||
gh pr edit ${{ github.event.pull_request.number }} \
|
|
||||||
--add-label needs-human-review
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
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
4
runner/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
data/
|
||||||
|
flake.lock
|
||||||
|
result
|
||||||
71
runner/README.md
Normal file
71
runner/README.md
Normal 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
20
runner/compose.yml
Normal 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
28
runner/config.yaml
Normal 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
96
runner/flake.nix
Normal 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"
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user