From b0d099274ecf67e2a9a945b2afb8904a4030ecb0 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 28 Jun 2026 23:44:38 +0200 Subject: [PATCH 1/7] wip(#2072): forge.mirrors option + DEFAULT_ACTIONS_URL=self when CI on General-purpose Forgejo pull-mirror config (services.hyperhive.forge.mirrors: list of {upstream, dest}). When CI is enabled, auto-append an actions/checkout mirror + point forgejo DEFAULT_ACTIONS_URL at this instance so CI's actions/checkout@vN resolves on loopback (immune to host-resolver blips, #2072). Seed oneshot (creates the dest orgs + pull-mirrors via the migrate API) is the next commit. --- nix/modules/hive-forge.nix | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index cc99f7bc..80974157 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -34,6 +34,18 @@ let else "http://${cfg.domain}:${toString cfg.httpPort}/"; effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl; + + # When CI is enabled, the runner needs `actions/checkout` resolvable + # without external DNS (hive-ci shares the host netns, so a host-resolver + # blip otherwise reds every `actions/checkout@vN` fetch from + # data.forgejo.org — see #2072). 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; + actionCheckoutMirror = { + upstream = "https://github.com/actions/checkout"; + dest = "actions/checkout"; + }; + effectiveMirrors = cfg.mirrors ++ lib.optional ciEnabled actionCheckoutMirror; in { # Private Forgejo in a `hive-forge` nixos-container, shared host @@ -177,6 +189,49 @@ in config before rebuilding. ''; }; + + mirrors = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + upstream = lib.mkOption { + type = lib.types.str; + example = "https://github.com/actions/checkout"; + description = "Upstream clone URL to mirror from."; + }; + dest = lib.mkOption { + type = lib.types.str; + example = "actions/checkout"; + description = '' + Local `/` the pull-mirror is created at. The + `` org is auto-created if missing. Keep mirror dests + in their own orgs (e.g. `actions/*`) — separate from the + hive-c0re-managed namespaces (config/shared/agents/core) so + the seed never collides with core's own provisioning. + ''; + }; + }; + } + ); + default = [ ]; + example = lib.literalExpression '' + [ { upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; } ] + ''; + description = '' + General-purpose Forgejo **pull-mirrors** to auto-seed on the local + forge. Each entry is created as a real Forgejo pull-mirror (it + re-syncs from `upstream` out-of-band), not a one-off pushed clone — + 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 + `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 + no external DNS on the critical path (the seed/re-sync needs + external DNS, but that's off the CI path). See `#2072`. + ''; + }; }; config = lib.mkIf config.services.hyperhive.enable { @@ -302,6 +357,13 @@ in # of token scopes. Required by `hive-ci-register.service` # in the hive-ci container. actions.ENABLED = true; + # When CI is enabled, resolve `uses: /@vN` from + # THIS instance (the seeded `actions/checkout` pull-mirror) + # instead of the upstream default `data.forgejo.org` — keeps + # the checkout step on loopback, immune to a host-resolver + # blip (#2072). `self` = forgejo expands actions against its + # own ROOT_URL. + actions.DEFAULT_ACTIONS_URL = lib.mkIf ciEnabled "self"; # F3 (federation) computes its data dir relative to the # forgejo binary, which lands in the read-only nix # store and crashes anything that touches the F3 From 4a3581a3d2d6b8acd803a8efa03dba339dc2f592 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 28 Jun 2026 23:48:11 +0200 Subject: [PATCH 2/7] feat(#2072): auto-seed Forgejo pull-mirrors (DEFAULT_ACTIONS_URL=self for CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit General-purpose mirror mechanism for the internal forge, per mara's call on #2074 (real Forgejo pull-mirrors, nix-configured — not a pushed clone). - services.hyperhive.forge.mirrors: list of { upstream, dest } pull-mirrors, any repo. Each is created as a real Forgejo pull-mirror (re-syncs from upstream), dest = / in its own org. - When forge.ci.enable is set: an actions/checkout mirror is auto-appended + forgejo DEFAULT_ACTIONS_URL is pointed at this instance, so CI 'uses: actions/checkout@vN' resolves on loopback — immune to a host-resolver blip that previously reded every checkout (the seed/re-sync needs external DNS, but that's off the CI critical path). - forgejo-seed-mirrors.service: host-side oneshot (the core admin token never enters a container), modelled on hive-ci-prefetch — waits <=60s for the core token, then idempotently ensures each dest org + creates the pull-mirror via the migrate API. partOf the forge container so it re-ensures on restart. - assertions: dest must be /; mirror orgs can't shadow the c0re-managed namespaces (config/shared/agents/core) so the seed never races hive-c0re's own provisioning. Supersedes #2074 (the raw-clone stopgap) as the durable #2072 fix. --- nix/modules/hive-forge.nix | 129 +++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index 80974157..dd1399fd 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -46,6 +46,82 @@ let dest = "actions/checkout"; }; effectiveMirrors = cfg.mirrors ++ lib.optional ciEnabled actionCheckoutMirror; + + # Host-side core admin token hive-c0re mints after provisioning the forge + # admin (same file hive-ci-prefetch reads). Root-only; never enters a + # container — so the mirror seed runs host-side, exactly like + # hive-ci-prefetch, rather than minting a second token in-container. + coreTokenPath = "/var/lib/hyperhive/forge-core-token"; + + # Idempotently create each `effectiveMirrors` entry as a real Forgejo + # pull-mirror via the migrate API. Host-side: only talks to the forge on + # loopback (forgejo itself does the upstream clone, so the upstream-DNS + # dependency lives in the container + is off the CI critical path). + # Modelled on hive-ci-prefetch's wait-for-core-token loop. + seedMirrorsScript = pkgs.writeShellScript "forgejo-seed-mirrors" '' + set -uo pipefail + FORGE_URL="http://127.0.0.1:${toString cfg.httpPort}" + + CORE_TOKEN="" + for i in $(seq 1 60); do + if [ -f "${coreTokenPath}" ]; then CORE_TOKEN=$(cat "${coreTokenPath}"); break; fi + echo "forgejo-seed-mirrors: waiting for core token ($i/60)..." >&2 + sleep 1 + done + if [ -z "''${CORE_TOKEN:-}" ]; then + echo "forgejo-seed-mirrors: core token absent after 60s — cannot seed mirrors" >&2 + exit 1 + fi + AUTH="Authorization: token $CORE_TOKEN" + rc=0 + + seed_one() { + upstream="$1"; owner="$2"; repo="$3" + # Ensure the dest org (idempotent: 201 created / 422 already exists). + ohttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' -X POST \ + -H "$AUTH" -H 'Content-Type: application/json' \ + "$FORGE_URL/api/v1/orgs" -d "{\"username\":\"$owner\"}" || echo 000) + case "$ohttp" in + 201 | 422) ;; + *) echo "forgejo-seed-mirrors: ensure org '$owner' returned HTTP $ohttp" >&2 ;; + esac + # Skip if the repo already exists (the mirror persists across reboots + # in the non-ephemeral forge state, so this no-ops on every reboot + # after the first). + rhttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' \ + -H "$AUTH" "$FORGE_URL/api/v1/repos/$owner/$repo" || echo 000) + if [ "$rhttp" = 200 ]; then + echo "forgejo-seed-mirrors: $owner/$repo already present — skipping" >&2 + return 0 + fi + # Create the pull-mirror. service=git → generic git clone of + # clone_addr (no upstream API token needed); mirror=true → forgejo + # keeps it re-syncing on its mirror interval. + resp=$(${pkgs.curl}/bin/curl -s -w $'\n%{http_code}' -X POST \ + -H "$AUTH" -H 'Content-Type: application/json' \ + "$FORGE_URL/api/v1/repos/migrate" \ + -d "{\"clone_addr\":\"$upstream\",\"repo_owner\":\"$owner\",\"repo_name\":\"$repo\",\"mirror\":true,\"service\":\"git\",\"private\":false}" \ + || printf '\n000') + mhttp=$(printf '%s' "$resp" | tail -n1) + case "$mhttp" in + 2*) echo "forgejo-seed-mirrors: created pull-mirror $owner/$repo from $upstream" >&2 ;; + *) + echo "forgejo-seed-mirrors: migrate $owner/$repo failed HTTP $mhttp: $(printf '%s' "$resp" | sed '$d')" >&2 + rc=1 + ;; + esac + } + + ${lib.concatMapStringsSep "\n" ( + m: + let + parts = lib.splitString "/" m.dest; + in + "seed_one ${lib.escapeShellArg m.upstream} ${lib.escapeShellArg (builtins.elemAt parts 0)} ${lib.escapeShellArg (builtins.elemAt parts 1)}" + ) effectiveMirrors} + + exit $rc + ''; in { # Private Forgejo in a `hive-forge` nixos-container, shared host @@ -262,6 +338,37 @@ in hostname like "forge.example.com" or "git.internal". ''; } + { + # Each mirror dest must be exactly `/` — the seed + # 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 + "/" (one slash). Got: ${ + lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors + } + ''; + } + { + # Keep mirror orgs out of the hive-c0re-managed namespaces + # (config/shared/agents/core) so the seed never races / collides + # with hive-c0re's own startup provisioning of those orgs. + assertion = lib.all ( + m: + !(lib.elem (builtins.elemAt (lib.splitString "/" m.dest) 0) [ + "config" + "shared" + "agents" + "core" + ]) + ) effectiveMirrors; + message = '' + services.hyperhive.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"). + ''; + } ]; containers.hive-forge = { @@ -474,5 +581,27 @@ in cfg.sshPort ]; }; + + # Seed the configured pull-mirrors once the forge + core token are up. + # Host-side (the core admin token never enters a container) and modelled + # on hive-ci-prefetch. partOf the forge container so it re-runs (and + # re-ensures, idempotently) on every forge (re)start. Only present when + # there's something to seed. + systemd.services.forgejo-seed-mirrors = lib.mkIf (effectiveMirrors != [ ]) { + description = "Seed Forgejo pull-mirrors (host-side)"; + after = [ + "hive-c0re.service" + "container@hive-forge.service" + ]; + wants = [ "container@hive-forge.service" ]; + wantedBy = [ "multi-user.target" ]; + partOf = [ "container@hive-forge.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = seedMirrorsScript; + SyslogIdentifier = "forgejo-seed-mirrors"; + }; + }; }; } From 990868b2e72aed583d7d3c19b72d29e8cbc12601 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 28 Jun 2026 23:53:07 +0200 Subject: [PATCH 3/7] address argus review on the mirror seed - drop the three cross-ref tracker tags from comments/description (prose only) - build the orgs + migrate JSON bodies with jq -n --arg (an upstream URL containing a quote no longer corrupts the request) - don't auto-append the actions/checkout mirror when the operator already declared that dest (avoids a duplicate effectiveMirrors entry when CI is on) --- nix/modules/hive-forge.nix | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index dd1399fd..bad108e8 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -38,14 +38,21 @@ let # When CI is enabled, the runner needs `actions/checkout` resolvable # without external DNS (hive-ci shares the host netns, so a host-resolver # blip otherwise reds every `actions/checkout@vN` fetch from - # data.forgejo.org — see #2072). 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. ciEnabled = config.services.hyperhive.forge.ci.enable; actionCheckoutMirror = { upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; }; - effectiveMirrors = cfg.mirrors ++ lib.optional ciEnabled actionCheckoutMirror; + # Auto-append the actions/checkout mirror only when CI is on AND the + # operator hasn't already declared that dest themselves (else CI-on + + # an explicit `actions/checkout` entry would duplicate it). + effectiveMirrors = + cfg.mirrors + ++ lib.optional ( + ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors) + ) actionCheckoutMirror; # Host-side core admin token hive-c0re mints after provisioning the forge # admin (same file hive-ci-prefetch reads). Root-only; never enters a @@ -78,9 +85,10 @@ let seed_one() { upstream="$1"; owner="$2"; repo="$3" # Ensure the dest org (idempotent: 201 created / 422 already exists). + org_body=$(${pkgs.jq}/bin/jq -nc --arg u "$owner" '{ username: $u }') ohttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' -X POST \ -H "$AUTH" -H 'Content-Type: application/json' \ - "$FORGE_URL/api/v1/orgs" -d "{\"username\":\"$owner\"}" || echo 000) + "$FORGE_URL/api/v1/orgs" -d "$org_body" || echo 000) case "$ohttp" in 201 | 422) ;; *) echo "forgejo-seed-mirrors: ensure org '$owner' returned HTTP $ohttp" >&2 ;; @@ -97,10 +105,13 @@ let # Create the pull-mirror. service=git → generic git clone of # clone_addr (no upstream API token needed); mirror=true → forgejo # keeps it re-syncing on its mirror interval. + mig_body=$(${pkgs.jq}/bin/jq -nc \ + --arg c "$upstream" --arg o "$owner" --arg r "$repo" \ + '{ clone_addr: $c, repo_owner: $o, repo_name: $r, mirror: true, service: "git", private: false }') resp=$(${pkgs.curl}/bin/curl -s -w $'\n%{http_code}' -X POST \ -H "$AUTH" -H 'Content-Type: application/json' \ "$FORGE_URL/api/v1/repos/migrate" \ - -d "{\"clone_addr\":\"$upstream\",\"repo_owner\":\"$owner\",\"repo_name\":\"$repo\",\"mirror\":true,\"service\":\"git\",\"private\":false}" \ + -d "$mig_body" \ || printf '\n000') mhttp=$(printf '%s' "$resp" | tail -n1) case "$mhttp" in @@ -305,7 +316,7 @@ in forgejo's `DEFAULT_ACTIONS_URL` is pointed at this instance, so CI `uses: actions/checkout@vN` steps resolve entirely on loopback with no external DNS on the critical path (the seed/re-sync needs - external DNS, but that's off the CI path). See `#2072`. + external DNS, but that's off the CI path). ''; }; }; @@ -468,7 +479,7 @@ in # THIS instance (the seeded `actions/checkout` pull-mirror) # instead of the upstream default `data.forgejo.org` — keeps # the checkout step on loopback, immune to a host-resolver - # blip (#2072). `self` = forgejo expands actions against its + # blip. `self` = forgejo expands actions against its # own ROOT_URL. actions.DEFAULT_ACTIONS_URL = lib.mkIf ciEnabled "self"; # F3 (federation) computes its data dir relative to the From 6f5dade9c92c94363c5900ae0cce3f5a46dc1f84 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 28 Jun 2026 23:58:40 +0200 Subject: [PATCH 4/7] treefmt: collapse the assertion-message interpolation to one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nixfmt wanted the ${...} on a single line (verified locally: nix build .#checks.x86_64-linux.formatting passes). The earlier CI 'docs' failure was a transient muede-pc2 build hiccup — the docs check builds clean locally on the same drv. --- nix/modules/hive-forge.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index bad108e8..5f84f9f8 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -355,9 +355,7 @@ in assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors; message = '' Every services.hyperhive.forge.mirrors[].dest must be exactly - "/" (one slash). Got: ${ - lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors - } + "/" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors} ''; } { From 53df2c9598a3a6285955e34f8e0394df27e9fe71 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 29 Jun 2026 00:10:15 +0200 Subject: [PATCH 5/7] rework(#2072): seed mirrors in c0re startup, not a host-side oneshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per mara: the mirror seeding belongs in hive-c0re's forge provisioning sweep, where the core admin token + org-ensure already live — not a parallel host-side nix oneshot. - forge.rs: ensure_mirrors() reads HYPERHIVE_FORGE_MIRRORS (JSON list of {upstream,dest}), ensures each dest org (reuse ensure_org) + creates the pull-mirror via the migrate API (reuse forge_http, serde_json::json! body, 409/existing = success). Called in ensure_all() right after the SEEDED_ORGS loop (token in scope, warn-and-continue like the other ensure_* steps). - hive-forge.nix: forward effectiveMirrors to c0re via systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS; drop the forgejo-seed-mirrors.service + its script + the host-side core-token read. Keep the forge.mirrors option, DEFAULT_ACTIONS_URL=self (CI-gated), and the dest-shape / no-c0re-namespace-collision assertions. Verified locally: nix parse + treefmt (incl rustfmt) clean; serde/serde_json patterns mirror dashboard.rs. cargo build runs in CI (no cc in my container). --- hive-c0re/src/forge.rs | 93 ++++++++++++++++++++++++++++++++ nix/modules/hive-forge.nix | 108 +++---------------------------------- 2 files changed, 100 insertions(+), 101 deletions(-) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index e2a868ae..2d87cc01 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -913,6 +913,94 @@ async fn ensure_org(name: &str, admin_token: &str) -> Result<()> { } } +/// One operator-declared pull-mirror, forwarded from the nix +/// `services.hyperhive.forge.mirrors` option as JSON in +/// `HYPERHIVE_FORGE_MIRRORS`. +#[derive(serde::Deserialize)] +struct Mirror { + /// Upstream clone URL to mirror from (e.g. `https://github.com/actions/checkout`). + upstream: String, + /// Local `/` the mirror is created at. + dest: String, +} + +/// Ensure each `HYPERHIVE_FORGE_MIRRORS` entry exists as a real Forgejo +/// pull-mirror. The env carries the JSON-encoded nix `forge.mirrors` list +/// (plus the CI-auto `actions/checkout` entry). Absent/empty env = no-op. +/// Per-mirror failures warn and continue — never abort the startup sweep. +async fn ensure_mirrors(admin_token: &str) { + let raw = match std::env::var("HYPERHIVE_FORGE_MIRRORS") { + Ok(s) if !s.trim().is_empty() => s, + _ => return, + }; + let mirrors: Vec = match serde_json::from_str(&raw) { + Ok(m) => m, + Err(e) => { + tracing::warn!(error = ?e, "forge: HYPERHIVE_FORGE_MIRRORS is not valid JSON; skipping mirror seed"); + return; + } + }; + for m in mirrors { + let Some((owner, repo)) = m.dest.split_once('/') else { + tracing::warn!(dest = %m.dest, "forge: mirror dest is not /; skipping"); + continue; + }; + // Create the dest org first (idempotent); the mirror can't land + // without its owner existing. + if let Err(e) = ensure_org(owner, admin_token).await { + tracing::warn!(%owner, error = ?e, "forge: ensure_org for mirror failed"); + continue; + } + if let Err(e) = ensure_mirror_repo(&m.upstream, owner, repo, admin_token).await { + tracing::warn!(dest = %m.dest, error = ?e, "forge: ensure_mirror_repo failed"); + } + } +} + +/// Create `owner/repo` as a pull-mirror of `upstream` via the migrate API. +/// Idempotent: a cheap existence check skips an already-seeded mirror (it +/// persists in the non-ephemeral forge state across reboots), and a +/// 409/422 from migrate is also treated as success. +async fn ensure_mirror_repo( + upstream: &str, + owner: &str, + repo: &str, + admin_token: &str, +) -> Result<()> { + let get_url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}"); + let (status, _) = forge_http(reqwest::Method::GET, &get_url, admin_token, "").await?; + if status.is_success() { + tracing::debug!(%owner, %repo, "forge: pull-mirror already present"); + return Ok(()); + } + // serde_json::json! → the upstream URL is escaped safely (no string + // interpolation into the JSON body). + let body = serde_json::json!({ + "clone_addr": upstream, + "repo_owner": owner, + "repo_name": repo, + "mirror": true, + "service": "git", + "private": false, + }) + .to_string(); + let url = format!("{FORGE_HTTP}/api/v1/repos/migrate"); + let (status, text) = forge_http(reqwest::Method::POST, &url, admin_token, &body).await?; + match status.as_u16() { + 201 => { + tracing::info!(%owner, %repo, %upstream, "forge: created pull-mirror"); + Ok(()) + } + 409 | 422 => { + tracing::debug!(%owner, %repo, "forge: pull-mirror already exists"); + Ok(()) + } + other => { + anyhow::bail!("POST /api/v1/repos/migrate {owner}/{repo} returned HTTP {other}: {text}") + } + } +} + /// Whether `ns` is a hive-managed Forgejo namespace that agent-initiated /// repo creation must never target — `internal` (operator-curated /// shared content) + `agent-configs` / `core` (hive-c0re-internal). The @@ -1141,6 +1229,11 @@ pub async fn ensure_all() { tracing::warn!(%org, error = ?e, "forge: ensure_org failed"); } } + // Seed the operator-declared pull-mirrors (nix `forge.mirrors` + + // the CI-auto `actions/checkout`, forwarded via the + // `HYPERHIVE_FORGE_MIRRORS` env). Each ensures its own dest org, so + // this is independent of the SEEDED_ORGS loop above. + ensure_mirrors(token).await; // Provision the operator merge-gate team (empty) inside the agents // org so branch protection can reference it before anyone joins //. The operator adds herself as a member out-of-band. diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index 5f84f9f8..c59e4d9a 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -53,86 +53,6 @@ let ++ lib.optional ( ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors) ) actionCheckoutMirror; - - # Host-side core admin token hive-c0re mints after provisioning the forge - # admin (same file hive-ci-prefetch reads). Root-only; never enters a - # container — so the mirror seed runs host-side, exactly like - # hive-ci-prefetch, rather than minting a second token in-container. - coreTokenPath = "/var/lib/hyperhive/forge-core-token"; - - # Idempotently create each `effectiveMirrors` entry as a real Forgejo - # pull-mirror via the migrate API. Host-side: only talks to the forge on - # loopback (forgejo itself does the upstream clone, so the upstream-DNS - # dependency lives in the container + is off the CI critical path). - # Modelled on hive-ci-prefetch's wait-for-core-token loop. - seedMirrorsScript = pkgs.writeShellScript "forgejo-seed-mirrors" '' - set -uo pipefail - FORGE_URL="http://127.0.0.1:${toString cfg.httpPort}" - - CORE_TOKEN="" - for i in $(seq 1 60); do - if [ -f "${coreTokenPath}" ]; then CORE_TOKEN=$(cat "${coreTokenPath}"); break; fi - echo "forgejo-seed-mirrors: waiting for core token ($i/60)..." >&2 - sleep 1 - done - if [ -z "''${CORE_TOKEN:-}" ]; then - echo "forgejo-seed-mirrors: core token absent after 60s — cannot seed mirrors" >&2 - exit 1 - fi - AUTH="Authorization: token $CORE_TOKEN" - rc=0 - - seed_one() { - upstream="$1"; owner="$2"; repo="$3" - # Ensure the dest org (idempotent: 201 created / 422 already exists). - org_body=$(${pkgs.jq}/bin/jq -nc --arg u "$owner" '{ username: $u }') - ohttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' -X POST \ - -H "$AUTH" -H 'Content-Type: application/json' \ - "$FORGE_URL/api/v1/orgs" -d "$org_body" || echo 000) - case "$ohttp" in - 201 | 422) ;; - *) echo "forgejo-seed-mirrors: ensure org '$owner' returned HTTP $ohttp" >&2 ;; - esac - # Skip if the repo already exists (the mirror persists across reboots - # in the non-ephemeral forge state, so this no-ops on every reboot - # after the first). - rhttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' \ - -H "$AUTH" "$FORGE_URL/api/v1/repos/$owner/$repo" || echo 000) - if [ "$rhttp" = 200 ]; then - echo "forgejo-seed-mirrors: $owner/$repo already present — skipping" >&2 - return 0 - fi - # Create the pull-mirror. service=git → generic git clone of - # clone_addr (no upstream API token needed); mirror=true → forgejo - # keeps it re-syncing on its mirror interval. - mig_body=$(${pkgs.jq}/bin/jq -nc \ - --arg c "$upstream" --arg o "$owner" --arg r "$repo" \ - '{ clone_addr: $c, repo_owner: $o, repo_name: $r, mirror: true, service: "git", private: false }') - resp=$(${pkgs.curl}/bin/curl -s -w $'\n%{http_code}' -X POST \ - -H "$AUTH" -H 'Content-Type: application/json' \ - "$FORGE_URL/api/v1/repos/migrate" \ - -d "$mig_body" \ - || printf '\n000') - mhttp=$(printf '%s' "$resp" | tail -n1) - case "$mhttp" in - 2*) echo "forgejo-seed-mirrors: created pull-mirror $owner/$repo from $upstream" >&2 ;; - *) - echo "forgejo-seed-mirrors: migrate $owner/$repo failed HTTP $mhttp: $(printf '%s' "$resp" | sed '$d')" >&2 - rc=1 - ;; - esac - } - - ${lib.concatMapStringsSep "\n" ( - m: - let - parts = lib.splitString "/" m.dest; - in - "seed_one ${lib.escapeShellArg m.upstream} ${lib.escapeShellArg (builtins.elemAt parts 0)} ${lib.escapeShellArg (builtins.elemAt parts 1)}" - ) effectiveMirrors} - - exit $rc - ''; in { # Private Forgejo in a `hive-forge` nixos-container, shared host @@ -591,26 +511,12 @@ in ]; }; - # Seed the configured pull-mirrors once the forge + core token are up. - # Host-side (the core admin token never enters a container) and modelled - # on hive-ci-prefetch. partOf the forge container so it re-runs (and - # re-ensures, idempotently) on every forge (re)start. Only present when - # there's something to seed. - systemd.services.forgejo-seed-mirrors = lib.mkIf (effectiveMirrors != [ ]) { - description = "Seed Forgejo pull-mirrors (host-side)"; - after = [ - "hive-c0re.service" - "container@hive-forge.service" - ]; - wants = [ "container@hive-forge.service" ]; - wantedBy = [ "multi-user.target" ]; - partOf = [ "container@hive-forge.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = seedMirrorsScript; - SyslogIdentifier = "forgejo-seed-mirrors"; - }; - }; + # Forward the declared pull-mirrors to hive-c0re, which seeds them in + # its forge provisioning sweep (`forge.rs::ensure_mirrors`, alongside + # the SEEDED_ORGS ensure). c0re already holds the core admin token and + # ensures the orgs there, so the seeding lives in one place rather than + # a parallel host-side unit. JSON-encoded list of { upstream, dest }; + # `[]` when nothing to seed (c0re no-ops). + systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS = builtins.toJSON effectiveMirrors; }; } From 64e51fe3bf2fc6691ec136f17336be5cd15f89bd Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 29 Jun 2026 00:19:25 +0200 Subject: [PATCH 6/7] address argus: 422 from migrate is a validation error, not 'exists' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensure_mirror_repo treated 409|422 as success (copied from ensure_org, where 422 *does* mean 'org exists'). For the migrate endpoint 422 is a validation error (bad clone_addr/service); the GET-first check is the real idempotency guard, so 409 stays as a race guard but 422 now falls through to the bail arm (→ caller warns) instead of silently dropping a misconfigured mirror. --- hive-c0re/src/forge.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 2d87cc01..35b69808 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -991,8 +991,12 @@ async fn ensure_mirror_repo( tracing::info!(%owner, %repo, %upstream, "forge: created pull-mirror"); Ok(()) } - 409 | 422 => { - tracing::debug!(%owner, %repo, "forge: pull-mirror already exists"); + // 409 = a race created it between our GET check and here (the GET + // is the real idempotency guard). NOT 422: for the migrate endpoint + // 422 is a validation error (bad clone_addr / service), so it must + // surface via the bail arm, not be swallowed as "already exists". + 409 => { + tracing::debug!(%owner, %repo, "forge: pull-mirror already exists (race)"); Ok(()) } other => { From af7ec98542dd8a5b724dcbb2f5aed12321487326 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 29 Jun 2026 00:20:45 +0200 Subject: [PATCH 7/7] =?UTF-8?q?doc:=20ensure=5Fmirror=5Frepo=20docstring?= =?UTF-8?q?=20=E2=80=94=20409=20only,=20not=20409/422=20(match=20the=20fix?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hive-c0re/src/forge.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 35b69808..85a9d90e 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -959,8 +959,8 @@ async fn ensure_mirrors(admin_token: &str) { /// Create `owner/repo` as a pull-mirror of `upstream` via the migrate API. /// Idempotent: a cheap existence check skips an already-seeded mirror (it -/// persists in the non-ephemeral forge state across reboots), and a -/// 409/422 from migrate is also treated as success. +/// persists in the non-ephemeral forge state across reboots); a 409 (a race +/// between that check and the POST) is also treated as success. async fn ensure_mirror_repo( upstream: &str, owner: &str,