-
Notifications
You must be signed in to change notification settings - Fork 0
Description
As an alternative to SSH-in-GHA deploys, we can listen to GH push webhooks and then trigger the deployment script for the site.
A simple GH webhook handler looks something like this:
async fn handle_github_push(
headers: HeaderMap,
state: Extension<Arc<HashMap<String, String>>>,
body: Bytes,
) -> impl IntoResponse {
if let Some(signature) = headers.get("X-Hub-Signature-256") {
let signature = signature.to_str().unwrap_or("");
if verify_signature(&body, signature) {
if let Ok(payload) = serde_json::from_slice::<Value>(&body) {
if let Some(repo_name) = payload["repository"]["full_name"].as_str() {
if let Some(ref_name) = payload["ref"].as_str() {
let (_, branch) = ref_name.split_once("refs/heads/").unwrap();
println!("Push to repository: {} branch {}", repo_name, branch);
let _ = notification(&format!("Push to repository: {} branch {}", repo_name, branch)).await;
if let Some(path) = state.get(&format!("{}:{}", repo_name, branch)) {
if let Err(error) = trigger_repo_deploy_script(path) {
let _ = notification(&format!("Deployment error: {}", error)).await;
return StatusCode::INTERNAL_SERVER_ERROR;
}
let _ = notification(&format!("Deployment successful ({}:{})", repo_name, branch)).await;
StatusCode::OK
} else {
eprintln!("Repository {}:{} does not have a configured path", repo_name, branch);
StatusCode::INTERNAL_SERVER_ERROR
}
} else {
eprintln!("Branch name not found in the payload");
StatusCode::BAD_REQUEST
}
} else {
eprintln!("Repository name not found in the payload");
StatusCode::BAD_REQUEST
}
} else {
eprintln!("Failed to parse payload");
StatusCode::BAD_REQUEST
}
} else {
eprintln!("Signature validation failed");
StatusCode::UNAUTHORIZED
}
} else {
eprintln!("Signature header missing");
StatusCode::BAD_REQUEST
}
}(state maps repo:branch to /absolute/path/deploy.sh). Since we use user-per-site we'd probably want some sudoers config, specifically with runAs so that the user serving this site can switch to another user just to run the deployment script.
This service itself would be a new site served by nginx. It'd be a Go app packaged as a Nix package. With Go we probably don't need any dependencies so it'd be a lot simpler than Rust (no axum, serde, ...). We just need an HTTP server, JSON parsing, and HMAC.
Go makes TLS easy so we wouldn't strictly need nginx but with nginx we get ACME for free on NixOS so having the Go service expose a unix socket and putting that behind nginx would be the easiest — also no 80/443 port contention.