//! 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() }