From 53df2c9598a3a6285955e34f8e0394df27e9fe71 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 29 Jun 2026 00:10:15 +0200 Subject: [PATCH] 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; }; }