Watch
0
0
Fork
You've already forked hyperhive
0

config PRs: each hive removes its own stale config-PR org hook on boot

The hook a hive registered on the agent-configs org points at its
/webhook/config-pr route, which no hive serves any more, so every
config-repo event fails delivery to it. The forge sweep now deletes it.

Only a hook whose URL equals this hive's own exactly is removed. The same
path on another base belongs to another hive and is left alone. A forge
error is logged and boot continues; once the hook is gone the step is a
no-op.

Closes #4850
This commit is contained in:
atlas 2026-10-02 23:10:35 +02:00
commit 56f592d525
2 changed files with 82 additions and 4 deletions

View file

@ -251,7 +251,8 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
}
/// The `core_token.is_some()` half of [`ensure_all`]: the meta repo, the
/// local knowledge clone, the core avatar, and CI runner registration —
/// local knowledge clone, the core avatar, CI runner registration, and
/// removing this hive's own config-PR hook —
/// every step that needs an authenticated forge client. The swarm-wide
/// objects (orgs, the `operators` team, mirrors, `internal/docs`,
/// `internal/knowledge`, the `agent-configs` avatar) are the
@ -291,6 +292,63 @@ async fn ensure_all_orgs_and_repos(token: &str) {
// Register the hive-ci Actions runner (off the container's boot path;
// no-op when CI is disabled or the runner already holds valid creds).
ci_runner::ensure_ci_runner_registered(token).await;
if let Err(e) = remove_config_pr_webhook(token).await {
tracing::warn!(error = ?e, "forge: remove_config_pr_webhook failed");
}
}
/// Delete this hive's own `/webhook/config-pr` hook from the
/// `agent-configs` org. No hive serves that route, so such a hook fails on
/// every config-repo event; swarm-controller's hook is the one that acts on
/// config PRs.
///
/// Only a hook whose URL equals this hive's exactly is removed, never one
/// matching by path: the same path on another base is another hive's hook.
/// Idempotent, and a no-op when `HYPERHIVE_HIVE_DOMAIN` is unset.
async fn remove_config_pr_webhook(core_token: &str) -> Result<()> {
const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
let Some(domain) = std::env::var("HYPERHIVE_HIVE_DOMAIN")
.ok()
.filter(|v| !v.is_empty())
else {
return Ok(());
};
let own_url = config_pr_hook_url(&domain);
let client = api(core_token)?;
let hooks = tokio::time::timeout(HTTP_TIMEOUT, client.org_list_hooks(CONFIG_ORG).send())
.await
.map_err(anyhow::Error::from)
.and_then(|r| r.map_err(anyhow::Error::from))
.with_context(|| format!("list webhooks for org {CONFIG_ORG}"))?;
for id in hook_ids_at(&hooks, &own_url) {
tokio::time::timeout(HTTP_TIMEOUT, client.org_delete_hook(CONFIG_ORG, id).send())
.await
.map_err(anyhow::Error::from)
.and_then(|r| r.map_err(anyhow::Error::from))
.with_context(|| format!("delete webhook {id} on org {CONFIG_ORG}"))?;
tracing::info!(%own_url, id, "forge: removed this hive's config-pr webhook");
}
Ok(())
}
/// The URL this hive's config-PR hook was registered under.
fn config_pr_hook_url(hive_domain: &str) -> String {
format!("https://{hive_domain}/webhook/config-pr")
}
/// Ids of the hooks whose URL is exactly `url`.
fn hook_ids_at(hooks: &[forgejo_api::structs::Hook], url: &str) -> Vec<i64> {
hooks
.iter()
.filter(|h| {
h.config
.as_ref()
.and_then(|c| c.get("url"))
.is_some_and(|u| u == url)
})
.filter_map(|h| h.id)
.collect()
}
/// Poll the local Forgejo API until it answers, with a timeout of 1 minute.
@ -400,7 +458,27 @@ pub async fn ensure_all() {
#[cfg(test)]
mod tests {
use super::{describe_forge_admin, git_url_with_base};
use super::{config_pr_hook_url, describe_forge_admin, git_url_with_base, hook_ids_at};
fn hook(id: i64, url: &str) -> forgejo_api::structs::Hook {
serde_json::from_value(serde_json::json!({ "id": id, "config": { "url": url } }))
.expect("hook json")
}
/// The same path on another base is another hive's hook, and removing it
/// would cut that hive off. Only an exact URL match is this hive's.
#[test]
fn only_this_hives_own_config_pr_hook_is_removed() {
let own = config_pr_hook_url("hive.example");
let hooks = [
hook(1, &own),
hook(2, "https://other-hive.example/webhook/config-pr"),
hook(3, "http://127.0.0.1:7000/webhook/config-pr"),
hook(4, "https://swarm.example/webhook/forge/config-pr"),
];
assert_eq!(hook_ids_at(&hooks, &own), vec![1]);
assert!(hook_ids_at(&hooks[1..], &own).is_empty());
}
/// The remote carries **no credential**. `argv` is world-readable through
/// `/proc/<pid>/cmdline`, so a token spliced in here would be published to