config mirror: never force-push — keep forge history on rolled-back deploys

This commit is contained in:
damocles 2026-07-03 00:24:52 +02:00 committed by mara
commit f310b1ce5a
2 changed files with 31 additions and 9 deletions

View file

@ -57,9 +57,11 @@ Two things live in the `agent-configs` Forgejo organization:
`main` is branch-protected core-only: only hive-c0re's verify-and-ff-push
merge handler lands on `main`, an operator-team approval is required, and
the agent can neither push `main` directly nor self-merge. `main` is
fast-forward-only — no auto force-push (the merge handler's ff push lands
fine; the legacy `push_config` mirror, which force-pushes to re-point status
tags and rewind on rollback, runs best-effort until the PR flow retires it).
fast-forward-only — hive-c0re never force-pushes (the merge handler's ff
push lands fine; the `push_config` mirror pushes `main` + the add-only
status tags without force, and treats a non-fast-forward rejection of
`main` after a rolled-back deploy as expected — the forge keeps the
approved history, the `failed/<id>` tag records the divergence).
Repos stay private, so an agent can't read another
agent's config. (Agents remain read-only collaborators on `core/meta`.)
- The dashboard links each container's "config" anchor to this

View file

@ -37,8 +37,8 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
/// branch-protected core-only, so only hive-c0re's verify-and-ff-push merge
/// handler lands on it (operator approval required; the agent can't push
/// `main` or self-merge). The repos remain private, so an agent still can't
/// reach *another* agent's config. `main` is fast-forward-only (no auto
/// force-push); the legacy `push_config` mirror runs best-effort until the
/// reach *another* agent's config. `main` is fast-forward-only — hive-c0re
/// never force-pushes; the `push_config` mirror runs best-effort until the
/// PR-merge flow retires it.
const CONFIG_ORG: &str = "agent-configs";
/// Forgejo org hosting the operator-curated shared docs/skills repo
@ -855,8 +855,20 @@ pub async fn ensure_meta_remote(name: &str) -> Result<()> {
/// forge isn't seeded or the applied repo doesn't exist yet.
///
/// Call this after every hive-c0re mutation of an applied repo's refs
/// so the forge copy always reflects what core actually did. `--force`
/// because a failed build rolls `main` backwards to the last-good sha.
/// so the forge copy always reflects what core actually did.
///
/// Never force-pushes. The status tags are id-suffixed
/// (`proposal/<id>`, `deployed/<id>`, …) and therefore add-only, and
/// `main` is published history — after a failed deploy rolls the LOCAL
/// applied `main` back to last-good, the forge `main` may legitimately
/// be ahead (e.g. an operator-merged config PR whose rebuild failed).
/// Rewinding it would erase that merged commit from the forge, which
/// is exactly the incident this guards against: the local repo tracks
/// "what last built", the forge tracks "what was approved", and the
/// `failed/<id>` tag records the divergence. A non-fast-forward
/// rejection of `main` is therefore expected + logged at info; the
/// tags in the same push still land (git pushes refspecs
/// independently). Any other failure is a real error.
///
/// The tokenised URL is passed straight to `git push` and deliberately
/// never stored as a named remote: the applied repo is bind-mounted
@ -875,7 +887,6 @@ pub async fn push_config(name: &str) -> Result<()> {
.current_dir(&dir)
.args([
"push",
"--force",
&url,
"refs/heads/main:refs/heads/main",
"refs/tags/*:refs/tags/*",
@ -884,10 +895,19 @@ pub async fn push_config(name: &str) -> Result<()> {
.await
.context("invoke git push agent-configs")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("non-fast-forward") {
tracing::info!(
%name,
"forge: mirror push of main rejected (non-fast-forward) — forge main is \
ahead of local applied main (rolled-back deploy); leaving forge history intact"
);
return Ok(());
}
anyhow::bail!(
"git push {CONFIG_ORG}/{name} failed ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
stderr.trim()
);
}
tracing::info!(%name, "forge: mirrored applied config to agent-configs");