From 5d597d92ea30e6e08fe555f197ded8a4cbc4aeb6 Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 3 Jul 2026 22:26:24 +0200 Subject: [PATCH] fix(#2174): create operators team in agent-configs org too so config-repo branch protection applies --- hive-c0re/src/forge.rs | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 6dfe32f9..fe44824b 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -1059,29 +1059,36 @@ pub fn is_hive_managed_namespace(ns: &str) -> bool { HIVE_MANAGED_NAMESPACES.contains(&ns) } -/// Provision the [`OPERATORS_TEAM`] inside [`AGENTS_ORG`] as an **empty** -/// team. Branch protection on agents-org repos references it as the +/// Provision the [`OPERATORS_TEAM`] inside `org` as an **empty** team. +/// Branch protection on that org's repos references it as the /// merge/approval whitelist; the operator adds herself as a member via the /// forge UI / hivectl. `includes_all_repositories` so the gate applies to -/// every agent repo; `write` is enough to approve + merge. hive-c0re never -/// manages membership. Idempotent (422/409 = already exists). -async fn ensure_operators_team(token: &str) -> Result<()> { - let url = format!("{FORGE_HTTP}/api/v1/orgs/{AGENTS_ORG}/teams"); +/// every repo in the org; `write` is enough to approve + merge. hive-c0re +/// never manages membership. Idempotent (422/409 = already exists). +/// +/// Must run for BOTH [`AGENTS_ORG`] and [`CONFIG_ORG`]: Gitea teams are +/// org-scoped, so a config-repo branch-protection rule referencing +/// `operators` needs the team to exist in `agent-configs` too. Missing it +/// there 422'd every `apply_config_repo_branch_protection`, leaving config +/// repos unprotected — operator-merged config PRs then bypassed the deploy +/// pipeline and silently didn't apply. +async fn ensure_operators_team(org: &str, token: &str) -> Result<()> { + let url = format!("{FORGE_HTTP}/api/v1/orgs/{org}/teams"); let body = format!( r#"{{"name":"{OPERATORS_TEAM}","description":"hyperhive operators — merge gate for agent repos","permission":"write","includes_all_repositories":true,"can_create_org_repo":false}}"# ); let (status, _) = forge_http(reqwest::Method::POST, &url, token, &body).await?; match status.as_u16() { 201 => { - tracing::info!("forge: created {OPERATORS_TEAM} team in {AGENTS_ORG}"); + tracing::info!(%org, "forge: created {OPERATORS_TEAM} team"); Ok(()) } 409 | 422 => { - tracing::debug!("forge: {OPERATORS_TEAM} team already exists"); + tracing::debug!(%org, "forge: {OPERATORS_TEAM} team already exists"); Ok(()) } other => { - anyhow::bail!("POST /orgs/{AGENTS_ORG}/teams ({OPERATORS_TEAM}) returned HTTP {other}") + anyhow::bail!("POST /orgs/{org}/teams ({OPERATORS_TEAM}) returned HTTP {other}") } } } @@ -1282,11 +1289,17 @@ pub async fn ensure_all() { // `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. - if let Err(e) = ensure_operators_team(token).await { - tracing::warn!(error = ?e, "forge: ensure_operators_team failed"); + // Provision the operator merge-gate team (empty) inside BOTH the + // agents org and the agent-configs org so branch protection in each + // can reference it before anyone joins. Gitea teams are org-scoped — + // missing the agent-configs copy 422'd every config-repo protection + // apply, leaving those repos unprotected and letting operator-merged + // config PRs bypass the deploy pipeline. The operator adds herself as + // a member out-of-band. + for org in [AGENTS_ORG, CONFIG_ORG] { + if let Err(e) = ensure_operators_team(org, token).await { + tracing::warn!(%org, error = ?e, "forge: ensure_operators_team failed"); + } } // Meta repo lives at core/meta — pushed from git_commit in // meta.rs on every deploy/lock-update. Make sure it exists