diff --git a/nix/modules/hive-ci.nix b/nix/modules/hive-ci.nix index 04925031..d62cef83 100644 --- a/nix/modules/hive-ci.nix +++ b/nix/modules/hive-ci.nix @@ -7,6 +7,54 @@ let cfg = config.services.hyperhive.ci; forgeCfg = config.services.hyperhive.forge; + + # hive-c0re writes its own admin token here on first forge startup. + # The token has read:admin + write:admin scopes — sufficient to call + # the runner registration-token API endpoint. + coreTokenPath = "/var/lib/hyperhive/forge-core-token"; + + # Script run before the gitea-runner-hive service starts. + # On first boot (no .runner credentials yet) it fetches a fresh + # runner registration token from the forge admin API and writes it to + # /run/hive-ci/runner-token so gitea-actions-runner can register. + # On subsequent boots the .runner credentials file already exists and + # the runner ignores the token file entirely, so we write a dummy to + # satisfy the file-existence check in the NixOS module. + autoRegisterScript = pkgs.writeShellScript "hive-ci-autoregister" '' + set -euo pipefail + TOKEN_FILE=/run/hive-ci/runner-token + STATE_FILE=/var/lib/gitea-runner/hive/.runner + + mkdir -p /run/hive-ci + chmod 700 /run/hive-ci + + if [ -f "$STATE_FILE" ]; then + # Already registered — dummy token satisfies the module's path check. + echo "already-registered" > "$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + exit 0 + fi + + CORE_TOKEN=$(cat /run/hive-ci/core-token) + FORGE_URL="http://127.0.0.1:${toString forgeCfg.httpPort}" + + # Retry up to 30s for forge to come up (containers autoStart in parallel). + for i in $(seq 1 30); do + REG_TOKEN=$(${pkgs.curl}/bin/curl -sf \ + "$FORGE_URL/api/v1/admin/runners/registration-token" \ + -H "Authorization: token $CORE_TOKEN" \ + | ${pkgs.jq}/bin/jq -r .token) && break + sleep 1 + done + + if [ -z "''${REG_TOKEN:-}" ] || [ "$REG_TOKEN" = "null" ]; then + echo "hive-ci: failed to fetch runner registration token from forge" >&2 + exit 1 + fi + + echo "$REG_TOKEN" > "$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + ''; in { # Forgejo Actions runner in a `hive-ci` nixos-container. @@ -16,16 +64,16 @@ in # survive restarts (gitea-actions-runner writes them to its stateDir # on first registration and reuses them on every subsequent start). # + # Auto-registration: on first boot the container fetches a runner + # registration token from the forge's admin API using the core token + # hive-c0re writes to /var/lib/hyperhive/forge-core-token. No manual + # token handling needed — `ci.enable = true` is the full operator + # bootstrap. See docs/ci.md for the registration flow. + # # Nix builds inside the container use the shared /nix/store (standard # nixos-container behaviour) with sandbox-fallback = true, because # nspawn containers can't create the user-namespaces that nix sandboxing # requires. See docs/gotchas.md. - # - # Operator bootstrap: generate a runner registration token in Forgejo - # (Site Administration → Runners → Registration Token), store it in - # a secrets file on the host, and point `runnerTokenFile` at it. - # The token is consumed on first start; the runner's persistent - # credentials live in the container state dir afterwards. options.services.hyperhive.ci = { enable = lib.mkOption { @@ -34,36 +82,22 @@ in example = true; description = '' Run a Forgejo Actions runner in a `hive-ci` nixos-container. - Disabled by default; requires `services.hyperhive.forge.enable = true` - (the runner registers against the hive-forge instance) and a - registration token at `runnerTokenFile`. - ''; - }; + Disabled by default; requires `services.hyperhive.forge.enable = true`. - runnerTokenFile = lib.mkOption { - type = lib.types.path; - example = "/run/secrets/ci-runner-token"; - description = '' - Host path to a file containing the Forgejo runner registration - token (one token per line, no trailing whitespace). Obtain it - from Forgejo: Site Administration → Runners → Registration Token. - - The file is bind-mounted read-only into the container and consumed - on first start. After registration the runner's actual credentials - are persisted in the container's state dir; the token file can be - deleted or revoked from Forgejo without affecting the running - runner. + On first start the container auto-registers against hive-forge using + hive-c0re's admin token — no manual token provisioning needed. + Runner credentials are persisted in the container's state dir and + reused on every subsequent boot. ''; }; name = lib.mkOption { type = lib.types.str; default = "hive-ci"; - example = "my-hive"; + example = "prod-hive"; description = '' - Runner name as shown in the Forgejo admin panel. - Defaults to "hive-ci"; override when multiple hives share a - Forgejo instance to keep them distinct. + Runner name as shown in the Forgejo admin panel. Defaults to + "hive-ci"; override when multiple hives share a Forgejo instance. ''; }; @@ -91,16 +125,13 @@ in "nix:host" ]; description = '' - Runner labels. Each entry has the shape `:`. The - `host` scheme means the runner executes commands directly on the - container (no docker/podman). Workflow files target this runner - with `runs-on: [hive-ci]` (or whichever label the operator picks). + Runner labels in `:` format. The `host` scheme runs + commands directly in the container (no docker/podman). Workflow + files target this runner with `runs-on: [hive-ci]`. - The `ubuntu-latest` and `ubuntu-22.04` aliases let upstream - workflow files that hardcode GitHub-style runner names work - unchanged — the host runner is a reasonable substitute for - CI steps that only need git + nix + cargo and don't depend on - Ubuntu-specific APT packages. + The `ubuntu-latest` and `ubuntu-22.04` aliases let workflow files + that hardcode GitHub-style runner names work unchanged, as long as + those jobs only need git + nix + cargo and not Ubuntu APT packages. ''; }; @@ -108,10 +139,7 @@ in type = lib.types.package; default = pkgs.gitea-actions-runner; defaultText = lib.literalExpression "pkgs.gitea-actions-runner"; - description = '' - gitea-actions-runner package. Defaults to `pkgs.gitea-actions-runner` - (the nixpkgs release tracking Forgejo's runner releases). - ''; + description = "gitea-actions-runner package."; }; }; @@ -122,8 +150,7 @@ in message = '' services.hyperhive.ci.enable = true requires services.hyperhive.forge.enable = true — the runner registers - against the hive-forge Forgejo instance. Either enable the forge - or leave ci.enable at its default of false. + against the hive-forge Forgejo instance. ''; } ]; @@ -131,15 +158,16 @@ in containers.hive-ci = { autoStart = true; ephemeral = false; - # Shared host netns: runner reaches hive-forge at localhost without - # any port-forwarding dance. Same pattern as hive-forge itself. + # Shared host netns: runner reaches hive-forge at localhost. privateNetwork = false; - # Bind the token file read-only into the container at a stable - # internal path the NixOS module option below references. - bindMounts."/run/hive-ci/runner-token" = { - hostPath = toString cfg.runnerTokenFile; - isReadOnly = true; + bindMounts = { + # Core token — used by the auto-register script on first boot. + # Read-only: the container only reads it, never modifies it. + "/run/hive-ci/core-token" = { + hostPath = coreTokenPath; + isReadOnly = true; + }; }; config = @@ -148,11 +176,9 @@ in system.stateVersion = "25.11"; # nspawn containers can't create user-namespaces, so nix - # sandboxing always fails. Fall back to unsandboxed builds - # rather than erroring out. See docs/gotchas.md. + # sandboxing always fails. Fall back to unsandboxed builds. + # See docs/gotchas.md. nix.settings.sandbox-fallback = lib.mkForce true; - - # Flakes + nix-command needed by workflow steps. nix.settings.experimental-features = [ "nix-command" "flakes" @@ -161,22 +187,28 @@ in services.gitea-actions-runner.instances.hive = { enable = true; name = cfg.name; - # Reach hive-forge on loopback — shared netns means this - # is always reachable regardless of firewall / DNS config. url = "http://127.0.0.1:${toString forgeCfg.httpPort}"; + # Token file is written by the ExecStartPre script below. + # On first boot: real registration token fetched from forge API. + # On subsequent boots: dummy value (runner uses .runner creds). tokenFile = "/run/hive-ci/runner-token"; labels = cfg.labels; settings = { runner.capacity = cfg.concurrency; - # Generous timeout for nix builds that may be cold-cache. + # Generous timeout for cold-cache nix builds. runner.timeout = "3h"; }; package = cfg.package; }; - # Tools available to workflow steps. Rust toolchain covers - # cargo test + cargo clippy. nix covers flake check + fmt. - # git is required by the runner and actions/checkout. + # Prepend auto-register script before the runner service starts. + # `+` prefix runs with elevated privileges so it can read the + # bind-mounted core-token (owned by root on the host). + systemd.services."gitea-runner-hive".serviceConfig.ExecStartPre = lib.mkBefore [ + "+${autoRegisterScript}" + ]; + + # Tools available to workflow steps. environment.systemPackages = [ pkgs.git pkgs.nix @@ -184,6 +216,8 @@ in pkgs.rustc pkgs.rustfmt pkgs.clippy + pkgs.curl + pkgs.jq ]; }; };