deploy: move the forge CI runner toggle out of swarm

Last of the swarm.* -> deploy.* moves for the enable-shaped options.
Whether this host also runs the CI runner is a per-machine decision, so
it was sitting in the namespace that has to be identical swarm-wide.

Renames all five options, not just enable: name, concurrency, labels and
package all describe the runner THIS host would run, and leaving them
behind would keep the original defect for four more options. One rename
entry each, because ci is a plain attrset rather than a submodule type,
so there is no parent path to rename in one go.

deploy.forgejo is the name deploy.nix's own header already specifies,
including this exact case -- "ci (does this host run the runner too) is
exactly that shape". It is the only entry with no enable: the forge is
not optional, it deploys with hyperhive itself, so running the runner is
the only deployment decision it has.

Also fixes hive-c0re/src/forge/ci_runner.rs, whose doc comment named
services.hyperhive.forge.ci.enable -- missing the swarm. segment, so it
had never resolved to a real option.
This commit is contained in:
atlas 2026-08-30 15:42:26 +02:00 committed by mara
commit 4423da4899
6 changed files with 47 additions and 12 deletions

View file

@ -5,7 +5,7 @@ executing CI jobs from `.forgejo/workflows/ci.yml` on every PR.
## For operators ## For operators
**Enabling it is one line**: `services.hyperhive.swarm.forge.ci.enable = true` **Enabling it is one line**: `services.hyperhive.deploy.forgejo.ci.enable = true`
in the host NixOS config. No manual token provisioning — hive-c0re in the host NixOS config. No manual token provisioning — hive-c0re
registers the runner with the forge automatically. registers the runner with the forge automatically.
@ -90,9 +90,9 @@ slow); run those manually before pushing Rust changes.
The internal forge is always present (mandatory), so the runner always has a The internal forge is always present (mandatory), so the runner always has a
hive-forge instance to register against — nothing extra to enable beyond hive-forge instance to register against — nothing extra to enable beyond
`services.hyperhive.swarm.forge.ci.enable = true` (see *For operators* above). `services.hyperhive.deploy.forgejo.ci.enable = true` (see *For operators* above).
Optional tuning: `services.hyperhive.swarm.forge.ci.name` (runner name in forge Optional tuning: `services.hyperhive.deploy.forgejo.ci.name` (runner name in forge
admin panel), `concurrency` (parallel job capacity), `labels` (workflow admin panel), `concurrency` (parallel job capacity), `labels` (workflow
targeting), `jobTimeout` (per-job wall-clock cap, default `"1h"`, Go duration targeting), `jobTimeout` (per-job wall-clock cap, default `"1h"`, Go duration
string e.g. `"3h"` — a job that exceeds it is killed so a hung or runaway string e.g. `"3h"` — a job that exceeds it is killed so a hung or runaway
@ -118,7 +118,7 @@ reaches the container.
### hive-c0re side (`forge/ci_runner.rs`, run during the startup sweep) ### hive-c0re side (`forge/ci_runner.rs`, run during the startup sweep)
Gated on `HYPERHIVE_FORGE_CI_ENABLED` (the nix module sets it on `hive-c0re.service` when `forge.ci.enable`). Best-effort — failures are logged and never abort the sweep; a healthy runner is never restarted. Gated on `HYPERHIVE_FORGE_CI_ENABLED` (the nix module sets it on `hive-c0re.service` when `deploy.forgejo.ci.enable`). Best-effort — failures are logged and never abort the sweep; a healthy runner is never restarted.
1. If `.runner` exists at `/var/lib/nixos-containers/hive-ci/var/lib/gitea-runner/hive/.runner`, validate its id against `GET /api/v1/admin/runners/{id}` with the core admin token: 1. If `.runner` exists at `/var/lib/nixos-containers/hive-ci/var/lib/gitea-runner/hive/.runner`, validate its id against `GET /api/v1/admin/runners/{id}` with the core admin token:
- **200**: still registered — done, no restart. - **200**: still registered — done, no restart.
@ -135,7 +135,7 @@ Gated on `HYPERHIVE_FORGE_CI_ENABLED` (the nix module sets it on `hive-c0re.serv
## Actions checkout mirror ## Actions checkout mirror
When `forge.ci.enable` is set, hive-c0re auto-seeds an When `deploy.forgejo.ci.enable` is set, hive-c0re auto-seeds an
`actions/checkout` pull-mirror on the local forge and sets Forgejo's `actions/checkout` pull-mirror on the local forge and sets Forgejo's
`DEFAULT_ACTIONS_URL` to point at the local instance. This means CI `DEFAULT_ACTIONS_URL` to point at the local instance. This means CI
`uses: actions/checkout@vN` steps resolve entirely on loopback — no `uses: actions/checkout@vN` steps resolve entirely on loopback — no

View file

@ -23,7 +23,7 @@ const RUNNER_FILE: &str = "/var/lib/nixos-containers/hive-ci/var/lib/gitea-runne
/// Whether the operator enabled the CI runner. The nix module sets /// Whether the operator enabled the CI runner. The nix module sets
/// `HYPERHIVE_FORGE_CI_ENABLED=1` on `hive-c0re.service` when /// `HYPERHIVE_FORGE_CI_ENABLED=1` on `hive-c0re.service` when
/// `services.hyperhive.forge.ci.enable` is on; absent means CI is off and /// `services.hyperhive.deploy.forgejo.ci.enable` is on; absent means CI is off and
/// there is no hive-ci container to register a runner for. /// there is no hive-ci container to register a runner for.
fn ci_enabled() -> bool { fn ci_enabled() -> bool {
std::env::var("HYPERHIVE_FORGE_CI_ENABLED") std::env::var("HYPERHIVE_FORGE_CI_ENABLED")

View file

@ -69,8 +69,43 @@ in
[ "services" "hyperhive" "swarm" "otel" "enable" ] [ "services" "hyperhive" "swarm" "otel" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-otel" "enable" ] [ "services" "hyperhive" "deploy" "swarm-otel" "enable" ]
) )
# The CI runner, and the only entry here that renames more than an
# `enable`: every knob under it describes the runner THIS host would run,
# so leaving `name`/`concurrency`/`labels`/`package` in the namespace that
# must be identical swarm-wide would keep the original defect for four
# more options. Renamed one by one because `ci` is a plain attrset of
# options rather than a submodule type, so there is no parent path to
# rename in a single entry.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "ci" "enable" ]
[ "services" "hyperhive" "deploy" "forgejo" "ci" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "ci" "name" ]
[ "services" "hyperhive" "deploy" "forgejo" "ci" "name" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "ci" "concurrency" ]
[ "services" "hyperhive" "deploy" "forgejo" "ci" "concurrency" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "ci" "labels" ]
[ "services" "hyperhive" "deploy" "forgejo" "ci" "labels" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "ci" "package" ]
[ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ]
)
]; ];
# ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the
# one entry with no `enable`: the forge is not optional — it is the canonical
# store for the meta flake and every agent's config repo, so it deploys with
# hyperhive itself. Running the CI runner is the only *deployment* decision
# it has, which is exactly the `{ enable; ci; }` shape the header describes,
# minus the half that does not apply. The knobs live with the module that
# reads them; this file stays the registry of toggles.
options.services.hyperhive.deploy = { options.services.hyperhive.deploy = {
grafana.enable = lib.mkOption { grafana.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;

View file

@ -5,7 +5,7 @@
... ...
}: }:
let let
cfg = config.services.hyperhive.swarm.forge.ci; cfg = config.services.hyperhive.deploy.forgejo.ci;
forgeCfg = config.services.hyperhive.swarm.forge; forgeCfg = config.services.hyperhive.swarm.forge;
gatewayCfg = config.services.hyperhive.gateway; gatewayCfg = config.services.hyperhive.gateway;
networkCfg = config.services.hyperhive.network; networkCfg = config.services.hyperhive.network;
@ -76,7 +76,7 @@ in
# nspawn containers can't create the user-namespaces that nix sandboxing # nspawn containers can't create the user-namespaces that nix sandboxing
# requires. See docs/gotchas.md. # requires. See docs/gotchas.md.
options.services.hyperhive.swarm.forge.ci = { options.services.hyperhive.deploy.forgejo.ci = {
enable = lib.mkOption { enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@ -166,7 +166,7 @@ in
{ {
assertion = forgeCfg.behindGateway; assertion = forgeCfg.behindGateway;
message = '' message = ''
services.hyperhive.swarm.forge.ci.enable requires services.hyperhive.deploy.forgejo.ci.enable requires
services.hyperhive.swarm.forge.behindGateway = true. services.hyperhive.swarm.forge.behindGateway = true.
The CI container runs with a private network namespace and The CI container runs with a private network namespace and
reaches the forge through the gateway vhost on the bridge IP. reaches the forge through the gateway vhost on the bridge IP.

View file

@ -88,7 +88,7 @@ let
# blip otherwise reds every `actions/checkout@vN` fetch from # blip otherwise reds every `actions/checkout@vN` fetch from
# data.forgejo.org). Auto-append a pull-mirror of it and point # data.forgejo.org). Auto-append a pull-mirror of it and point
# forgejo's DEFAULT_ACTIONS_URL at this instance so `uses:` resolves local. # forgejo's DEFAULT_ACTIONS_URL at this instance so `uses:` resolves local.
ciEnabled = config.services.hyperhive.swarm.forge.ci.enable; ciEnabled = config.services.hyperhive.deploy.forgejo.ci.enable;
actionCheckoutMirror = { actionCheckoutMirror = {
upstream = "https://github.com/actions/checkout"; upstream = "https://github.com/actions/checkout";
dest = "actions/checkout"; dest = "actions/checkout";
@ -350,7 +350,7 @@ in
so a host-resolver blip leaves a *stale* mirror, never a hard so a host-resolver blip leaves a *stale* mirror, never a hard
failure on whatever reads it. failure on whatever reads it.
When `services.hyperhive.swarm.forge.ci.enable` is set, an When `services.hyperhive.deploy.forgejo.ci.enable` is set, an
`actions/checkout` mirror is auto-appended to this list and `actions/checkout` mirror is auto-appended to this list and
forgejo's `DEFAULT_ACTIONS_URL` is pointed at this instance, so CI forgejo's `DEFAULT_ACTIONS_URL` is pointed at this instance, so CI
`uses: actions/checkout@vN` steps resolve entirely on loopback with `uses: actions/checkout@vN` steps resolve entirely on loopback with

View file

@ -69,7 +69,7 @@ let
allLocal = hive { enableAllLocalDefaults = true; }; allLocal = hive { enableAllLocalDefaults = true; };
bare = hive { }; bare = hive { };
withCi = hive { swarm.forge.ci.enable = true; }; withCi = hive { deploy.forgejo.ci.enable = true; };
# A priority collision is a property of the *option*, not # A priority collision is a property of the *option*, not
# of the merged value's interior — nix throws the moment the value is # of the merged value's interior — nix throws the moment the value is