pub mod auth;
pub mod firewall;
pub mod posting;
pub mod pr;
pub mod webhook;
pub use auth::{AuthStrategy, RunMode, mint_app_jwt, resolve_token_for_mode};
pub use firewall::{GH_ALLOW_PUSH, assert_no_push_operation};
pub use posting::{PostedReview, post_pr_review};
pub use pr::{PrMetadata, PrRef, PrUser, fetch_pr_diff, fetch_pr_metadata};
pub use webhook::verify_webhook_signature;
#[derive(Debug, thiserror::Error)]
pub enum GithubError {
#[error("no GitHub token configured; set GITHUB_TOKEN or GitHub App credentials")]
MissingToken,
#[error("GitHub App auth error: {0}")]
Auth(String),
#[error("GitHub request failed: {0}")]
Transport(String),
#[error("GitHub API returned {status}: {body}")]
Api {
status: u16,
body: String,
},
#[error(
"push operation blocked by firewall (GH_ALLOW_PUSH=false, spec REV-403). \
Write operations are permanently disabled."
)]
PushFirewall,
}
pub struct GithubClient {
pub http: reqwest::Client,
pub user_agent: String,
}
impl GithubClient {
pub fn new() -> Self {
Self::with_timeout(std::time::Duration::from_secs(30))
}
pub fn with_timeout(timeout: std::time::Duration) -> Self {
let http = reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("reqwest::Client::build failed — TLS backend unavailable");
Self {
http,
user_agent: "trusty-review".to_string(),
}
}
}
impl Default for GithubClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn github_error_display_missing_token() {
let err = GithubError::MissingToken;
let s = err.to_string();
assert!(
s.contains("GITHUB_TOKEN"),
"MissingToken message should mention GITHUB_TOKEN: {s}"
);
}
#[test]
fn github_error_display_auth() {
let err = GithubError::Auth("bad PEM".to_string());
assert!(err.to_string().contains("bad PEM"));
}
#[test]
fn github_error_display_api() {
let err = GithubError::Api {
status: 404,
body: "not found".to_string(),
};
let s = err.to_string();
assert!(s.contains("404"));
assert!(s.contains("not found"));
}
#[test]
fn github_error_display_push_firewall() {
let err = GithubError::PushFirewall;
let s = err.to_string();
assert!(
s.contains("GH_ALLOW_PUSH=false"),
"PushFirewall message must reference the constant: {s}"
);
assert!(
s.contains("REV-403"),
"PushFirewall message must reference spec REV-403: {s}"
);
}
#[test]
fn github_client_default_constructs() {
let _client = GithubClient::default();
}
}