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.
This commit is contained in:
atlas 2026-07-11 12:57:06 +02:00
commit c222418d76

View file

@ -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<Option<crate::warnings::WarningGuard>> = 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:<token>` or just
// any-username:<token>; 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