Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,13 @@ isn't in the commit's changed paths). The finding's prose is preserved;
only the bad anchor is removed, so the viewer falls back to rendering it
as a commit-level comment.

The JSON document is versioned with a top-level `schema_version` field. The
current schema version is `1`; additive optional fields do not require a bump,
while renamed or removed fields do. For CI or pre-send gating, `review` also
accepts the opt-in `--fail-on=critical|high|medium|low` flag. It exits with
status `1` when a surviving finding meets or exceeds the selected severity;
without the flag, the exit status remains unchanged.

### Structured review highlights

Review JSON keeps `summary.text` and `summary.counts` and adds
Expand Down
90 changes: 90 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,40 @@ enum ValidationMode {
Findings,
}

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum FailOn {
/// Fail when at least one Critical finding survives review.
Critical,
/// Fail when at least one High or Critical finding survives review.
High,
/// Fail when at least one Medium, High, or Critical finding survives review.
Medium,
/// Fail when any finding survives review.
Low,
}

impl FailOn {
fn rank(self) -> u8 {
match self {
Self::Critical => 4,
Self::High => 3,
Self::Medium => 2,
Self::Low => 1,
}
}

fn matches(self, severity: &str) -> bool {
let rank = match severity {
"Critical" => 4,
"High" => 3,
"Medium" => 2,
"Low" => 1,
_ => 0,
};
rank >= self.rank()
}
}

impl ValidationMode {
fn label(self) -> &'static str {
match self {
Expand Down Expand Up @@ -248,6 +282,10 @@ struct ReviewArgs {
#[arg(long = "validation-mode", value_enum, default_value_t = ValidationMode::Filter)]
validation_mode: ValidationMode,

/// Exit non-zero when the final findings include this severity or higher.
#[arg(long, value_enum)]
fail_on: Option<FailOn>,

/// Git URI whose selected branch is checked for follow-up fixes.
#[arg(
long = "upstream-repo",
Expand Down Expand Up @@ -2492,6 +2530,10 @@ async fn run_apply_command(
async fn main() -> Result<()> {
let run_start = Instant::now();
let cli = Cli::parse();
let fail_on = match &cli.command {
Command::Review(args) => args.fail_on,
_ => None,
};

if let Command::Apply(args) = &cli.command {
return run_apply_command(&cli.global, args, run_start).await;
Expand Down Expand Up @@ -3144,12 +3186,35 @@ The review will use {} prompts and persona and may be inaccurate — did you mea
} else {
output::print_report_human(&out);
}
if !cli.global.dry_run
&& !cancelled
&& fail_on.is_some_and(|threshold| has_finding_at_or_above(&out, threshold))
{
std::process::exit(1);
}
if cancelled {
std::process::exit(130);
}
Ok(())
}

fn has_finding_at_or_above(out: &Value, threshold: FailOn) -> bool {
out.get("commits")
.and_then(Value::as_array)
.into_iter()
.flat_map(|commits| commits.iter())
.flat_map(|commit| {
commit
.get("validated_findings")
.or_else(|| commit.get("findings"))
.and_then(Value::as_array)
.into_iter()
.flat_map(|findings| findings.iter())
})
.filter_map(|finding| finding.get("severity").and_then(Value::as_str))
.any(|severity| threshold.matches(severity))
}

async fn prefetch_context_block(
effective_repo: &Path,
patch_diff: &str,
Expand Down Expand Up @@ -5281,4 +5346,29 @@ mod cli_tests {

assert!(parsed.is_err());
}

#[test]
fn review_accepts_fail_on_threshold() {
let parsed =
Cli::try_parse_from(["boro", "review", "--fail-on", "high", "HEAD~1..HEAD"]).unwrap();

let Command::Review(args) = parsed.command else {
panic!("expected review command");
};
assert!(matches!(args.fail_on, Some(FailOn::High)));
}

#[test]
fn fail_on_checks_final_findings() {
let out = json!({
"commits": [{
"findings": [{"severity": "Low"}],
"validated_findings": [{"severity": "High"}]
}]
});

assert!(!has_finding_at_or_above(&out, FailOn::Critical));
assert!(has_finding_at_or_above(&out, FailOn::High));
assert!(has_finding_at_or_above(&out, FailOn::Low));
}
}