use std::path::Path;
pub fn check() -> anyhow::Result<()> {
let gates: &[(&str, fn() -> Gate)] = &[
("Cargo.lock committed", gate_lockfile),
("No uncommitted changes", gate_clean_git),
("Release build passes", gate_release_build),
("DATABASE_URL set", gate_database_url),
("No debug! / dbg! macros in src/", gate_no_debug_macros),
("No TODO / FIXME in src/", gate_no_todo),
("CHANGELOG.md updated", gate_changelog),
];
let mut passed = 0usize;
let mut blocked = 0usize;
let mut warned = 0usize;
println!();
println!("rok deploy:check — pre-deploy gate");
println!("{}", "─".repeat(50));
for (label, gate_fn) in gates {
let g = gate_fn();
match g.status {
GateStatus::Pass => {
passed += 1;
println!(" ✓ {label}");
}
GateStatus::Warn => {
warned += 1;
println!(" ! {label} — {}", g.message.as_deref().unwrap_or(""));
}
GateStatus::Block => {
blocked += 1;
println!(" ✗ {label} — {}", g.message.as_deref().unwrap_or("BLOCKED"));
}
}
}
println!("{}", "─".repeat(50));
println!(" {passed} passed {warned} warnings {blocked} blocked");
println!();
if blocked > 0 {
anyhow::bail!("{blocked} gate(s) failed — fix them before deploying");
}
Ok(())
}
enum GateStatus { Pass, Warn, Block }
struct Gate {
status: GateStatus,
message: Option<String>,
}
impl Gate {
fn pass() -> Self { Self { status: GateStatus::Pass, message: None } }
fn warn(msg: impl Into<String>) -> Self { Self { status: GateStatus::Warn, message: Some(msg.into()) } }
fn block(msg: impl Into<String>) -> Self { Self { status: GateStatus::Block, message: Some(msg.into()) } }
}
fn gate_lockfile() -> Gate {
match std::process::Command::new("git")
.args(["status", "--porcelain", "Cargo.lock"])
.output()
{
Ok(o) => {
let s = String::from_utf8_lossy(&o.stdout);
if s.trim().is_empty() { Gate::pass() }
else { Gate::warn("Cargo.lock has uncommitted changes — commit it for reproducible builds") }
}
Err(_) => Gate::warn("git not found — cannot check Cargo.lock"),
}
}
fn gate_clean_git() -> Gate {
match std::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
{
Ok(o) => {
let s = String::from_utf8_lossy(&o.stdout);
if s.trim().is_empty() { Gate::pass() }
else { Gate::warn(format!("{} uncommitted change(s)", s.lines().count())) }
}
Err(_) => Gate::warn("git not found"),
}
}
fn gate_release_build() -> Gate {
println!(" … running cargo check --release (this may take a moment)");
match std::process::Command::new("cargo")
.args(["check", "--release", "--quiet"])
.output()
{
Ok(o) if o.status.success() => Gate::pass(),
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
Gate::block(format!("cargo check --release failed: {}", stderr.lines().next().unwrap_or("")))
}
Err(e) => Gate::block(format!("cargo not found: {e}")),
}
}
fn gate_database_url() -> Gate {
if std::env::var("DATABASE_URL").is_ok() { Gate::pass() }
else { Gate::warn("DATABASE_URL not set in environment") }
}
fn gate_no_debug_macros() -> Gate {
if !Path::new("src").exists() { return Gate::pass(); }
let output = std::process::Command::new("grep")
.args(["-r", "--include=*.rs", "-l", r"debug!\|dbg!", "src/"])
.output();
match output {
Ok(o) if o.stdout.is_empty() => Gate::pass(),
Ok(o) => {
let files = String::from_utf8_lossy(&o.stdout);
Gate::warn(format!("debug!/dbg! found in: {}", files.lines().next().unwrap_or("")))
}
Err(_) => Gate::pass(), }
}
fn gate_no_todo() -> Gate {
if !Path::new("src").exists() { return Gate::pass(); }
let output = std::process::Command::new("grep")
.args(["-r", "--include=*.rs", "-l", r"TODO\|FIXME", "src/"])
.output();
match output {
Ok(o) if o.stdout.is_empty() => Gate::pass(),
Ok(o) => {
let count = String::from_utf8_lossy(&o.stdout).lines().count();
Gate::warn(format!("{count} file(s) contain TODO/FIXME comments"))
}
Err(_) => Gate::pass(),
}
}
fn gate_changelog() -> Gate {
if Path::new("CHANGELOG.md").exists() {
Gate::pass()
} else {
Gate::warn("CHANGELOG.md not found — consider documenting changes")
}
}