Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 89 additions & 4 deletions bots/rhodibot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

use anyhow::Result;
use axum::{
Json, Router,
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use clap::Parser;
use clap::{Parser, Subcommand};
use serde::Serialize;
use std::sync::Arc;
use tokio::net::TcpListener;
Expand Down Expand Up @@ -46,6 +46,31 @@ struct Cli {
/// Webhook secret for verification
#[arg(long, env = "GITHUB_WEBHOOK_SECRET")]
webhook_secret: Option<String>,

#[command(subcommand)]
command: Option<Command>,
}

#[derive(Subcommand, Debug)]
enum Command {
/// Run a one-shot RSR compliance check against a remote repository and exit.
///
/// Uses the GitHub REST API (honours the `GITHUB_TOKEN` env var for rate
/// limits / private repos; works unauthenticated on public repos). Exits
/// non-zero when required checks fail, so it can gate CI directly.
Check {
/// Repository owner (user or org), e.g. `hyperpolymath`
#[arg(long)]
owner: String,

/// Repository name, e.g. `ubicity`
#[arg(long)]
repo: String,

/// Output format
#[arg(long, default_value = "pretty", value_parser = ["pretty", "json"])]
format: String,
},
}

/// Application state shared across handlers
Expand All @@ -70,14 +95,25 @@ async fn main() -> Result<()> {
// Parse CLI arguments
let cli = Cli::parse();

info!("Starting Rhodibot v{}", env!("CARGO_PKG_VERSION"));

// Build configuration
let config = Config::new(
cli.app_id,
cli.private_key_path.as_deref(),
cli.webhook_secret.clone(),
)?;

// One-shot CLI mode: run a compliance check and exit with a CI-usable code.
if let Some(Command::Check {
owner,
repo,
format,
}) = &cli.command
{
return run_check(&config, owner, repo, format).await;
}

info!("Starting Rhodibot v{}", env!("CARGO_PKG_VERSION"));

let state = AppState {
config: Arc::new(config),
};
Expand All @@ -101,6 +137,55 @@ async fn main() -> Result<()> {
Ok(())
}

/// One-shot RSR compliance check for CI. Prints a report and exits non-zero
/// when required checks fail (so the caller's job fails too).
async fn run_check(config: &Config, owner: &str, repo: &str, format: &str) -> Result<()> {
rhodibot::sanitize::validate_owner_repo(owner, repo)?;

let report = rsr::check_compliance(config, owner, repo).await?;

if format == "json" {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
println!(
"RSR compliance — {}/{} (policy: {})",
report.owner, report.repo, report.policy
);
println!("{}", rsr::policy_summary(report.policy));
println!(
"Score: {}/{} ({:.0}%)",
report.score, report.max_score, report.percentage
);
println!();
for check in &report.checks {
println!(
" [{:?}] {} ({:?}, {}/{}) — {}",
check.status,
check.name,
check.severity,
check.points,
check.max_points,
check.message
);
}
println!();
println!("{}", report.summary);
println!(
"Required checks: {}",
if report.required_passed {
"PASSED"
} else {
"FAILED"
}
);
}

if !report.required_passed {
std::process::exit(1);
}
Ok(())
}

/// Health check endpoint
async fn health_check() -> impl IntoResponse {
Json(HealthResponse {
Expand Down
2 changes: 1 addition & 1 deletion bots/rhodibot/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ mod webhook_tests {
let payload = r#"{"action":"opened","repository":{"name":"test"}}"#;

// Compute expected signature
use hmac::{Hmac, Mac};
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;

Expand Down
Loading