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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export GITHUB_TOKEN=...
postil review --repo owner/repository --pr 123 --publish
```

`--publish` is required for any forge write. Without it, the CLI fetches the pull request and reports locally. Published runs create inline findings and separate `postil/review` and `postil/gate` checks. Mark only `postil/gate` as required in branch protection.
`--publish` is required for any forge write. Without it, the CLI fetches the pull request and reports locally. Published runs create separate `postil/review` and `postil/gate` checks. Findings appear in one batched review by default; GitHub repositories can set `review.findingPresentation: checkAnnotations` to put them on the advisory check instead. Mark only `postil/gate` as required in branch protection.

For GitHub Actions, use [`postil-action`](https://github.com/postil-dev/postil-action). Hosted GitHub reviews are available at [postil.dev/install](https://postil.dev/install).

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ reviewer:
focus: [security, concurrency]
review:
onClean: skip
findingPresentation: reviewComments # reviewComments or checkAnnotations (GitHub only)
uncertaintyResolution: true # resolve uncertainty findings from referenced repository files
conciseFindings: true # compress over-long finding bodies before rendering
gate:
Expand Down
2 changes: 2 additions & 0 deletions docs/forges.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ postil review --repo owner/repository --pr 123 --publish

`GITHUB_API_URL` selects a GitHub Enterprise Server API. GitHub review delivery needs pull-request and check-run write access.

Findings appear in one batched review by default. Set `review.findingPresentation: checkAnnotations` to publish them only as annotations on `postil/review`. Both modes retain the advisory and gate check summaries and conclusions.

Automated callers can bind a run to an observed pull-request snapshot with `--sha <head>` and `--base-sha <target>`. Before each GitHub write, Postil verifies the head commit, target-branch commit, and merge base from the acquired review snapshot. A changed value suppresses delivery.

## GitLab
Expand Down
44 changes: 42 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,26 @@ pub enum OnClean {
Comment,
}

/// Where GitHub renders individual findings. Check runs always retain their
/// status and summary; this setting selects one detailed finding surface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FindingPresentation {
/// Publish findings as one batched pull-request review with inline comments.
ReviewComments,
/// Publish findings as annotations on the advisory check run.
CheckAnnotations,
}

impl FindingPresentation {
pub fn as_str(self) -> &'static str {
match self {
Self::ReviewComments => "reviewComments",
Self::CheckAnnotations => "checkAnnotations",
}
}
}

/// What the gate does when the review cannot complete (model outage, rate-limit
/// exhaustion). Default is fail closed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -878,7 +898,7 @@ pub enum OnError {
/// Operational error fails the gate. Nothing merges on a broken review.
Block,
/// Provider outage passes the gate (advisory only): an outage does not
/// freeze every merge in the org; the review check goes neutral. Unusable
/// freeze every merge in the org; the review check still fails. Unusable
/// model output still blocks because that class is attacker-influenceable.
Advisory,
}
Expand Down Expand Up @@ -924,6 +944,7 @@ pub struct Config {
pub tone: String,
pub focus: Vec<String>,
pub on_clean: OnClean,
pub finding_presentation: FindingPresentation,
pub uncertainty_resolution: bool,
pub concise_findings: bool,
pub gate_fail_on: GateLevel,
Expand Down Expand Up @@ -970,6 +991,7 @@ impl Default for Config {
tone: "concise, dry, lightly sardonic, never hostile; no praise or filler".to_string(),
focus: Vec::new(),
on_clean: OnClean::Skip,
finding_presentation: FindingPresentation::ReviewComments,
uncertainty_resolution: true,
concise_findings: true,
gate_fail_on: GateLevel::Severity(Severity::Error),
Expand Down Expand Up @@ -1019,6 +1041,7 @@ pub struct ReviewerSection {
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ReviewSection {
pub on_clean: Option<OnClean>,
pub finding_presentation: Option<FindingPresentation>,
pub uncertainty_resolution: Option<bool>,
pub concise_findings: Option<bool>,
}
Expand Down Expand Up @@ -1168,6 +1191,9 @@ impl Config {
if let Some(oc) = r.on_clean {
self.on_clean = oc;
}
if let Some(presentation) = r.finding_presentation {
self.finding_presentation = presentation;
}
if let Some(enabled) = r.uncertainty_resolution {
self.uncertainty_resolution = enabled;
}
Expand Down Expand Up @@ -1932,6 +1958,7 @@ reviewer:

review:
onClean: skip # skip = stay silent on clean PRs (default) | comment
# findingPresentation: reviewComments # reviewComments (default) | checkAnnotations (GitHub only)
# Reviews fetch referenced repository files to resolve uncertainty findings
# by default. Uncomment this explicit opt-out to disable that pass.
# uncertaintyResolution: false
Expand All @@ -1945,7 +1972,7 @@ gate:
- humanEscalation # genuine owner/product decisions only; concrete bugs remain risk
# onError: block # block (default, fail closed) | advisory: gate outcome when
# # the review itself errors (model outage). advisory keeps an
# # outage from freezing merges; the review check goes neutral, not green.
# # outage from freezing merges; the review check still fails.

model:
name: __DEFAULT_MODEL__
Expand Down Expand Up @@ -2271,6 +2298,7 @@ scorer = { enabled = true, default_model = "provider/scorer", fallback = "provid
let c = Config::default();
assert_eq!(c.min_confidence, 0.6);
assert_eq!(c.on_clean, OnClean::Skip);
assert_eq!(c.finding_presentation, FindingPresentation::ReviewComments);
assert!(c.uncertainty_resolution);
assert!(c.concise_findings);
assert!(matches!(
Expand All @@ -2288,6 +2316,18 @@ scorer = { enabled = true, default_model = "provider/scorer", fallback = "provid
assert!(config.uncertainty_resolution);
}

#[test]
fn file_selects_check_annotation_finding_presentation() {
let file: FileConfig =
yaml_serde::from_str("review:\n findingPresentation: checkAnnotations\n").unwrap();
let mut config = Config::default();
config.apply_file_inner(file, false, false).unwrap();
assert_eq!(
config.finding_presentation,
FindingPresentation::CheckAnnotations
);
}

#[test]
fn file_disables_concise_findings() {
let file: FileConfig = yaml_serde::from_str("review:\n conciseFindings: false\n").unwrap();
Expand Down
8 changes: 4 additions & 4 deletions src/forge/azure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use serde_json::json;
use std::io::Write;

use super::{
CheckState, Forge, PrMeta, ReviewPublicationReceipt, ThreadKind, check_summary, check_title,
untracked_review_publication_receipt,
CheckRunIds, CheckState, Forge, PrMeta, ReviewPublicationReceipt, ThreadKind, check_summary,
check_title, untracked_review_publication_receipt,
};
use crate::diff::{DiffSnapshot, DiffSpool, WorkspaceBudget};
use crate::envelope::{Envelope, Finding};
Expand Down Expand Up @@ -527,12 +527,12 @@ impl Forge for Azure {

async fn complete_checks(
&self,
_advisory_id: &str,
_gate_id: &str,
_check_ids: CheckRunIds<'_>,
advisory: CheckState,
gate: Option<CheckState>,
envelope: &Envelope,
snapshot: &PrMeta,
_annotate_findings: bool,
) -> Result<()> {
if !self.snapshot_is_current(snapshot).await? {
eprintln!("postil: azure status delivery skipped because the pull request changed");
Expand Down
8 changes: 4 additions & 4 deletions src/forge/bitbucket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ use sha2::Digest;
use std::io::Write;

use super::{
CheckState, Forge, PrMeta, ReviewPublicationReceipt, ThreadKind, check_summary, check_title,
untracked_review_publication_receipt,
CheckRunIds, CheckState, Forge, PrMeta, ReviewPublicationReceipt, ThreadKind, check_summary,
check_title, untracked_review_publication_receipt,
};
use crate::diff::{DiffSnapshot, DiffSpool, WorkspaceBudget};
use crate::envelope::{Envelope, Finding};
Expand Down Expand Up @@ -575,12 +575,12 @@ impl Forge for Bitbucket {

async fn complete_checks(
&self,
_advisory_id: &str,
_gate_id: &str,
_check_ids: CheckRunIds<'_>,
advisory: CheckState,
gate: Option<CheckState>,
envelope: &Envelope,
snapshot: &PrMeta,
_annotate_findings: bool,
) -> Result<()> {
if !self.snapshot_is_current(snapshot).await? {
eprintln!("postil: bitbucket status delivery skipped because the pull request changed");
Expand Down
Loading
Loading