hive-c0re: fast-forward applied/<name>/main too, not just the one-shot relock
argus + mara (PR #4339 review): the previous commit's per-agent lock relock is a one-shot effect on the single rebuild the cascade triggers - applied/<name> never moves, so the next relock=true rebuild trigger (the boot sweep, most notably) re-locks against applied/<name> and reverts straight back to whatever it was stuck on. The fix didn't outlive the transaction it ran in. New forge::fast_forward_applied_main(name), sibling to the existing reseed-only fetch_config_main_into_applied: for an applied repo that already has a .git and just needs to catch up, force-set rather than fast-forward-gated since there's no PR to review on this path either. Called per cascade agent right after the relock, best-effort so one unreachable agent repo doesn't block the others. Once applied/<name>/main has actually moved, lock_update_for_rebuild's override (always reads current applied/<name>/main, no ?rev pin) naturally stays in sync on any later relock=true rebuild instead of reverting.
This commit is contained in:
parent
f0ddbe49d0
commit
c45d679a32
3 changed files with 87 additions and 11 deletions
|
|
@ -18,8 +18,8 @@ pub use pr_merge::{
|
||||||
pub use reconcile::{reconcile_config_apply, reconcile_config_status};
|
pub use reconcile::{reconcile_config_apply, reconcile_config_status};
|
||||||
pub use repos::{
|
pub use repos::{
|
||||||
clone_config_into_proposed, create_agent_repo, ensure_config_repo, ensure_knowledge_repo,
|
clone_config_into_proposed, create_agent_repo, ensure_config_repo, ensure_knowledge_repo,
|
||||||
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fetch_config_main_into_applied,
|
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main,
|
||||||
meta_read_access, push_config, push_meta, shared_docs_access,
|
fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access,
|
||||||
};
|
};
|
||||||
pub use users::{core_token, ensure_user_for, provision_user_token};
|
pub use users::{core_token, ensure_user_for, provision_user_token};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -593,6 +593,66 @@ pub async fn fetch_config_main_into_applied(name: &str) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fast-forward `applied/<name>/main` to the config repo's current live
|
||||||
|
/// main. Unlike [`fetch_config_main_into_applied`] (reseed-only — bails
|
||||||
|
/// if `applied` already has a `.git`), this is for an `applied` repo that
|
||||||
|
/// already exists and just needs to catch up: force-sets the local ref to
|
||||||
|
/// match live main rather than requiring a fast-forward or any review
|
||||||
|
/// gate — there is no PR to review on this path, so nothing to check
|
||||||
|
/// ancestry against. Contrast the approval-deploy flow's CAS'd,
|
||||||
|
/// ancestry-gated advance for a reviewed PR merge, which this is not.
|
||||||
|
///
|
||||||
|
/// Best-effort, same shape as [`fetch_config_main_into_applied`]: returns
|
||||||
|
/// `false` (not an error) when the forge is absent, the core token isn't
|
||||||
|
/// minted, `applied` has no `.git` yet (nothing to fast-forward — reseed
|
||||||
|
/// instead), or any git step fails.
|
||||||
|
pub async fn fast_forward_applied_main(name: &str) -> bool {
|
||||||
|
if !is_present().await {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(token) = core_token() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let applied = crate::paths::applied_dir(name);
|
||||||
|
if !applied.join(".git").exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let url = forge_git_url(&format!("{CONFIG_ORG}/{name}"));
|
||||||
|
let auth = core_auth_header(&token);
|
||||||
|
let out = crate::lifecycle::git_command_authed(&auth)
|
||||||
|
.current_dir(&applied)
|
||||||
|
.args(["fetch", "--no-tags", &url, "refs/heads/main"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
match out {
|
||||||
|
Ok(o) if o.status.success() => {}
|
||||||
|
Ok(o) => {
|
||||||
|
tracing::warn!(
|
||||||
|
%name,
|
||||||
|
stderr = %String::from_utf8_lossy(&o.stderr).trim(),
|
||||||
|
"forge: applied fast-forward fetch failed"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(%name, error = ?e, "forge: applied fast-forward fetch failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) =
|
||||||
|
crate::lifecycle::git_update_ref(&applied, "refs/heads/main", "FETCH_HEAD").await
|
||||||
|
{
|
||||||
|
tracing::warn!(%name, error = ?e, "forge: applied fast-forward update-ref failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Err(e) = crate::lifecycle::git_read_tree_reset(&applied, "refs/heads/main").await {
|
||||||
|
tracing::warn!(%name, error = ?e, "forge: applied fast-forward read-tree failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tracing::info!(%name, "forge: fast-forwarded applied/main to agent-configs main");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Seed an agent's `proposed` repo from `agent-configs/<name>` on the forge.
|
/// Seed an agent's `proposed` repo from `agent-configs/<name>` on the forge.
|
||||||
///
|
///
|
||||||
/// When the swarm creates an agent it writes that agent's config to the forge
|
/// When the swarm creates an agent it writes that agent's config to the forge
|
||||||
|
|
|
||||||
|
|
@ -551,19 +551,35 @@ async fn run_meta_lock(
|
||||||
// Pull each cascade agent's own input too — an agent's config-repo main
|
// Pull each cascade agent's own input too — an agent's config-repo main
|
||||||
// is trusted, so a meta-input bump is a reasonable place to also catch
|
// is trusted, so a meta-input bump is a reasonable place to also catch
|
||||||
// it up. Without this, a meta-input bump rebuilds every affected agent
|
// it up. Without this, a meta-input bump rebuilds every affected agent
|
||||||
// against whatever `applied/<name>`
|
// against whatever `applied/<name>` already happened to be locked to —
|
||||||
// already happened to be locked to — normally current, but silently
|
// normally current, but silently stale forever if a past deploy failed
|
||||||
// stale forever if a past deploy failed and nothing since retried it.
|
// and nothing since retried it. Relocks against each agent's *declared*
|
||||||
// Relocks against each agent's *declared* input (the forge URL, not the
|
// input (the forge URL, not the local `applied/<name>` mirror
|
||||||
// local `applied/<name>` mirror `prepare_deploy` uses for a reviewed
|
// `prepare_deploy` uses for a reviewed deploy) — this path has no PR to
|
||||||
// deploy) — this path has no PR to review, so there's nothing to gate.
|
// review, so there's nothing to gate. One combined call, not per-agent:
|
||||||
// One combined call, not per-agent: simpler, at the cost of one
|
// simpler, at the cost of one unreachable/broken agent repo failing the
|
||||||
// unreachable/broken agent repo failing the whole cascade relock rather
|
// whole cascade relock rather than just that agent.
|
||||||
// than just that agent.
|
|
||||||
if !cascade.is_empty() {
|
if !cascade.is_empty() {
|
||||||
let agent_inputs: Vec<String> =
|
let agent_inputs: Vec<String> =
|
||||||
cascade.iter().map(|name| format!("agent-{name}")).collect();
|
cascade.iter().map(|name| format!("agent-{name}")).collect();
|
||||||
crate::meta::lock_update(&agent_inputs).await?;
|
crate::meta::lock_update(&agent_inputs).await?;
|
||||||
|
// The relock above is a one-shot effect on *this* rebuild only —
|
||||||
|
// `applied/<name>` itself hasn't moved, so the next `relock = true`
|
||||||
|
// rebuild trigger (the boot sweep, most notably) re-locks against
|
||||||
|
// `applied/<name>` and reverts straight back to whatever it was
|
||||||
|
// stuck on. Fast-forward each cascade agent's own applied mirror
|
||||||
|
// too so a later rebuild through a different trigger doesn't undo
|
||||||
|
// this one. Best-effort per agent (unlike the relock above): one
|
||||||
|
// agent's forge repo being briefly unreachable here shouldn't be
|
||||||
|
// fatal to the cascade that already relocked its input above.
|
||||||
|
for name in &cascade {
|
||||||
|
if !crate::forge::fast_forward_applied_main(name).await {
|
||||||
|
tracing::warn!(
|
||||||
|
%name,
|
||||||
|
"meta-update cascade: applied/main fast-forward skipped or failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Grow one rebuild subgraph per affected agent into *this* meta-update
|
// Grow one rebuild subgraph per affected agent into *this* meta-update
|
||||||
// DAG (rooted on this `MetaLock`, so they build against the post-bump
|
// DAG (rooted on this `MetaLock`, so they build against the post-bump
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue