From 52cdfe84a066d83aceb8ccfbb6e6d51cbfaa633d Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 9 Nov 2026 13:07:41 +0100 Subject: [PATCH] keep the mirror hint off forge errors, forward the hyperhive mirror from any controller host --- docs/1-getting-started/nixos.md | 6 +-- nix/host-modules/glue-forge-controller.nix | 43 +++++++++++++--------- nix/host-modules/hive-forge/default.nix | 17 +++++---- nix/module-eval/swarm-services-switch.nix | 13 ++++--- swarm-controller/src/meta_flake.rs | 27 +++++++++++++- 5 files changed, 70 insertions(+), 36 deletions(-) diff --git a/docs/1-getting-started/nixos.md b/docs/1-getting-started/nixos.md index ca94489d4..d45a81117 100644 --- a/docs/1-getting-started/nixos.md +++ b/docs/1-getting-started/nixos.md @@ -89,9 +89,9 @@ Run `nixos-rebuild switch --flake .#my-host`. The swarm UI then answers at The swarm controller pins the hive's flake inputs to a commit of hyperhive on the swarm's own forge, so the forge needs a `hyperhive/hyperhive` repo before -the first agent builds. On a host that runs the forge and the controller, -the controller creates it as a pull-mirror of -`https://forge.darkest.space/hyperhive/hyperhive`. You run no extra step. +the first agent builds. The controller creates it as a pull-mirror of +`https://forge.darkest.space/hyperhive/hyperhive`, on the swarm's forge wherever +that runs. You run no extra step. | option | what it does | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/nix/host-modules/glue-forge-controller.nix b/nix/host-modules/glue-forge-controller.nix index 0cc1a24f9..4fd8a4063 100644 --- a/nix/host-modules/glue-forge-controller.nix +++ b/nix/host-modules/glue-forge-controller.nix @@ -1,13 +1,15 @@ -# Glue: wire the forge and the swarm-controller together when one host runs both. +# Glue: wire the swarm-controller's forge seeding to the forge options. # # ONE PAIRING PER FILE — forge ← swarm-controller, and nothing else. Deleting # this leaves the controller seeding no pull-mirrors, the forge resolving # workflow actions from `data.forgejo.org` and a fresh swarm without the hyperhive # repo its meta flake pins. # -# ⚠️ Gated on BOTH services being here: the mirrors reach a controller on this -# host only, and the forge's own `config` block hangs off -# `deploy.forgejo.enable`. The forge's `mirrors` warning covers the split case. +# ⚠️ The mirror list is forwarded wherever the controller runs: it creates the +# mirrors through the forge's API, so the forge may be on another host. The +# list is that of the controller's own host; the forge's `mirrors` warning +# covers a list declared on a forge host without one. `actionsFromSelf` changes +# the forge's own settings, so it follows only a forge beside the controller. { lib, config, @@ -17,20 +19,25 @@ let deployCfg = config.services.hyperhive.deploy; in { - config = lib.mkIf (deployCfg.forgejo.enable && deployCfg.swarm-controller.enable) { - # The controller seeds the `actions/*` mirrors, so a forge beside one can - # resolve actions from itself. `mkDefault`: an operator's explicit value wins. - services.hyperhive.deploy.forgejo.actionsFromSelf = lib.mkDefault true; + config = lib.mkMerge [ + (lib.mkIf deployCfg.swarm-controller.enable { + # The meta flake pins the hyperhive repo on the swarm's forge, and a fresh + # forge has none. The controller creates the mirror only if + # `hyperhive/hyperhive` is absent, so a swarm that hosts the real repo is + # not touched. `mkDefault`: an operator's explicit value wins. + services.hyperhive.deploy.forgejo.hyperhiveUpstream = + lib.mkDefault "https://forge.darkest.space/hyperhive/hyperhive"; - # The meta flake pins the hyperhive repo on this forge, and a fresh forge has - # none. The controller creates the mirror only if `hyperhive/hyperhive` is - # absent, so a swarm that hosts the real repo is not touched. - services.hyperhive.deploy.forgejo.hyperhiveUpstream = - lib.mkDefault "https://forge.darkest.space/hyperhive/hyperhive"; + # JSON-encoded list of { upstream, dest }, seeded by the controller's + # forge-objects pass (`swarm-controller/src/forge/objects.rs`). + systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS = + builtins.toJSON deployCfg.forgejo.effectiveMirrors; + }) - # JSON-encoded list of { upstream, dest }, seeded by the controller's - # forge-objects pass (`swarm-controller/src/forge/objects.rs`). - systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS = - builtins.toJSON deployCfg.forgejo.effectiveMirrors; - }; + (lib.mkIf (deployCfg.forgejo.enable && deployCfg.swarm-controller.enable) { + # The controller seeds the `actions/*` mirrors, so a forge beside one can + # resolve actions from itself. `mkDefault`: an operator's explicit value wins. + services.hyperhive.deploy.forgejo.actionsFromSelf = lib.mkDefault true; + }) + ]; } diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 295ac6a57..f2fb5429e 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -171,6 +171,9 @@ in imports = [ ./service.nix + # Set on every host, not only the forge's: the controller's host forwards the + # list whether or not the forge runs beside it. + { services.hyperhive.deploy.forgejo.effectiveMirrors = effectiveMirrors; } (lib.mkRemovedOptionModule [ "services" "hyperhive" "deploy" "forgejo" "sso" "clientSecretFile" ] '' The forge's OIDC client secret is fetched from the swarm secret store (swarm-bao-forge-oidc) into a fixed runtime path. Remove this @@ -311,12 +314,13 @@ in build fetches it from there, so without it the first agent's `update_meta` step fails and no agent container is built. - Defaults to `https://forge.darkest.space/hyperhive/hyperhive` when this - host also runs the swarm-controller, which seeds the mirror (set by - `glue-forge-controller.nix`), and to `null` otherwise. The controller - creates the mirror only when `hyperhive/hyperhive` does not exist: a - swarm that already holds the repo as a normal repo keeps it unchanged. A `hyperhive/hyperhive` entry in - {option}`services.hyperhive.deploy.forgejo.mirrors` takes precedence. + Defaults to `https://forge.darkest.space/hyperhive/hyperhive` on a host + that runs the swarm-controller, which seeds the mirror on the swarm's + forge wherever that runs (set by `glue-forge-controller.nix`), and to + `null` otherwise. The controller creates the mirror only when + `hyperhive/hyperhive` does not exist: a swarm that already holds the + repo as a normal repo keeps it unchanged. A `hyperhive/hyperhive` entry + in {option}`services.hyperhive.deploy.forgejo.mirrors` takes precedence. ''; }; @@ -1400,7 +1404,6 @@ in # ../glue-forge-controller.nix); a controller elsewhere reads its own # host's list, so the warning below says so at eval rather than leaving # it to be noticed as a missing repo. - services.hyperhive.deploy.forgejo.effectiveMirrors = effectiveMirrors; warnings = lib.optional (effectiveMirrors != [ ] && !deployCfg.swarm-controller.enable) '' services.hyperhive.deploy.forgejo.mirrors (or the actions/* and hyperhive mirrors deploy.forgejo.actionsFromSelf and hyperhiveUpstream add) is set on a host that does not run the diff --git a/nix/module-eval/swarm-services-switch.nix b/nix/module-eval/swarm-services-switch.nix index 42574ad88..3d269116d 100644 --- a/nix/module-eval/swarm-services-switch.nix +++ b/nix/module-eval/swarm-services-switch.nix @@ -1410,19 +1410,20 @@ let # forge has none, so the controller seeds a mirror of it: by default, # with another upstream when the option says so, and not at all when it # is null (controllerWithMirrors) or without the controller (forgeOnly). - name = "a forge beside the controller mirrors hyperhive/hyperhive unless told not to"; + # controllerAlone has the forge on another host and still gets the default. + name = "the controller mirrors hyperhive/hyperhive on its forge unless told not to"; ok = let mirrorsOf = m: builtins.fromJSON m.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS; hyperhiveOf = m: lib.filter (e: e.dest == "hyperhive/hyperhive") (mirrorsOf m); - in - hyperhiveOf forgeWithController == [ - { + defaultHyperhive = { upstream = "https://forge.darkest.space/hyperhive/hyperhive"; dest = "hyperhive/hyperhive"; - } - ] + }; + in + hyperhiveOf forgeWithController == [ defaultHyperhive ] + && hyperhiveOf controllerAlone == [ defaultHyperhive ] && map (e: e.upstream) (hyperhiveOf controllerWithSelfActions) == [ hyperhiveElsewhere ] && hyperhiveOf controllerWithMirrors == [ ] && forgeOnly.services.hyperhive.deploy.forgejo.hyperhiveUpstream == null; diff --git a/swarm-controller/src/meta_flake.rs b/swarm-controller/src/meta_flake.rs index 9f3908b21..c55d2fae9 100644 --- a/swarm-controller/src/meta_flake.rs +++ b/swarm-controller/src/meta_flake.rs @@ -374,8 +374,7 @@ async fn hyperhive_pin( let Some(flake) = committed_flake else { let head = forge .head_files(HYPERHIVE_OWNER, HYPERHIVE_REPO, None, &[]) - .await - .with_context(missing_hyperhive)? + .await? .with_context(missing_hyperhive)?; return Ok(HyperhivePin { rev: head.commit, @@ -575,6 +574,8 @@ mod tests { compares: Mutex>, /// Every compare fails, as a forge that is down does. compare_fails: bool, + /// Reads of this `owner/repo` fail, as on a forge that refuses or is down. + head_fails: Option, /// Every `head_files` read, as `("owner/repo", branch)`. head_reads: Mutex)>>, } @@ -603,6 +604,9 @@ mod tests { branch: Option<&str>, paths: &[&str], ) -> Result> { + if self.head_fails.as_deref() == Some(&format!("{owner}/{repo}")) { + anyhow::bail!("forge answered 503"); + } self.head_reads .lock() .unwrap() @@ -874,6 +878,25 @@ mod tests { assert!(forge.commits.lock().unwrap().is_empty()); } + #[tokio::test] + async fn a_forge_error_reading_hyperhive_does_not_carry_the_mirror_hint() { + let mut forge = forge_with(&[]); + forge.head_fails = Some("hyperhive/hyperhive".to_owned()); + let err = update( + &forge, + &FakeLocker::locked(), + &settings(), + None, + &argus_and_reviewer(), + &[], + ) + .await + .unwrap_err(); + let err = format!("{err:#}"); + assert!(err.contains("forge answered 503"), "{err}"); + assert!(!err.contains("hyperhiveUpstream"), "{err}"); + } + #[tokio::test] async fn a_refused_lock_commits_nothing() { let forge = forge_with(&[]);