diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index e90cdef4..74f38b25 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -610,18 +610,36 @@ impl Client { event: &str, secret: &str, ) -> Result<()> { + // ⚠️ Both non-matching arms log at a level the journal keeps, because + // this idempotency check fails *silently*: when the list step does + // not recognise a hook that is already there, the create below runs + // again, and that stays harmless only while the forge rejects the + // duplicate. A forge that accepts it leaves one extra hook per + // process start, each delivering on every event — and the create + // arm's line reads exactly like a first, correct registration. + // + // The two ways to reach a create have different causes, so they are + // separated: `listed=0` is a permission or scope problem, a non-zero + // count with no match means the recorded url is not the one compared. match scope.list_hook_urls(&self.api).await { Ok(urls) => { if urls.iter().any(|u| u == target_url) { tracing::debug!(%target_url, "swarm forge: webhook already registered"); return Ok(()); } + tracing::info!( + %target_url, + listed = urls.len(), + "swarm forge: no listed hook matches; registering" + ); } Err(e) => { // Best-effort, same as the per-hive registrars: a forge that // cannot be listed may still accept a create, and a - // duplicate create is folded into success below. - tracing::debug!(error = %e, %target_url, "swarm forge: listing hooks failed; attempting create"); + // duplicate create is folded into success below. `warn!` + // because that fold is an assumption about the forge, not a + // guarantee — see the note above. + tracing::warn!(error = %e, %target_url, "swarm forge: listing hooks failed; registering blind"); } }