diff --git a/docs/agent-lifecycle/config-changes.md b/docs/agent-lifecycle/config-changes.md index ea41dd98..3ef05de4 100644 --- a/docs/agent-lifecycle/config-changes.md +++ b/docs/agent-lifecycle/config-changes.md @@ -44,10 +44,12 @@ No MCP tool exists for config changes: opening the PR IS the request. deploy subject. With no such hive, or more than one, it deploys nothing and logs a warning naming them. 5. If the hive's `applied/main` already is that commit, it does nothing. - Otherwise it fetches the config repo's `main`, requires the commit to - descend from `applied/main`, fast-forwards `applied/main` to it, and - queues a rebuild. A failure before the rebuild (the fetch, or a commit - that doesn't descend) deploys nothing and logs `swarm events: rev not + Otherwise it fetches the config repo's `main`. A commit that + `applied/main` already contains is a no-op too: requests for two merges + can arrive in either order. A commit that descends from `applied/main` + is fast-forwarded to and a rebuild queued. A failure before the rebuild + (the fetch, or a commit that has diverged from `applied/main`) deploys + nothing and logs `swarm events: rev not applied; nothing queued` at error, which fires the Grafana alert `config rev refused`. An agent with no container on that hive yet ignores the commit; its first deploy builds what it seeds. diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index cc3c074e..841e123e 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -30,6 +30,11 @@ pub(crate) enum RevAdvance { AlreadyApplied, /// `applied/main` was fast-forwarded to the rev. Advanced, + /// The rev is an ancestor of `applied/main`, so a later commit already + /// carries it and there is nothing to deploy. Deploy requests are not + /// ordered: two merges' requests can arrive in either order, and a + /// meta-update cascade fast-forwards `main` to the forge's on its own. + Superseded, } /// Fast-forward `agent`'s `applied/main` to `rev`, a commit the swarm asks @@ -40,8 +45,8 @@ pub(crate) enum RevAdvance { /// /// # Errors /// -/// Returns an error if the forge fetch fails, if `rev` does not descend from -/// `applied/main`, or if `main` moved during the fast-forward. `main` does not +/// Returns an error if the forge fetch fails, if `rev` has diverged from +/// `applied/main` (neither descendant nor ancestor), or if `main` moved during the fast-forward. `main` does not /// move in any of these. pub(crate) async fn advance_applied_to_rev(agent: &str, rev: &str) -> Result { advance_applied_main( @@ -75,6 +80,12 @@ async fn advance_applied_main( return Ok(RevAdvance::AlreadyApplied); } if !descends { + let contained = lifecycle::git_is_ancestor(applied_dir, rev, &main) + .await + .map_err(|e| anyhow::anyhow!("ancestry check {rev}..{main}: {e:#}"))?; + if contained { + return Ok(RevAdvance::Superseded); + } bail!( "{rev} does not descend from applied/main ({main}); fast-forwarding to it would \ discard commits" @@ -211,6 +222,14 @@ mod tests { assert_eq!(main_of(&dir).await, b); } + #[tokio::test] + async fn an_ancestor_of_main_is_superseded_not_refused() { + let (dir, [a, b, _]) = repo().await; + let outcome = advance_applied_main(dir.path(), &a, async { anyhow::Ok(()) }).await; + assert_eq!(outcome.expect("no-op"), RevAdvance::Superseded); + assert_eq!(main_of(&dir).await, b); + } + #[tokio::test] async fn a_descendant_rev_fast_forwards_main() { let (dir, [a, b, _]) = repo().await; diff --git a/hive-c0re/src/swarm_status.rs b/hive-c0re/src/swarm_status.rs index dee3ef13..67f7ac50 100644 --- a/hive-c0re/src/swarm_status.rs +++ b/hive-c0re/src/swarm_status.rs @@ -328,6 +328,10 @@ async fn apply_rev(agent: &str, rev: &str) -> bool { tracing::info!(%agent, %rev, "swarm events: deploy requested for the applied rev; nothing queued"); false } + Ok(crate::actions::RevAdvance::Superseded) => { + tracing::info!(%agent, %rev, "swarm events: deploy requested for a rev applied/main already contains; nothing queued"); + false + } Err(e) => { tracing::error!(%agent, %rev, error = %format!("{e:#}"), "swarm events: rev not applied; nothing queued"); false