{ pkgs, lib, config, ... }: let cfg = config.services.hyperhive.ci; forgeCfg = config.services.hyperhive.forge; in { # Forgejo Actions runner in a `hive-ci` nixos-container. # Shares host netns (same as hive-forge), so the runner reaches # the forge at `http://127.0.0.1:` without extra plumbing. # Container is non-ephemeral: the runner's registered credentials # survive restarts (gitea-actions-runner writes them to its stateDir # on first registration and reuses them on every subsequent start). # # 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 { type = lib.types.bool; default = false; 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`. ''; }; 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. ''; }; name = lib.mkOption { type = lib.types.str; default = "hive-ci"; example = "my-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. ''; }; concurrency = lib.mkOption { type = lib.types.ints.positive; default = 1; example = 4; description = '' Maximum number of workflow jobs the runner executes in parallel. Each job gets its own temporary working directory; multiple parallel jobs share the container's nix store and cargo registry cache. Higher values trade memory + CPU headroom for throughput. ''; }; labels = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "hive-ci:host" "ubuntu-latest:host" "ubuntu-22.04:host" ]; example = [ "hive-ci:host" "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). 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. ''; }; package = lib.mkOption { 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). ''; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = forgeCfg.enable; 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. ''; } ]; 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. 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; }; config = { pkgs, lib, ... }: { 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. nix.settings.sandbox-fallback = lib.mkForce true; # Flakes + nix-command needed by workflow steps. nix.settings.experimental-features = [ "nix-command" "flakes" ]; 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}"; tokenFile = "/run/hive-ci/runner-token"; labels = cfg.labels; settings = { runner.capacity = cfg.concurrency; # Generous timeout for nix builds that may be cold-cache. 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. environment.systemPackages = [ pkgs.git pkgs.nix pkgs.cargo pkgs.rustc pkgs.rustfmt pkgs.clippy ]; }; }; }; }