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:
parent
a88ed9f24e
commit
56f592d525
2 changed files with 82 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue