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: 2 additions & 0 deletions .github/workflows/ai-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr-number }}
AI_MODE: ${{ inputs.mode }}
AI_REVIEW_REVISION: ${{ job.workflow_sha }}
AI_REVIEW_COMMENT_AUTHOR: github-actions[bot]
run: ./target/release/ai-review

ai-review-skipped:
Expand Down
73 changes: 73 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ takes precedence as `NEEDS_WORK`. Re-running a mode on the same head commit
updates its global and inline bot comments instead of publishing duplicates,
and removes inline results that are no longer confirmed by that mode.

Successful `team` reviews also keep a versioned, size-bounded cache in an
invisible HTML marker owned by the workflow bot. An identical diff reviewed by
the same workflow revision returns before any Mistral call. When only part of
the diff changes, unchanged findings reuse their previous three-lens verdict
only if their exact identity and review context still match. Invalid cache data,
workflow changes, incomplete model responses, and changed context all fall back
to fresh verification.

### Calling the reusable workflow

Default per-PR setup, the multi-agent team review plus the PR description.
Expand Down
2 changes: 2 additions & 0 deletions tools/ai-review/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ octocrab = "0.44"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
base64 = "0.22"
sha2 = "0.10"

[lints.rust]
unsafe_code = "forbid"
Expand Down
86 changes: 80 additions & 6 deletions tools/ai-review/src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,20 @@ pub async fn upsert_global_comment(
.await
.context("failed to fetch every PR comment page")?;

let existing_id = comments.iter().find_map(|c| {
c.body
.as_deref()
.filter(|b| has_bot_marker(b, marker))
.map(|_| c.id)
});
let trusted_author = std::env::var("AI_REVIEW_COMMENT_AUTHOR")
.ok()
.filter(|author| !author.trim().is_empty());
let existing_id = comments
.iter()
.find(|comment| {
is_trusted_global_comment(
&comment.user.login,
comment.body.as_deref(),
marker,
trusted_author.as_deref(),
)
})
.map(|comment| comment.id);

if let Some(comment_id) = existing_id {
octo.issues(owner, repo)
Expand All @@ -409,6 +417,49 @@ pub async fn upsert_global_comment(
Ok(())
}

/// Returns the existing global bot comment containing `marker`, if any.
pub async fn fetch_global_comment(
octo: &Octocrab,
owner: &str,
repo: &str,
pr_number: u64,
marker: &str,
author_login: &str,
) -> anyhow::Result<Option<String>> {
let first_page = octo
.issues(owner, repo)
.list_comments(pr_number)
.per_page(100)
.send()
.await
.context("failed to list PR comments")?;
let comments = octo
.all_pages(first_page)
.await
.context("failed to fetch every PR comment page")?;
Ok(comments
.into_iter()
.find(|comment| {
is_trusted_global_comment(
&comment.user.login,
comment.body.as_deref(),
marker,
Some(author_login),
)
})
.and_then(|comment| comment.body))
}

fn is_trusted_global_comment(
comment_author: &str,
body: Option<&str>,
marker: &str,
trusted_author: Option<&str>,
) -> bool {
trusted_author.is_none_or(|author| comment_author == author)
&& body.is_some_and(|value| has_bot_marker(value, marker))
}

#[derive(serde::Serialize)]
struct ReviewRequest<'a> {
commit_id: &'a str,
Expand Down Expand Up @@ -995,6 +1046,29 @@ mod tests {
assert_eq!(ctx.lens_context(&finding), ctx.patch_for(&finding));
}

#[test]
fn global_comment_markers_are_accepted_only_from_the_trusted_author() {
let body = Some("review\n<!-- ai-team-bot -->");
assert!(is_trusted_global_comment(
"github-actions[bot]",
body,
"<!-- ai-team-bot -->",
Some("github-actions[bot]")
));
assert!(!is_trusted_global_comment(
"pull-request-author",
body,
"<!-- ai-team-bot -->",
Some("github-actions[bot]")
));
assert!(!is_trusted_global_comment(
"github-actions[bot]",
Some("ordinary comment"),
"<!-- ai-team-bot -->",
Some("github-actions[bot]")
));
}

#[test]
fn prepares_one_stable_inline_comment_per_location() {
let comments = vec![
Expand Down
1 change: 1 addition & 0 deletions tools/ai-review/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod github;
mod mistral;
mod review;
mod team;
mod team_cache;
mod text;
mod types;

Expand Down
Loading