fix(#2553): mirror agent-config tags + main as separate pushes so a protected-main reject doesn't drop the status tags
This commit is contained in:
parent
bf913df67a
commit
fc00e38490
1 changed files with 53 additions and 31 deletions
|
|
@ -415,27 +415,28 @@ pub async fn ensure_meta_remote(name: &str) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Mirror agent `name`'s applied config repo — `main` plus every tag
|
||||
/// Mirror agent `name`'s applied config repo — every status tag
|
||||
/// (`proposal` / `approved` / `building` / `deployed` / `failed` /
|
||||
/// `denied`) — to `agent-configs/<name>` on the local forge.
|
||||
/// Best-effort: returns Err which callers log + ignore. No-op when the
|
||||
/// forge isn't seeded or the applied repo doesn't exist yet.
|
||||
/// `denied`) plus a best-effort `main` — to `agent-configs/<name>` on
|
||||
/// the local forge. Best-effort: returns Err which callers log + ignore.
|
||||
/// No-op when the forge isn't seeded or the applied repo doesn't exist.
|
||||
///
|
||||
/// Call this after every hive-c0re mutation of an applied repo's refs
|
||||
/// 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.
|
||||
/// **Two separate pushes, not one.** The status tags are id-suffixed
|
||||
/// (`deployed/<id>`, …) and add-only, so they must always land. `main`,
|
||||
/// by contrast, is core-only branch-protected and authoritatively
|
||||
/// advanced by `pr_merge`'s ff-only merge API — so a mirror push of an
|
||||
/// already-established `main` is routinely rejected (protected-branch,
|
||||
/// or non-fast-forward after a rolled-back deploy). Git's pre-receive
|
||||
/// hook is all-or-nothing: bundling both refspecs in one push means that
|
||||
/// `main` reject declines the whole push, dropping the tags too.
|
||||
/// So we push the tags on their own first, then attempt `main`
|
||||
/// separately and swallow the expected reject. The `main` push still
|
||||
/// matters for the initial seed of a fresh (empty) config repo, where it
|
||||
/// creates `main`. Operator-driven divergence fix: the `hivectl forge
|
||||
/// reconcile-config` command.
|
||||
///
|
||||
/// The tokenised URL is passed straight to `git push` and deliberately
|
||||
/// never stored as a named remote: the applied repo is bind-mounted
|
||||
|
|
@ -450,29 +451,34 @@ pub async fn push_config(name: &str) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
let url = forge_git_url(&token, &format!("{CONFIG_ORG}/{name}"));
|
||||
let out = crate::lifecycle::git_command()
|
||||
.current_dir(&dir)
|
||||
.args([
|
||||
"push",
|
||||
&url,
|
||||
"refs/heads/main:refs/heads/main",
|
||||
"refs/tags/*:refs/tags/*",
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("invoke git push agent-configs")?;
|
||||
// Tags first, in their own push, so they land regardless of main's fate.
|
||||
let out = run_config_push(&dir, &url, "refs/tags/*:refs/tags/*").await?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"git push tags {CONFIG_ORG}/{name} failed ({}): {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
// Then main on its own — a protected-branch / non-ff reject of an
|
||||
// established main is expected (pr_merge owns main); only the initial
|
||||
// empty-repo seed actually advances it here.
|
||||
let out = run_config_push(&dir, &url, "refs/heads/main:refs/heads/main").await?;
|
||||
if !out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
if stderr.contains("non-fast-forward") {
|
||||
if stderr.contains("non-fast-forward")
|
||||
|| stderr.contains("protected branch")
|
||||
|| stderr.contains("pre-receive hook declined")
|
||||
{
|
||||
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"
|
||||
"forge: mirror push of main skipped — forge main is protected / \
|
||||
owned by the config-PR ff-merge (expected); tags mirrored"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
anyhow::bail!(
|
||||
"git push {CONFIG_ORG}/{name} failed ({}): {}",
|
||||
"git push main {CONFIG_ORG}/{name} failed ({}): {}",
|
||||
out.status,
|
||||
stderr.trim()
|
||||
);
|
||||
|
|
@ -481,6 +487,22 @@ pub async fn push_config(name: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a single `git push <url> <refspec>` in the applied repo `dir` and
|
||||
/// return the raw output for the caller to classify. Split out so
|
||||
/// [`push_config`] can push tags and `main` as independent pushes.
|
||||
async fn run_config_push(
|
||||
dir: &std::path::Path,
|
||||
url: &str,
|
||||
refspec: &str,
|
||||
) -> Result<std::process::Output> {
|
||||
crate::lifecycle::git_command()
|
||||
.current_dir(dir)
|
||||
.args(["push", url, refspec])
|
||||
.output()
|
||||
.await
|
||||
.context("invoke git push agent-configs")
|
||||
}
|
||||
|
||||
/// Create an org named `name` (`org_create`). Idempotent: HTTP 422
|
||||
/// ("user already exists") / 409 is treated as success.
|
||||
pub(super) async fn ensure_org(name: &str, admin_token: &str) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue