From 1255268f4f6029a03c6c4bec057372ac9ac7b4a0 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 8 Jun 2026 22:36:47 +0200 Subject: [PATCH] refactor(#1456): extract knowledge push-webhook endpoint into dashboard/webhook.rs --- hive-c0re/src/dashboard.rs | 56 +------------------------- hive-c0re/src/dashboard/webhook.rs | 64 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 54 deletions(-) create mode 100644 hive-c0re/src/dashboard/webhook.rs diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 76eb89ef..6f0e050d 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -32,6 +32,7 @@ use crate::lifecycle::{self, MANAGER_NAME}; mod permissions; mod schedules; +mod webhook; #[derive(Clone)] struct AppState { @@ -118,7 +119,7 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { ) .route("/dashboard/stream", get(dashboard_stream)) .route("/dashboard/history", get(dashboard_history)) - .route("/webhook/knowledge", post(post_webhook_knowledge)) + .route("/webhook/knowledge", post(webhook::post_webhook_knowledge)) // Anything not matched by the dynamic routes above falls // through to the bundled dashboard dist (GET / → // dist/index.html, /favicon.svg → dist/favicon.svg, @@ -2755,56 +2756,3 @@ async fn get_approval_diff( fn plain_text(body: String) -> Response { (StatusCode::OK, body).into_response() } - -/// Minimal Forgejo push-webhook payload — only the fields we care about. -#[derive(Deserialize)] -struct PushWebhookPayload { - #[serde(rename = "ref")] - git_ref: Option, - repository: Option, -} - -#[derive(Deserialize)] -struct PushWebhookRepo { - full_name: Option, -} - -/// POST `/webhook/knowledge` — Forgejo push webhook for -/// `internal/knowledge`. Runs `git pull` on the local clone so -/// agents see up-to-date documents on their next turn. -/// -/// Expected Forgejo webhook configuration: -/// - URL: `http://127.0.0.1:/webhook/knowledge` -/// - Event: "Push" (fires on merge commits to main as well) -/// -/// No signature verification for now; the endpoint is loopback-only -/// and only triggers a read-only `git pull` on an operator-curated repo. -async fn post_webhook_knowledge( - axum::extract::Json(payload): axum::extract::Json, -) -> Response { - let expected_repo = format!("{}/{}", crate::knowledge::ORG, crate::knowledge::REPO); - let full_name = payload - .repository - .as_ref() - .and_then(|r| r.full_name.as_deref()) - .unwrap_or(""); - if full_name != expected_repo { - tracing::debug!( - full_name, - "webhook/knowledge: ignoring push from unexpected repo" - ); - return (StatusCode::OK, "ignored").into_response(); - } - let git_ref = payload.git_ref.as_deref().unwrap_or(""); - if git_ref != "refs/heads/main" { - tracing::debug!(git_ref, "webhook/knowledge: ignoring non-main push"); - return (StatusCode::OK, "ignored").into_response(); - } - tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}"); - tokio::spawn(async { - if let Err(e) = crate::knowledge::pull().await { - tracing::warn!(error = ?e, "webhook/knowledge: pull failed"); - } - }); - (StatusCode::OK, "ok").into_response() -} diff --git a/hive-c0re/src/dashboard/webhook.rs b/hive-c0re/src/dashboard/webhook.rs new file mode 100644 index 00000000..a10a4328 --- /dev/null +++ b/hive-c0re/src/dashboard/webhook.rs @@ -0,0 +1,64 @@ +//! Forgejo push-webhook endpoint for the `internal/knowledge` repo. +//! +//! Loopback-only; on a push to `main` of the knowledge repo it triggers a +//! read-only `git pull` on the local clone so agents see up-to-date +//! documents on their next turn. + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::Deserialize; + +/// Minimal Forgejo push-webhook payload — only the fields we care about. +#[derive(Deserialize)] +pub(super) struct PushWebhookPayload { + #[serde(rename = "ref")] + git_ref: Option, + repository: Option, +} + +#[derive(Deserialize)] +pub(super) struct PushWebhookRepo { + full_name: Option, +} + +/// POST `/webhook/knowledge` — Forgejo push webhook for +/// `internal/knowledge`. Runs `git pull` on the local clone so +/// agents see up-to-date documents on their next turn. +/// +/// Expected Forgejo webhook configuration: +/// - URL: `http://127.0.0.1:/webhook/knowledge` +/// - Event: "Push" (fires on merge commits to main as well) +/// +/// No signature verification for now; the endpoint is loopback-only +/// and only triggers a read-only `git pull` on an operator-curated repo. +pub(super) async fn post_webhook_knowledge( + axum::extract::Json(payload): axum::extract::Json, +) -> Response { + let expected_repo = format!("{}/{}", crate::knowledge::ORG, crate::knowledge::REPO); + let full_name = payload + .repository + .as_ref() + .and_then(|r| r.full_name.as_deref()) + .unwrap_or(""); + if full_name != expected_repo { + tracing::debug!( + full_name, + "webhook/knowledge: ignoring push from unexpected repo" + ); + return (StatusCode::OK, "ignored").into_response(); + } + let git_ref = payload.git_ref.as_deref().unwrap_or(""); + if git_ref != "refs/heads/main" { + tracing::debug!(git_ref, "webhook/knowledge: ignoring non-main push"); + return (StatusCode::OK, "ignored").into_response(); + } + tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}"); + tokio::spawn(async { + if let Err(e) = crate::knowledge::pull().await { + tracing::warn!(error = ?e, "webhook/knowledge: pull failed"); + } + }); + (StatusCode::OK, "ok").into_response() +}