robson-gateway-webhook 0.1.0

Rust async agent orchestrator for automated development workflows
Documentation
pub mod db;
pub mod entities;
pub mod gateway;
pub mod migration;
pub mod webhook;

use axum::{
    http::{header, HeaderValue, Method},
    routing::post,
    Router,
};
use sea_orm::DatabaseConnection;
use tokio::sync::watch;
use tower_http::cors::CorsLayer;
use tracing::info;
use webhook::agent_webhook;

pub use gateway::WebhookGateway;

#[derive(Clone)]
pub struct WebAppState {
    pub db: DatabaseConnection,
    pub shutdown_tx: watch::Sender<bool>,
}

pub async fn serve(db: DatabaseConnection, port: u16) -> anyhow::Result<()> {
    serve_with_extra(db, port, None).await
}

pub async fn serve_with_extra(
    db: DatabaseConnection,
    port: u16,
    extra: Option<axum::Router>,
) -> anyhow::Result<()> {
    let (shutdown_tx, _) = watch::channel(false);
    let state = WebAppState {
        db,
        shutdown_tx: shutdown_tx.clone(),
    };

    let mut app = build_router_with_state(state);
    if let Some(extra_router) = extra {
        app = app.merge(extra_router);
    }

    let addr = format!("0.0.0.0:{}", port);
    info!("Webhook server listening on {}", addr);

    let listener = tokio::net::TcpListener::bind(&addr).await?;
    axum::serve(listener, app)
        .with_graceful_shutdown(async move {
            shutdown_signal().await;
            info!("Shutdown signal received — stopping webhook server");
            let _ = shutdown_tx.send(true);
        })
        .await?;

    Ok(())
}

pub fn build_router(db: DatabaseConnection) -> Router {
    let (shutdown_tx, _) = watch::channel(false);
    let state = WebAppState { db, shutdown_tx };
    build_router_with_state(state)
}

fn build_router_with_state(state: WebAppState) -> Router {
    let cors = CorsLayer::new()
        .allow_origin("http://localhost:5173".parse::<HeaderValue>().unwrap())
        .allow_methods([
            Method::GET,
            Method::POST,
            Method::PUT,
            Method::DELETE,
            Method::OPTIONS,
        ])
        .allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]);

    Router::new()
        .route("/webhook", post(agent_webhook))
        .layer(cors)
        .with_state(state)
}

async fn shutdown_signal() {
    let ctrl_c = async {
        tokio::signal::ctrl_c()
            .await
            .expect("Failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .expect("Failed to install SIGTERM handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {}
        _ = terminate => {}
    }
}