rework(#2072): seed mirrors in c0re startup, not a host-side oneshot

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).
This commit is contained in:
atlas 2026-06-29 00:10:15 +02:00 committed by mara
commit 53df2c9598
2 changed files with 100 additions and 101 deletions

View file

@ -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 `<owner>/<repo>` 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<Mirror> = 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 <owner>/<repo>; 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.