From 38c222fadd532491446cfa87d1fe60f75f4bf0f8 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 4 Aug 2026 22:48:37 +0200 Subject: [PATCH 1/3] feat(nix): move the forge host options under services.hyperhive.swarm Forgejo is a swarm-global service, so its operator-facing host options move to services.hyperhive.swarm.forge (and .swarm.forge.ci) as the first of the namespace consolidation. Existing hive configs keep evaluating: swarm-renames.nix maps every moved leaf with mkRenamedOptionModule, which also emits a deprecation warning naming both the old and new path, so an operator is told what to rename rather than discovering it from a failed eval. The per-agent hyperhive.forge.url does NOT move. It is a client pointer at whatever forge an agent talks to - it shares a word with the service and nothing else, and the two are already documented as separate option surfaces. Verified by evaluating the host module, since no Rust gate evaluates nix: setting the old paths and reading the new ones yields the values (httpPort 3999, ci.concurrency 7), and config.warnings carries the rename notice. --- nix/docs/default.nix | 3 +- nix/host-modules/default.nix | 1 + nix/host-modules/hive-c0re/default.nix | 2 +- nix/host-modules/hive-c0re/environment.nix | 8 ++-- nix/host-modules/hive-ci.nix | 12 ++--- nix/host-modules/hive-forge/default.nix | 18 ++++---- nix/host-modules/hive-gateway/default.nix | 6 +-- nix/host-modules/swarm-renames.nix | 52 ++++++++++++++++++++++ 8 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 nix/host-modules/swarm-renames.nix diff --git a/nix/docs/default.nix b/nix/docs/default.nix index fef90953..4f7a1144 100644 --- a/nix/docs/default.nix +++ b/nix/docs/default.nix @@ -153,7 +153,8 @@ let - [host options](host.md) — options exposed by `hyperhive.nixosModules.default` to operator host configurations - (`services.hyperhive.{enable,domain,c0re,forge,matrix,gateway}.*`). + (`services.hyperhive.{enable,domain,c0re,gateway}.*`, + `services.hyperhive.swarm.{forge,matrix}.*`). - [per-agent options](agent.md) — options declared in `nix/agent-modules/`, visible from every `agent.nix` (`hyperhive.model`, `hyperhive.allowedRecipients`, diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index 2b87a421..9a0ebcc7 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -12,6 +12,7 @@ { imports = [ ./hyperhive.nix + ./swarm-renames.nix ./hive-c0re ./hive-ci.nix ./hive-forge diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 28937561..2cb40eea 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -31,7 +31,7 @@ let safeDirGitconfig = pkgs.writeText "hyperhive-safe-gitconfig" '' [safe] directory = * - [credential "http://${config.services.hyperhive.forge.domain}"] + [credential "http://${config.services.hyperhive.swarm.forge.domain}"] helper = hive-forge username = core [http] diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index 956a23af..0a86bbba 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -98,7 +98,7 @@ in # agents via meta.rs for their forge-notify client. The forge is # mandatory, so this is unconditional (the whole env block is already # gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`. - HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}"; + HIVE_FORGE_URL = "http://${config.services.hyperhive.swarm.forge.domain}"; } // lib.optionalAttrs @@ -147,17 +147,17 @@ in # dashboard doesn't need to learn the gateway is unconditional. HIVE_GATEWAY_ENABLED = "1"; } -// lib.optionalAttrs (config.services.hyperhive.forge.publicUrl != null) { +// lib.optionalAttrs (config.services.hyperhive.swarm.forge.publicUrl != null) { # Public URL of the forge, for the dashboard to build browser-facing # forge links from instead of guessing `:3000` (which # breaks the moment the operator's browser hostname isn't the forge # host, e.g. through the gateway or a reverse proxy). Sourced from - # `services.hyperhive.forge.publicUrl`, which itself defaults to the + # `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the # gateway vhost URL when `behindGateway = true` and `null` otherwise # — see that option's doc for the "hide, don't guess" rationale. # Absent here whenever `publicUrl` is `null`; the dashboard hides # forge links rather than emitting one it can't justify. - HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.forge.publicUrl; + HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl; } // lib.optionalAttrs diff --git a/nix/host-modules/hive-ci.nix b/nix/host-modules/hive-ci.nix index cd8e3a34..408e490a 100644 --- a/nix/host-modules/hive-ci.nix +++ b/nix/host-modules/hive-ci.nix @@ -5,8 +5,8 @@ ... }: let - cfg = config.services.hyperhive.forge.ci; - forgeCfg = config.services.hyperhive.forge; + cfg = config.services.hyperhive.swarm.forge.ci; + forgeCfg = config.services.hyperhive.swarm.forge; gatewayCfg = config.services.hyperhive.gateway; networkCfg = config.services.hyperhive.network; tlsCfg = config.services.hyperhive.tls; @@ -52,14 +52,14 @@ in # nspawn containers can't create the user-namespaces that nix sandboxing # requires. See docs/gotchas.md. - options.services.hyperhive.forge.ci = { + options.services.hyperhive.swarm.forge.ci = { enable = lib.mkOption { type = lib.types.bool; default = false; example = true; description = '' Run a Forgejo Actions runner in a `hive-ci` nixos-container. - Grouped under `services.hyperhive.forge` because the runner is + Grouped under `services.hyperhive.swarm.forge` because the runner is tightly coupled to the forge instance it registers against. Disabled by default; the internal forge it registers against is always present (mandatory), so enabling this is all that's needed. @@ -142,8 +142,8 @@ in { assertion = forgeCfg.behindGateway; message = '' - services.hyperhive.forge.ci.enable requires - services.hyperhive.forge.behindGateway = true. + services.hyperhive.swarm.forge.ci.enable requires + services.hyperhive.swarm.forge.behindGateway = true. The CI container runs with a private network namespace and reaches the forge through the gateway vhost on the bridge IP. Set behindGateway = true (it defaults to true alongside diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index ea1fe885..118f5022 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -5,7 +5,7 @@ ... }: let - cfg = config.services.hyperhive.forge; + cfg = config.services.hyperhive.swarm.forge; gatewayCfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; tlsCfg = config.services.hyperhive.tls; @@ -53,7 +53,7 @@ let # blip otherwise reds every `actions/checkout@vN` fetch from # data.forgejo.org). Auto-append a pull-mirror of it and point # forgejo's DEFAULT_ACTIONS_URL at this instance so `uses:` resolves local. - ciEnabled = config.services.hyperhive.forge.ci.enable; + ciEnabled = config.services.hyperhive.swarm.forge.ci.enable; actionCheckoutMirror = { upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; @@ -86,7 +86,7 @@ in # meta flake + every agent's config repo (and the `internal/*` repos), # so there is no enable/disable toggle. It deploys whenever hyperhive # itself is enabled (`services.hyperhive.enable`). - options.services.hyperhive.forge = { + options.services.hyperhive.swarm.forge = { httpPort = lib.mkOption { type = lib.types.port; default = 3000; @@ -244,7 +244,7 @@ in **Breaking change**: this used to default to `true`. If you relied on the old default for external reach, add - `services.hyperhive.forge.openFirewall = true;` to your host + `services.hyperhive.swarm.forge.openFirewall = true;` to your host config before rebuilding. ''; }; @@ -283,7 +283,7 @@ in so a host-resolver blip leaves a *stale* mirror, never a hard failure on whatever reads it. - When `services.hyperhive.forge.ci.enable` is set, an + When `services.hyperhive.swarm.forge.ci.enable` is set, an `actions/checkout` mirror is auto-appended to this list and forgejo's `DEFAULT_ACTIONS_URL` is pointed at this instance, so CI `uses: actions/checkout@vN` steps resolve entirely on loopback with @@ -298,7 +298,7 @@ in { assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl; message = '' - services.hyperhive.forge.rootUrl must end with "/". forgejo's + services.hyperhive.swarm.forge.rootUrl must end with "/". forgejo's ROOT_URL contract requires a trailing slash for correct relative-link generation; without it forgejo emits URLs like `https://forge.example.com.user.id` instead of @@ -313,7 +313,7 @@ in # loud. assertion = cfg.domain != ""; message = '' - services.hyperhive.forge.domain = "" is rejected. The + services.hyperhive.swarm.forge.domain = "" is rejected. The rendered URLs would be invalid (nginx wildcard catch-all for an empty server_name, /etc/hosts rejects empty entries). Either leave at default (auto-derives to @@ -326,7 +326,7 @@ in # splits on the single slash to create the org + repo. assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors; message = '' - Every services.hyperhive.forge.mirrors[].dest must be exactly + Every services.hyperhive.swarm.forge.mirrors[].dest must be exactly "/" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors} ''; } @@ -344,7 +344,7 @@ in ]) ) effectiveMirrors; message = '' - services.hyperhive.forge.mirrors[].dest must not place a mirror + services.hyperhive.swarm.forge.mirrors[].dest must not place a mirror in a hive-c0re-managed org (config / shared / agents / core) — those are provisioned by hive-c0re and a mirror there would collide. Use a dedicated org (e.g. "actions/checkout"). diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index f813692e..327c27ce 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -16,7 +16,7 @@ let cfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; matrixCfg = config.services.hyperhive.matrix; - forgeCfg = config.services.hyperhive.forge; + forgeCfg = config.services.hyperhive.swarm.forge; networkCfg = config.services.hyperhive.network; # Dashboard SPA dist, static-served by nginx. Read in OUTER scope so @@ -411,8 +411,8 @@ in networking.hosts = lib.mkIf cfg.localHostsEntry { "127.0.0.1" = lib.unique ( [ hyperhiveDomain ] - ++ lib.optional (config.services.hyperhive.forge.behindGateway or false - ) config.services.hyperhive.forge.domain + ++ lib.optional (config.services.hyperhive.swarm.forge.behindGateway or false + ) config.services.hyperhive.swarm.forge.domain ++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost ); }; diff --git a/nix/host-modules/swarm-renames.nix b/nix/host-modules/swarm-renames.nix new file mode 100644 index 00000000..6acd7749 --- /dev/null +++ b/nix/host-modules/swarm-renames.nix @@ -0,0 +1,52 @@ +# Backwards-compatibility aliases for options that moved under +# `services.hyperhive.swarm.*` when the swarm-global services were +# consolidated into one namespace. +# +# `mkRenamedOptionModule` maps the old path to the new one *and* emits a +# deprecation warning naming both, so an existing hive keeps evaluating and +# its operator is told exactly what to rename. That is the whole reason the +# move is safe to make in one commit: nothing has to be edited in lockstep. +# +# ⚠️ Only options that are genuinely **swarm-global** belong here. The +# per-agent `hyperhive.forge.url` (`nix/agent-modules/forge.nix`) is a +# client pointer at whatever forge an agent talks to — it shares a word with +# the service and nothing else, and it does not move. +{ lib, ... }: +let + # One entry per moved leaf. Written as a list rather than a fold over the + # option tree on purpose: a rename has to name both paths explicitly, and + # deriving them would make an accidental rename invisible in review. + renamed = old: new: lib.mkRenamedOptionModule old new; + + forgeOption = + name: + renamed [ "services" "hyperhive" "forge" name ] [ "services" "hyperhive" "swarm" "forge" name ]; + + forgeCiOption = + name: + renamed + [ "services" "hyperhive" "forge" "ci" name ] + [ "services" "hyperhive" "swarm" "forge" "ci" name ]; +in +{ + imports = + map forgeOption [ + "httpPort" + "sshPort" + "domain" + "publicUrl" + "package" + "behindGateway" + "rootUrl" + "openFirewall" + "mirrors" + ] + ++ map forgeCiOption [ + "enable" + "name" + "concurrency" + "labels" + "package" + "jobTimeout" + ]; +} From f7beea3a6fc1b9b5ec6bc332f38c77f995d1d190 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 4 Aug 2026 23:00:52 +0200 Subject: [PATCH 2/3] fix(nix): rename the forge attrset, not fifteen leaves mara on the PR: "why deprecate individual keys instead of the whole attrset? there is no hyperhive.forge anymore after the move". Right, and mkRenamedOptionModule takes the namespace path directly - the subtree comes with it, nested ci.* included. Fifteen explicit renames collapse to one. The list was also a liability: an option added under swarm.forge later would have needed remembering here to get an alias, so it could silently fall behind the options it mirrors. Re-ran both evals against the new form. Values still map (httpPort 3999, ci.concurrency 7 through the old paths) and the warning still fires; it now names the attrset rather than the leaf, which also means one warning per hive instead of one per option an operator had set. --- nix/host-modules/swarm-renames.nix | 60 +++++++++--------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/nix/host-modules/swarm-renames.nix b/nix/host-modules/swarm-renames.nix index 6acd7749..dc06afe2 100644 --- a/nix/host-modules/swarm-renames.nix +++ b/nix/host-modules/swarm-renames.nix @@ -2,51 +2,25 @@ # `services.hyperhive.swarm.*` when the swarm-global services were # consolidated into one namespace. # -# `mkRenamedOptionModule` maps the old path to the new one *and* emits a -# deprecation warning naming both, so an existing hive keeps evaluating and -# its operator is told exactly what to rename. That is the whole reason the -# move is safe to make in one commit: nothing has to be edited in lockstep. +# One rename per moved *namespace*, not per option: `mkRenamedOptionModule` +# takes the attrset path and carries the whole subtree, nested children +# included. An existing hive keeps evaluating, and its operator gets a single +# warning naming both paths rather than one per option they had set. +# +# A per-leaf list would also work and would be strictly worse: every option +# added under `swarm.forge` later would need remembering here to get an alias, +# so the list is a thing that can silently fall behind the options it mirrors. # # ⚠️ Only options that are genuinely **swarm-global** belong here. The -# per-agent `hyperhive.forge.url` (`nix/agent-modules/forge.nix`) is a -# client pointer at whatever forge an agent talks to — it shares a word with -# the service and nothing else, and it does not move. +# per-agent `hyperhive.forge.url` (`nix/agent-modules/forge.nix`) is a client +# pointer at whatever forge an agent talks to — it shares a word with the +# service and nothing else, and it does not move. { lib, ... }: -let - # One entry per moved leaf. Written as a list rather than a fold over the - # option tree on purpose: a rename has to name both paths explicitly, and - # deriving them would make an accidental rename invisible in review. - renamed = old: new: lib.mkRenamedOptionModule old new; - - forgeOption = - name: - renamed [ "services" "hyperhive" "forge" name ] [ "services" "hyperhive" "swarm" "forge" name ]; - - forgeCiOption = - name: - renamed - [ "services" "hyperhive" "forge" "ci" name ] - [ "services" "hyperhive" "swarm" "forge" "ci" name ]; -in { - imports = - map forgeOption [ - "httpPort" - "sshPort" - "domain" - "publicUrl" - "package" - "behindGateway" - "rootUrl" - "openFirewall" - "mirrors" - ] - ++ map forgeCiOption [ - "enable" - "name" - "concurrency" - "labels" - "package" - "jobTimeout" - ]; + imports = [ + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "forge" ] + [ "services" "hyperhive" "swarm" "forge" ] + ) + ]; } From 0d23896dbbd987e3658af26c1a970c0828e919fe Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 4 Aug 2026 23:21:12 +0200 Subject: [PATCH 3/3] fix(nix): the rename lives with the module that declares the options mara: "remove the extra deprecatios file, less comments". The alias now sits in hive-forge/default.nix next to the options it renames, and the comment is three lines instead of eighteen. Better than tidier: there is no shared file for the sibling migrations to queue behind. Matrix's rename goes in hive-matrix.nix, authelia's in its own module, and none of them touch each other. hive-ci.nix declares swarm.forge.ci.* from a different module and stays covered, because the rename is on the namespace rather than on the declaring file. Re-ran the check both ways this time, per argus on the last round: old paths set and new paths read back 3999 / 7, and a config that only uses the new paths produces no warnings at all. --- nix/host-modules/default.nix | 1 - nix/host-modules/hive-forge/default.nix | 10 ++++++++++ nix/host-modules/swarm-renames.nix | 26 ------------------------- 3 files changed, 10 insertions(+), 27 deletions(-) delete mode 100644 nix/host-modules/swarm-renames.nix diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index 9a0ebcc7..2b87a421 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -12,7 +12,6 @@ { imports = [ ./hyperhive.nix - ./swarm-renames.nix ./hive-c0re ./hive-ci.nix ./hive-forge diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 118f5022..518e22cf 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -82,6 +82,16 @@ in # (base URL), the same shape as the GitHub PAT / matrix extra-account # flows. See `hive-c0re/src/dashboard/extra_forges.rs`. + # Forge moved under `swarm` when the swarm-global services were + # consolidated. One rename for the namespace: the subtree comes with it, + # so existing hives keep evaluating and get one warning naming both paths. + imports = [ + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "forge" ] + [ "services" "hyperhive" "swarm" "forge" ] + ) + ]; + # The internal forge is mandatory — it's the canonical store for the # meta flake + every agent's config repo (and the `internal/*` repos), # so there is no enable/disable toggle. It deploys whenever hyperhive diff --git a/nix/host-modules/swarm-renames.nix b/nix/host-modules/swarm-renames.nix deleted file mode 100644 index dc06afe2..00000000 --- a/nix/host-modules/swarm-renames.nix +++ /dev/null @@ -1,26 +0,0 @@ -# Backwards-compatibility aliases for options that moved under -# `services.hyperhive.swarm.*` when the swarm-global services were -# consolidated into one namespace. -# -# One rename per moved *namespace*, not per option: `mkRenamedOptionModule` -# takes the attrset path and carries the whole subtree, nested children -# included. An existing hive keeps evaluating, and its operator gets a single -# warning naming both paths rather than one per option they had set. -# -# A per-leaf list would also work and would be strictly worse: every option -# added under `swarm.forge` later would need remembering here to get an alias, -# so the list is a thing that can silently fall behind the options it mirrors. -# -# ⚠️ Only options that are genuinely **swarm-global** belong here. The -# per-agent `hyperhive.forge.url` (`nix/agent-modules/forge.nix`) is a client -# pointer at whatever forge an agent talks to — it shares a word with the -# service and nothing else, and it does not move. -{ lib, ... }: -{ - imports = [ - (lib.mkRenamedOptionModule - [ "services" "hyperhive" "forge" ] - [ "services" "hyperhive" "swarm" "forge" ] - ) - ]; -}