From c222418d7609628b0f660ddde05582fe97bed383 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 11 Jul 2026 12:57:06 +0200 Subject: [PATCH] fix(#2380): push_meta drop --force + dashboard warning for non-fast-forward If the remote is ahead of our local mirror (non-fast-forward), the old code used --force which silently destroyed remote history. Fix: - Drop --force from the git push invocation. - On non-ff exit, detect the condition and return Ok(()) instead of bailing (intentional no-op; leaving remote history intact is correct). - Raise a persistent dashboard warning banner via crate::warnings so the operator sees it in the UI rather than having to grep the journal. - Clear the banner on the next successful push. Closes #2380. --- hive-c0re/src/forge/repos.rs | 43 +++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/hive-c0re/src/forge/repos.rs b/hive-c0re/src/forge/repos.rs index 2bc5c186..452b4178 100644 --- a/hive-c0re/src/forge/repos.rs +++ b/hive-c0re/src/forge/repos.rs @@ -5,6 +5,7 @@ //! constructor + org-name constants live in the module root (`super`). use std::path::Path; +use std::sync::Mutex; use anyhow::{Context, Result}; use forgejo_api::structs::{ @@ -188,6 +189,11 @@ async fn ensure_org_repo(org: &str, name: &str, token: &str) -> Result<()> { created_or_exists(res, &format!("{org}/{name}")) } +/// Dashboard-warning guard for the meta non-fast-forward condition. +/// Held while `core/meta` remote is ahead of our local mirror; cleared +/// automatically when the next push succeeds. +static META_NON_FF_GUARD: Mutex> = Mutex::new(None); + /// Push `dir` (the meta repo) to `core/meta` on the local forge. /// Best-effort: returns Err which callers log + ignore. No-op when /// the core token isn't present yet (forge container not provisioned). @@ -198,20 +204,47 @@ pub async fn push_meta(dir: &Path) -> Result<()> { // Token-in-URL push. Forgejo accepts `oauth2:` or just // any-username:; using `core` matches the owner so the // remote name is self-describing. + // + // No --force: `meta.rs` uses regular `git commit` (append-only), + // so the push is always fast-forward in normal operation. Per + // operator directive, hive-c0re must not force-push anywhere. If + // the remote is somehow ahead (e.g. split-brain or manual push) we + // raise a dashboard warning and leave the remote intact rather than + // silently erasing its history — consistent with `push_config`'s + // non-fast-forward handling. let url = forge_git_url(&token, "core/meta"); let out = Command::new("git") .current_dir(dir) - .args(["push", "--force", &url, "HEAD:main"]) + .args(["push", &url, "HEAD:main"]) .output() .await .context("invoke git push core/meta")?; if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr); + if stderr.contains("non-fast-forward") || stderr.contains("fetch first") { + let msg = "forge/core/meta push rejected (non-fast-forward): \ + remote is ahead of local; leaving remote history intact"; + tracing::warn!("{msg}"); + // Raise a persistent dashboard banner; cleared on next successful push. + if let Ok(mut g) = META_NON_FF_GUARD.lock() { + *g = Some(crate::warnings::set_warning( + "forge-meta-non-ff", + "warn", + msg, + )); + } + return Ok(()); + } anyhow::bail!( "git push core/meta failed ({}): {}", out.status, - String::from_utf8_lossy(&out.stderr).trim() + stderr.trim() ); } + // Successful push: clear any outstanding non-ff warning. + if let Ok(mut g) = META_NON_FF_GUARD.lock() { + *g = None; + } tracing::info!("forge: pushed meta to core/meta"); Ok(()) } @@ -770,10 +803,8 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> /// the team) cannot self-approve. /// - **force-pushing `main` stays impossible** — `main` only ever advances by /// fast-forward. The merge handler's `ff_push_to_main` is already a -/// non-force push, so it lands fine. The legacy `push_config` mirror DOES -/// force-push (it re-points status tags and rewinds `main` on a failed-build -/// rollback), so the protection rejects those non-ff updates — that -/// mirror runs best-effort until the agent-opened PR-merge flow retires it. +/// non-force push, so it lands fine. The legacy `push_config` mirror is +/// also ff-only (non-fast-forward is caught and silently skipped). /// (Auto force-push is intentionally not allowed: per operator directive a /// silent force-push is a bug, not a feature. The raw-HTTP predecessor /// sent `"enable_force_push":false` + `"allow_manual_merge":true` in this