Files
nixpkgs/nixos/tests/github-runner.nix
Samuel Silva e6ed17b918 nixos/github-runners: GitHub App authentication and multi-org runners
Add three options to `services.github-runners.<name>`:

- `githubApp`: authenticate via a GitHub App installation instead of a
  `tokenFile`. The service derives a short-lived installation token from the
  App's private key on each start, registers the runner, and de-registers it
  on stop. The PEM key is a deployed secret, never copied into the store.

- `count`: fan a single entry out into N identical runner services.
  `github-runner-<name>` is unchanged for `count == 1`, suffixed `-<n>` for
  `count > 1`.

- `orgs`: serve several organisations (or repositories) from one entry,
  fanning out into `github-runner-<name>-<org>-<n>`. The entry-level auth is
  shared across orgs and only the App `login` changes per org; the per-org
  `count` defaults to the entry-level `count`.

This brings the multi-org consumption shape of the soon-to-be-removed srvos
runner role (nix-community/srvos#836, nix-community/srvos#837) into nixpkgs.
An entry without `orgs` and with the default `count = 1` is unchanged, so
existing runners do not re-register.
2026-06-19 15:21:54 -03:00

122 lines
4.4 KiB
Nix

{ pkgs, ... }:
{
name = "github-runner";
meta = with pkgs.lib.maintainers; {
maintainers = [ veehaitch ];
};
nodes.machine =
{ pkgs, ... }:
let
appPrivateKey = pkgs.runCommand "github-app.pem" {
nativeBuildInputs = [ pkgs.openssl ];
} "openssl genrsa -out $out 2048";
in
{
services.github-runners.test = {
enable = true;
url = "https://github.com/yaxitech";
tokenFile = builtins.toFile "github-runner.token" "not-so-secret";
};
services.github-runners.test-disabled = {
enable = false;
url = "https://github.com/yaxitech";
tokenFile = builtins.toFile "github-runner.token" "not-so-secret";
};
# Runner authenticated via a GitHub App installation. This exercises the
# module evaluation and the App authentication code path up to the point it
# contacts the (stubbed) GitHub API; a successful registration would
# require talking to the real API.
services.github-runners.test-app = {
enable = true;
url = "https://github.com/yaxitech";
githubApp = {
id = 123456;
login = "yaxitech";
privateKeyFile = appPrivateKey;
};
};
# A single org/repo entry with `count > 1` fans out into one systemd
# service per replica, named `github-runner-<name>-<n>`.
services.github-runners.test-replicas = {
enable = true;
url = "https://github.com/yaxitech";
tokenFile = builtins.toFile "github-runner.token" "not-so-secret";
count = 2;
};
# A single entry with `orgs` fans out into one systemd service per org and
# per replica, named `github-runner-<name>-<org>-<n>`.
services.github-runners.test-orgs = {
enable = true;
tokenFile = builtins.toFile "github-runner.token" "not-so-secret";
orgs = {
yaxitech.count = 2;
another = { };
};
};
# `orgs` combined with a shared GitHub App: the same App is used for every
# org and only the per-org `login` changes (derived from the attribute
# name), so no entry-level `githubApp.login` is needed.
services.github-runners.test-orgs-app = {
enable = true;
githubApp = {
id = 123456;
privateKeyFile = appPrivateKey;
};
orgs = {
yaxitech = { };
"another-org" = { };
};
};
systemd.services.dummy-github-com = {
wantedBy = [ "multi-user.target" ];
before = [ "github-runner-test.service" ];
script = "${pkgs.netcat}/bin/nc -Fl 443 | true && touch /tmp/registration-connect";
};
networking.hosts."127.0.0.1" = [ "api.github.com" ];
};
testScript = ''
start_all()
machine.wait_for_unit("dummy-github-com")
try:
machine.wait_for_unit("github-runner-test")
except Exception:
pass
out = machine.succeed("journalctl -u github-runner-test")
assert "Self-hosted runner registration" in out, "did not read runner registration header"
machine.wait_until_succeeds("test -f /tmp/registration-connect")
# The GitHub App runner unit is generated and wired to the App auth path.
machine.succeed("systemctl cat github-runner-test-app.service | grep -F unconfigure-github-app")
# `count > 1` on a single entry fans out into one unit per replica with a
# `-<n>` suffix; there is no bare unit.
machine.succeed("systemctl cat github-runner-test-replicas-1.service")
machine.succeed("systemctl cat github-runner-test-replicas-2.service")
machine.fail("systemctl cat github-runner-test-replicas.service")
# `orgs` fans out into one unit per org and replica; there is no bare unit.
machine.succeed("systemctl cat github-runner-test-orgs-yaxitech-1.service")
machine.succeed("systemctl cat github-runner-test-orgs-yaxitech-2.service")
machine.succeed("systemctl cat github-runner-test-orgs-another-1.service")
machine.fail("systemctl cat github-runner-test-orgs.service")
# `orgs` with a shared GitHub App generates one App-authenticated unit per
# org, each with its per-org login derived from the attribute name.
machine.succeed("systemctl cat github-runner-test-orgs-app-yaxitech-1.service | grep -F unconfigure-github-app")
machine.succeed("systemctl cat github-runner-test-orgs-app-another-org-1.service | grep -F unconfigure-github-app")
machine.fail("systemctl list-unit-files | grep test-disabled")
'';
}