Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ci-runner: run the forge CI runner as a swarm service

The CI runner was a hive deploy module: hive-ci.nix ran it beside the
forge, and hive-c0re registered it through the forge's admin API with
the `core` token, writing the registration token through hive-priv. It
could only run on the forge's host, and it kept an admin-scoped forge
token on a hive.

Now the runner is a swarm service:

- swarm-ci-runner.nix runs it in a `swarm-ci-runner` container on the
  host that sets deploy.ci-runner.enable, reaching the forge by name.
  It needs neither the forge nor a hive there.
- swarm-controller reads the forge's runner registration token with its
  own admin token and writes it to swarm/services/ci-runner/
  registration-token whenever it differs from what it last wrote. Its
  grant on that path is create/update only.
- The runner's own leaf (CN swarm-ci-runner, role and policy of the same
  name) reads that one path. swarm-bao-ci-runner.service fetches it into
  the runner's EnvironmentFile and restarts the runner on a change.
- A pre-start step removes .runner when the daemon's journal since its
  last start holds Forgejo's "unregistered runner" refusal (the daemon
  exits 1 when its startup Declare gets it), or when .runner names
  another forge address; upstream's pre-start step then registers again.

Removed from the hive side: hive-ci.nix and deploy.forgejo.ci.*,
forge/ci_runner.rs, PrivRequest::RegisterCiRunner and its handler and
ReadWritePaths entry, InfraContainer::Ci with hivectl's --ci and the
dashboard's hive-ci journal entry, HYPERHIVE_FORGE_CI_ENABLED, and
read:admin,write:admin from CORE_TOKEN_SCOPES: nothing else in hive-c0re
calls /api/v1/admin/*. The old ci options, under both swarm.forge.ci and
deploy.forgejo.ci, now fail evaluation naming deploy.ci-runner.

The forge's half of the old ci.enable, the actions/checkout mirror and
DEFAULT_ACTIONS_URL = self, is deploy.forgejo.actionsFromSelf.

Closes #4954
This commit is contained in:
atlas 2026-10-05 02:25:05 +02:00 • committed by mara
commit e2da5509ae
60 changed files with 1088 additions and 1201 deletions

View file

@ -20,9 +20,9 @@
#
# ⚠️ Each entry is an attrset with an `enable`, not a bare bool, so a
# service that grows a second *deployment* decision has somewhere to put
# it — `deploy.forgejo = { enable; ci; }` is then an ordinary addition
# rather than a migration. `ci` ("does this host run the runner too") is
# exactly that shape, and a bare bool leaves it unrepresentable.
# it — `deploy.nats = { enable; autoGenerateCallout; }` is then an ordinary
# addition rather than a migration, where a bare bool leaves it
# unrepresentable.
#
# ⚠️ The renames below are deliberately in this one file rather than
# spread across the service modules, so all of them have a single home
@ -31,6 +31,32 @@
{ lib, config, ... }:
let
deployCfg = config.services.hyperhive.deploy;
# A removed CI-runner option. Its replacement has the same name under
# `deploy.ci-runner`.
ciRunnerRemoved =
from: opt:
lib.mkRemovedOptionModule
(
[
"services"
"hyperhive"
]
++ from
++ [ opt ]
)
''
The CI runner is the swarm service in swarm-ci-runner.nix. It runs on
any host, needs neither the forge nor a hive there, and registers with
the token swarm-controller publishes to the secret store. Set
services.hyperhive.deploy.ci-runner.${opt} instead.
The runner registers as a new runner on the forge, so delete the old
one in the forge's admin panel (Site administration, Actions, Runners).
To keep resolving `uses: actions/checkout` from the forge, set
services.hyperhive.deploy.forgejo.actionsFromSelf = true on its host.
See docs/scheduler/ci.md.
'';
in
{
imports = [
@ -146,33 +172,20 @@ in
[ "services" "hyperhive" "deploy" "swarm-otel" "clientSecretFile" ]
)
# 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" ]
)
# Removed rather than renamed: the runner is a different container with a
# registration of its own, so an old definition is a deployment to re-read
# rather than a path to forward.
(ciRunnerRemoved [ "swarm" "forge" "ci" ] "enable")
(ciRunnerRemoved [ "swarm" "forge" "ci" ] "name")
(ciRunnerRemoved [ "swarm" "forge" "ci" ] "concurrency")
(ciRunnerRemoved [ "swarm" "forge" "ci" ] "labels")
(ciRunnerRemoved [ "swarm" "forge" "ci" ] "package")
(ciRunnerRemoved [ "deploy" "forgejo" "ci" ] "enable")
(ciRunnerRemoved [ "deploy" "forgejo" "ci" ] "name")
(ciRunnerRemoved [ "deploy" "forgejo" "ci" ] "concurrency")
(ciRunnerRemoved [ "deploy" "forgejo" "ci" ] "labels")
(ciRunnerRemoved [ "deploy" "forgejo" "ci" ] "package")
(ciRunnerRemoved [ "deploy" "forgejo" "ci" ] "jobTimeout")
# The queue's callout identity. What stays under `swarm.nats` is what the
# queue IS to every hive — the ports it answers on and the client id it is
@ -216,8 +229,7 @@ in
#
# `mirrors` renames in one entry rather than one per field: it is a single
# option of a list-of-submodule type, so the rename carries its whole
# value. The `ci` block above needs five because it is a plain attrset of
# separate options, which is the case with no parent path to rename.
# value.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "openFirewall" ]
[ "services" "hyperhive" "deploy" "forgejo" "openFirewall" ]
@ -451,9 +463,8 @@ in
];
# ⚠️ Only `deploy.forgejo.enable` is declared here. The rest of
# `deploy.forgejo` is in ./hive-forge/default.nix and ./hive-ci.nix: the
# knobs live with the module that reads them; this file stays the registry
# of toggles.
# `deploy.forgejo` is in ./hive-forge/default.nix: the knobs live with the
# module that reads them; this file stays the registry of toggles.
options.services.hyperhive.deploy = {
forgejo.enable = lib.mkOption {
type = lib.types.bool;
@ -602,6 +613,23 @@ in
'';
};
ci-runner.enable = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Run the swarm's Forgejo Actions runner in a `swarm-ci-runner`
container on this host. It reaches the forge at
{option}`services.hyperhive.swarm.forge.domain` and registers with
the token swarm-controller publishes to the secret store, so it
needs neither the forge nor a hive on this host.
Not derived from
{option}`services.hyperhive.deploy.allSwarmServices`: a runner
executes pull-request code, so running one is a decision of its own.
'';
};
hive-controller.enable = lib.mkOption {
type = lib.types.bool;
default = false;