Add webhook crate for extensible signature validation and integration

- Introduce a new `webhook` crate to centralize signature validation for GitHub, Hookdeck, and Forgejo webhooks.
- Enable `github-integration` to perform unified webhook signature verification using the `webhook` crate.
- Refactor `github-integration`: replace legacy HMAC verification with the reusable `webhook` structure.
- Extend Podman configuration for Hookdeck webhook signature handling and improve documentation.
- Clean up unused dependencies by migrating to the new implementation.

Signed-off-by: Till Wegmueller <toasterson@gmail.com>
This commit is contained in:
Till Wegmueller 2026-01-25 22:16:11 +01:00
parent e33ddf2892
commit 4c5a8567a4
No known key found for this signature in database
8 changed files with 419 additions and 63 deletions

1
.idea/solstice-ci.iml generated
View file

@ -12,6 +12,7 @@
<sourceFolder url="file://$MODULE_DIR$/crates/workflow-runner/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/crates/workflow-runner/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/migration/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/crates/migration/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/logs-service/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/crates/logs-service/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/webhook/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
webhook = { path = "../webhook" }
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
miette = { version = "7", features = ["fancy"] } miette = { version = "7", features = ["fancy"] }
tracing = "0.1" tracing = "0.1"
@ -14,10 +15,6 @@ axum = { version = "0.8", features = ["macros"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
# Signature verification
hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
# GitHub App auth # GitHub App auth
jsonwebtoken = "9" jsonwebtoken = "9"
time = { version = "0.3", features = ["formatting"] } time = { version = "0.3", features = ["formatting"] }

View file

@ -13,12 +13,11 @@ use axum::{
use base64::Engine; use base64::Engine;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use futures_util::StreamExt; use futures_util::StreamExt;
use hmac::{Hmac, Mac};
use jsonwebtoken::{EncodingKey, Header}; use jsonwebtoken::{EncodingKey, Header};
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use webhook::{SignatureCheck, SignaturePolicy, WebhookError, WebhookInfo};
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Cmd { enum Cmd {
@ -55,6 +54,10 @@ struct Opts {
#[arg(long, env = "GITHUB_WEBHOOK_SECRET")] #[arg(long, env = "GITHUB_WEBHOOK_SECRET")]
webhook_secret: Option<String>, webhook_secret: Option<String>,
/// Hookdeck signing secret (proxy signature)
#[arg(long, env = "HOOKDECK_SIGNING_SECRET")]
hookdeck_signing_secret: Option<String>,
/// GitHub API base (e.g., https://api.github.com) /// GitHub API base (e.g., https://api.github.com)
#[arg(long, env = "GITHUB_API_BASE", default_value = "https://api.github.com")] #[arg(long, env = "GITHUB_API_BASE", default_value = "https://api.github.com")]
github_api_base: String, github_api_base: String,
@ -125,6 +128,7 @@ struct Opts {
struct AppState { struct AppState {
mq_cfg: common::MqConfig, mq_cfg: common::MqConfig,
webhook_secret: Option<String>, webhook_secret: Option<String>,
hookdeck_signing_secret: Option<String>,
github_api_base: String, github_api_base: String,
app_id: Option<u64>, app_id: Option<u64>,
app_key_pem: Option<String>, app_key_pem: Option<String>,
@ -137,8 +141,6 @@ struct AppState {
runs_on_map: std::collections::HashMap<String, String>, // key: owner/repo runs_on_map: std::collections::HashMap<String, String>, // key: owner/repo
} }
type HmacSha256 = Hmac<Sha256>;
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Load internal config (preloads KDL -> env, then reads env) // Load internal config (preloads KDL -> env, then reads env)
@ -180,10 +182,11 @@ async fn main() -> Result<()> {
let webhook_secret = opts let webhook_secret = opts
.webhook_secret .webhook_secret
.or_else(|| std::env::var("WEBHOOK_SECRET").ok()); .or_else(|| std::env::var("WEBHOOK_SECRET").ok());
if webhook_secret.is_none() { let hookdeck_signing_secret = opts
warn!( .hookdeck_signing_secret
"GITHUB_WEBHOOK_SECRET is not set — accepting webhooks without signature validation (dev mode)" .or_else(|| std::env::var("HOOKDECK_SECRET").ok());
); if webhook_secret.is_none() && hookdeck_signing_secret.is_none() {
warn!("GITHUB_WEBHOOK_SECRET and HOOKDECK_SIGNING_SECRET are not set — accepting webhooks without signature validation (dev mode)");
} }
let app_key_pem = match (&opts.app_key_pem, &opts.app_key_path) { let app_key_pem = match (&opts.app_key_pem, &opts.app_key_path) {
@ -202,6 +205,7 @@ async fn main() -> Result<()> {
let state = Arc::new(AppState { let state = Arc::new(AppState {
mq_cfg, mq_cfg,
webhook_secret, webhook_secret,
hookdeck_signing_secret,
github_api_base: opts.github_api_base, github_api_base: opts.github_api_base,
app_id: opts.app_id, app_id: opts.app_id,
app_key_pem, app_key_pem,
@ -455,56 +459,69 @@ async fn handle_webhook(
headers: HeaderMap, headers: HeaderMap,
body: Bytes, body: Bytes,
) -> impl IntoResponse { ) -> impl IntoResponse {
// Signature validation (X-Hub-Signature-256) let info = WebhookInfo::from_headers(&headers);
if let Some(secret) = state.webhook_secret.as_ref() { let checks = signature_checks(&state);
if let Some(sig_hdr) = headers.get("X-Hub-Signature-256") { let event = info
let sig = match sig_hdr.to_str() { .github
Ok(s) => s.trim(), .event
Err(_) => return StatusCode::UNAUTHORIZED, .as_deref()
};
if !verify_github_hmac(sig, secret.as_bytes(), &body) {
warn!("invalid webhook signature");
return StatusCode::UNAUTHORIZED;
}
} else {
warn!("missing signature header");
return StatusCode::UNAUTHORIZED;
}
}
let event = headers
.get("X-GitHub-Event")
.and_then(|v| v.to_str().ok())
.unwrap_or("") .unwrap_or("")
.to_lowercase(); .to_ascii_lowercase();
match event.as_str() { match event.as_str() {
"push" => handle_push(state, body).await, "push" => match webhook::extract_json::<PushPayload>(
"pull_request" => handle_pull_request(state, body).await, &headers,
"ping" => StatusCode::OK, body.as_ref(),
&checks,
SignaturePolicy::Any,
) {
Ok(extract) => handle_push(state, extract.payload).await,
Err(err) => map_webhook_error(err),
},
"pull_request" => match webhook::extract_json::<PrPayload>(
&headers,
body.as_ref(),
&checks,
SignaturePolicy::Any,
) {
Ok(extract) => handle_pull_request(state, extract.payload).await,
Err(err) => map_webhook_error(err),
},
"ping" => match webhook::verify_signatures(
&headers,
body.as_ref(),
&checks,
SignaturePolicy::Any,
) {
Ok(_) => StatusCode::OK,
Err(err) => map_webhook_error(WebhookError::Signature(err)),
},
_ => StatusCode::NO_CONTENT, _ => StatusCode::NO_CONTENT,
} }
} }
fn verify_github_hmac(sig_hdr: &str, secret: &[u8], body: &[u8]) -> bool { fn signature_checks(state: &AppState) -> Vec<SignatureCheck> {
let sig = sig_hdr.strip_prefix("sha256=").unwrap_or(sig_hdr); let mut checks = Vec::new();
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size"); if let Some(secret) = state.webhook_secret.as_deref() {
mac.update(body); checks.push(SignatureCheck::github(secret));
match hex::decode(sig) {
Ok(sig) => mac.verify_slice(&sig).is_ok(),
Err(_) => false,
} }
if let Some(secret) = state.hookdeck_signing_secret.as_deref() {
checks.push(SignatureCheck::hookdeck(secret));
}
checks
} }
async fn handle_push(state: Arc<AppState>, body: Bytes) -> StatusCode { fn map_webhook_error(err: WebhookError) -> StatusCode {
let payload: PushPayload = match serde_json::from_slice(&body) { let (status, message) = match &err {
Ok(p) => p, WebhookError::Signature(_) => (StatusCode::UNAUTHORIZED, "webhook signature check failed"),
Err(e) => { WebhookError::Json(_) => (StatusCode::BAD_REQUEST, "failed to parse webhook payload"),
warn!(error = %e, "failed to parse push payload");
return StatusCode::BAD_REQUEST;
}
}; };
let report = miette::Report::new(err);
warn!(error = %report, "{message}");
status
}
async fn handle_push(state: Arc<AppState>, payload: PushPayload) -> StatusCode {
// Ignore delete events (after = all zeros) // Ignore delete events (after = all zeros)
let is_delete = payload.after.chars().all(|c| c == '0'); let is_delete = payload.after.chars().all(|c| c == '0');
if is_delete { if is_delete {
@ -561,15 +578,7 @@ async fn handle_push(state: Arc<AppState>, body: Bytes) -> StatusCode {
} }
} }
async fn handle_pull_request(state: Arc<AppState>, body: Bytes) -> StatusCode { async fn handle_pull_request(state: Arc<AppState>, payload: PrPayload) -> StatusCode {
let payload: PrPayload = match serde_json::from_slice(&body) {
Ok(p) => p,
Err(e) => {
warn!(error = %e, "failed to parse pull_request payload");
return StatusCode::BAD_REQUEST;
}
};
// Only act on opened/synchronize/reopened // Only act on opened/synchronize/reopened
match payload.action.as_str() { match payload.action.as_str() {
"opened" | "synchronize" | "reopened" => {} "opened" | "synchronize" | "reopened" => {}

15
crates/webhook/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "webhook"
version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22"
hex = "0.4"
hmac = "0.12"
http = "1"
miette = { version = "7", features = ["fancy"] }
serde = "1"
serde_json = "1"
sha2 = "0.10"
thiserror = "2"

331
crates/webhook/src/lib.rs Normal file
View file

@ -0,0 +1,331 @@
use std::net::IpAddr;
use base64::Engine;
use hmac::{Hmac, Mac};
use http::HeaderMap;
use serde::de::DeserializeOwned;
use sha2::Sha256;
use miette::Diagnostic;
use thiserror::Error;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureSource {
Github,
Hookdeck,
Forgejo,
}
#[derive(Debug, Clone, Copy)]
pub enum SignatureFormat {
Hex { prefix: Option<&'static str> },
Base64,
}
#[derive(Debug, Clone)]
pub struct SignatureCheck {
source: SignatureSource,
headers: &'static [&'static str],
format: SignatureFormat,
secret: Vec<u8>,
}
impl SignatureCheck {
pub fn github(secret: impl AsRef<[u8]>) -> Self {
Self {
source: SignatureSource::Github,
headers: &["X-Hub-Signature-256"],
format: SignatureFormat::Hex {
prefix: Some("sha256="),
},
secret: secret.as_ref().to_vec(),
}
}
pub fn hookdeck(secret: impl AsRef<[u8]>) -> Self {
Self {
source: SignatureSource::Hookdeck,
headers: &["X-Hookdeck-Signature"],
format: SignatureFormat::Base64,
secret: secret.as_ref().to_vec(),
}
}
pub fn forgejo(secret: impl AsRef<[u8]>) -> Self {
Self {
source: SignatureSource::Forgejo,
headers: &["X-Gitea-Signature", "X-Forgejo-Signature"],
format: SignatureFormat::Hex { prefix: None },
secret: secret.as_ref().to_vec(),
}
}
fn verify(&self, headers: &HeaderMap, body: &[u8]) -> Result<(), SignatureCheckError> {
let mut saw_header = false;
for name in self.headers {
if let Some(value) = headers.get(*name) {
saw_header = true;
let sig = value.to_str().map_err(|_| SignatureCheckError::Invalid)?;
let ok = match self.format {
SignatureFormat::Hex { prefix } => {
verify_hmac_hex(sig, prefix, &self.secret, body)
}
SignatureFormat::Base64 => verify_hmac_base64(sig, &self.secret, body),
};
if ok {
return Ok(());
}
}
}
if saw_header {
Err(SignatureCheckError::Invalid)
} else {
Err(SignatureCheckError::Missing)
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum SignaturePolicy {
Any,
All,
}
#[derive(Debug, Clone)]
pub enum SignatureStatus {
Verified(Vec<SignatureSource>),
Skipped,
}
#[derive(Debug, Error, Diagnostic)]
pub enum SignatureError {
#[error("missing signature headers for {0:?}")]
#[diagnostic(
code("webhook.signature.missing"),
help("Ensure the webhook sender or proxy includes the expected signature header(s) for one of the enabled signature sources.")
)]
Missing(Vec<SignatureSource>),
#[error("invalid signature for {0:?}")]
#[diagnostic(
code("webhook.signature.invalid"),
help("Check that the signing secret matches the sender/proxy configuration and that the request body is unmodified.")
)]
Invalid(Vec<SignatureSource>),
}
#[derive(Debug, Error, Diagnostic)]
enum SignatureCheckError {
#[error("missing signature header")]
#[diagnostic(
code("webhook.signature.header_missing"),
help("Configure the webhook sender or proxy to include the expected signature header(s).")
)]
Missing,
#[error("signature verification failed")]
#[diagnostic(
code("webhook.signature.verify_failed"),
help("Confirm the signing secret and ensure the request body is not modified by intermediaries.")
)]
Invalid,
}
pub fn verify_signatures(
headers: &HeaderMap,
body: &[u8],
checks: &[SignatureCheck],
policy: SignaturePolicy,
) -> Result<SignatureStatus, SignatureError> {
if checks.is_empty() {
return Ok(SignatureStatus::Skipped);
}
let mut verified = Vec::new();
let mut missing = Vec::new();
let mut invalid = Vec::new();
for check in checks {
match check.verify(headers, body) {
Ok(()) => verified.push(check.source),
Err(SignatureCheckError::Missing) => missing.push(check.source),
Err(SignatureCheckError::Invalid) => invalid.push(check.source),
}
}
match policy {
SignaturePolicy::Any => {
if let Some(first) = verified.first().copied() {
return Ok(SignatureStatus::Verified(vec![first]));
}
if !invalid.is_empty() {
return Err(SignatureError::Invalid(invalid));
}
Err(SignatureError::Missing(missing))
}
SignaturePolicy::All => {
if invalid.is_empty() && missing.is_empty() {
return Ok(SignatureStatus::Verified(verified));
}
if !invalid.is_empty() {
return Err(SignatureError::Invalid(invalid));
}
Err(SignatureError::Missing(missing))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct GithubInfo {
pub event: Option<String>,
pub delivery: Option<String>,
pub hook_id: Option<u64>,
pub installation_target_id: Option<u64>,
pub installation_target_type: Option<String>,
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct HookdeckInfo {
pub event_id: Option<String>,
pub request_id: Option<String>,
pub idempotency_key: Option<String>,
pub original_ip: Option<IpAddr>,
pub verified: Option<bool>,
pub event_url: Option<String>,
pub source_name: Option<String>,
pub connection_name: Option<String>,
pub destination_name: Option<String>,
pub attempt_count: Option<u32>,
pub attempt_trigger: Option<String>,
pub will_retry_after: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct WebhookInfo {
pub github: GithubInfo,
pub hookdeck: HookdeckInfo,
pub content_type: Option<String>,
pub content_length: Option<u64>,
}
impl WebhookInfo {
pub fn from_headers(headers: &HeaderMap) -> Self {
let github = GithubInfo {
event: header_string(headers, "X-GitHub-Event"),
delivery: header_string(headers, "X-GitHub-Delivery"),
hook_id: header_u64(headers, "X-GitHub-Hook-Id"),
installation_target_id: header_u64(headers, "X-GitHub-Hook-Installation-Target-Id"),
installation_target_type: header_string(headers, "X-GitHub-Hook-Installation-Target-Type"),
user_agent: header_string(headers, "User-Agent"),
};
let hookdeck = HookdeckInfo {
event_id: header_string(headers, "X-Hookdeck-EventId"),
request_id: header_string(headers, "X-Hookdeck-RequestId"),
idempotency_key: header_string(headers, "Idempotency-Key"),
original_ip: header_ip(headers, "X-Hookdeck-Original-Ip"),
verified: header_bool(headers, "X-Hookdeck-Verified"),
event_url: header_string(headers, "X-Hookdeck-Event-Url"),
source_name: header_string(headers, "X-Hookdeck-Source-Name"),
connection_name: header_string(headers, "X-Hookdeck-Connection-Name"),
destination_name: header_string(headers, "X-Hookdeck-Destination-Name"),
attempt_count: header_u32(headers, "X-Hookdeck-Attempt-Count"),
attempt_trigger: header_string(headers, "X-Hookdeck-Attempt-Trigger"),
will_retry_after: header_string(headers, "X-Hookdeck-Will-Retry-After")
.filter(|v| v.to_ascii_lowercase() != "empty"),
};
Self {
github,
hookdeck,
content_type: header_string(headers, "Content-Type"),
content_length: header_u64(headers, "Content-Length"),
}
}
}
#[derive(Debug)]
pub struct WebhookExtraction<T> {
pub payload: T,
pub info: WebhookInfo,
pub signature: SignatureStatus,
}
#[derive(Debug, Error, Diagnostic)]
pub enum WebhookError {
#[error("signature verification failed")]
#[diagnostic(
code("webhook.signature.failed"),
help("Provide a valid signature header or disable verification only in trusted development environments.")
)]
Signature(#[from] SignatureError),
#[error("failed to parse json payload")]
#[diagnostic(
code("webhook.payload.json_invalid"),
help("Verify the webhook payload is valid JSON and matches the expected event schema.")
)]
Json(#[from] serde_json::Error),
}
pub fn extract_json<T: DeserializeOwned>(
headers: &HeaderMap,
body: &[u8],
checks: &[SignatureCheck],
policy: SignaturePolicy,
) -> Result<WebhookExtraction<T>, WebhookError> {
let info = WebhookInfo::from_headers(headers);
let signature = verify_signatures(headers, body, checks, policy)?;
let payload = serde_json::from_slice(body)?;
Ok(WebhookExtraction {
payload,
info,
signature,
})
}
fn verify_hmac_hex(sig: &str, prefix: Option<&'static str>, secret: &[u8], body: &[u8]) -> bool {
let sig = sig.trim();
let sig = prefix.and_then(|p| sig.strip_prefix(p)).unwrap_or(sig);
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(body);
match hex::decode(sig) {
Ok(decoded) => mac.verify_slice(&decoded).is_ok(),
Err(_) => false,
}
}
fn verify_hmac_base64(sig: &str, secret: &[u8], body: &[u8]) -> bool {
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(body);
match base64::engine::general_purpose::STANDARD.decode(sig.trim()) {
Ok(decoded) => mac.verify_slice(&decoded).is_ok(),
Err(_) => false,
}
}
fn header_string(headers: &HeaderMap, name: &str) -> Option<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
fn header_u64(headers: &HeaderMap, name: &str) -> Option<u64> {
header_string(headers, name).and_then(|v| v.parse().ok())
}
fn header_u32(headers: &HeaderMap, name: &str) -> Option<u32> {
header_string(headers, name).and_then(|v| v.parse().ok())
}
fn header_bool(headers: &HeaderMap, name: &str) -> Option<bool> {
header_string(headers, name).and_then(|v| match v.to_ascii_lowercase().as_str() {
"true" => Some(true),
"false" => Some(false),
_ => None,
})
}
fn header_ip(headers: &HeaderMap, name: &str) -> Option<IpAddr> {
header_string(headers, name).and_then(|v| v.parse().ok())
}

View file

@ -75,10 +75,10 @@ FORGEJO_BASE_URL=
# GitHub Integration secrets (set per deployment) # GitHub Integration secrets (set per deployment)
# Shared secret used to validate GitHub webhooks (X-Hub-Signature-256) # Shared secret used to validate GitHub webhooks (X-Hub-Signature-256)
GITHUB_WEBHOOK_SECRET= GITHUB_WEBHOOK_SECRET=
# Hookdeck signing secret (when webhooks are proxied via Hookdeck)
HOOKDECK_SIGNING_SECRET=
# GitHub App ID (numeric) # GitHub App ID (numeric)
GITHUB_APP_ID= GITHUB_APP_ID=
# GitHub App private key (PEM) or a filesystem path to the PEM
GITHUB_APP_KEY=
GITHUB_APP_KEY_PATH= GITHUB_APP_KEY_PATH=
# Optional: override GitHub API base (GitHub Enterprise) # Optional: override GitHub API base (GitHub Enterprise)
GITHUB_API_BASE= GITHUB_API_BASE=

View file

@ -166,6 +166,7 @@ Forge integration configuration
GitHub integration configuration GitHub integration configuration
- Set GITHUB_WEBHOOK_SECRET in deploy/podman/.env to validate webhook signatures (X-Hub-Signature-256). If unset, webhooks are accepted without validation (dev mode). - Set GITHUB_WEBHOOK_SECRET in deploy/podman/.env to validate webhook signatures (X-Hub-Signature-256). If unset, webhooks are accepted without validation (dev mode).
- If you proxy webhooks through Hookdeck, set HOOKDECK_SIGNING_SECRET to validate Hookdeck's signature header (X-Hookdeck-Signature). Either GitHub or Hookdeck signatures can satisfy verification.
- To enable check runs and workflow fetches, configure a GitHub App and set GITHUB_APP_ID plus either GITHUB_APP_KEY (PEM contents) or GITHUB_APP_KEY_PATH (path inside the container). - To enable check runs and workflow fetches, configure a GitHub App and set GITHUB_APP_ID plus either GITHUB_APP_KEY (PEM contents) or GITHUB_APP_KEY_PATH (path inside the container).
- Optional overrides: GITHUB_API_BASE for GitHub Enterprise and GITHUB_CHECK_NAME to customize the check run title. - Optional overrides: GITHUB_API_BASE for GitHub Enterprise and GITHUB_CHECK_NAME to customize the check run title.
- The compose file passes these variables to the container. After editing .env, run: podman compose up -d github-integration - The compose file passes these variables to the container. After editing .env, run: podman compose up -d github-integration

View file

@ -320,13 +320,15 @@ services:
HTTP_ADDR: 0.0.0.0:8082 HTTP_ADDR: 0.0.0.0:8082
WEBHOOK_PATH: /webhooks/github WEBHOOK_PATH: /webhooks/github
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET} GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET}
HOOKDECK_SIGNING_SECRET: ${HOOKDECK_SIGNING_SECRET}
GITHUB_APP_ID: ${GITHUB_APP_ID} GITHUB_APP_ID: ${GITHUB_APP_ID}
GITHUB_APP_KEY_PATH: ${GITHUB_APP_KEY_PATH} GITHUB_APP_KEY_PATH: /app/github-app-key.pem
GITHUB_APP_KEY: ${GITHUB_APP_KEY}
GITHUB_API_BASE: ${GITHUB_API_BASE:-https://api.github.com} GITHUB_API_BASE: ${GITHUB_API_BASE:-https://api.github.com}
GITHUB_CHECK_NAME: ${GITHUB_CHECK_NAME:-Solstice CI} GITHUB_CHECK_NAME: ${GITHUB_CHECK_NAME:-Solstice CI}
# URL where logs-service is exposed (used for check-run links) # URL where logs-service is exposed (used for check-run links)
LOGS_BASE_URL: https://logs.${ENV}.${DOMAIN} LOGS_BASE_URL: https://logs.${ENV}.${DOMAIN}
volumes:
- ${GITHUB_APP_KEY_PATH}:/app/github-app-key.pem
depends_on: depends_on:
rabbitmq: rabbitmq:
condition: service_healthy condition: service_healthy