-
Notifications
You must be signed in to change notification settings - Fork 1
NAS-144026 / 27.0.0-BETA.1 / Request a team review when a human review is needed #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,8 @@ | |||||||||||
| * `gh pr review`, so the review state on the PR cannot disagree with the check. | ||||||||||||
| * | ||||||||||||
| * any finding >= MEDIUM REQUEST_CHANGES, job fails | ||||||||||||
| * clean, but a human must look COMMENT naming why, job passes | ||||||||||||
| * clean, but a human must look COMMENT naming why, team review | ||||||||||||
| * requested once per PR, job passes | ||||||||||||
|
Comment on lines
+12
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW — This outcome table now says the human-review path requests a team review "once per PR", unqualified, but
Suggested change
|
||||||||||||
| * clean, approve-when-clean off COMMENT "would approve", job passes | ||||||||||||
| * clean, approve-when-clean on APPROVE, job passes | ||||||||||||
| * clean, but GitHub refuses the APPROVE COMMENT saying so, job passes | ||||||||||||
|
|
@@ -44,6 +45,9 @@ | |||||||||||
| const number = Number(env('PR_NUMBER')); | ||||||||||||
| const head = env('HEAD_SHA'); | ||||||||||||
| const approveWhenClean = env('APPROVE_WHEN_CLEAN') === 'true'; | ||||||||||||
| const humanReviewTeam = env('HUMAN_REVIEW_TEAM'); | ||||||||||||
| const hasGithubToken = env('HAS_GITHUB_TOKEN') === 'true'; | ||||||||||||
| const REQUESTED_MARKER = '<!-- claude-review-requested-team -->'; | ||||||||||||
| const humanReviewPaths = env('HUMAN_REVIEW_PATHS') | ||||||||||||
| .split('\n') | ||||||||||||
| .map((p) => p.trim()) | ||||||||||||
|
|
@@ -83,6 +87,10 @@ | |||||||||||
| return all; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| // May be filled before the verdict is posted; callers that must not see it exclude it by id. | ||||||||||||
| let reviewsCache; | ||||||||||||
| const listReviews = async () => (reviewsCache ??= await paginate(`/pulls/${number}/reviews`)); | ||||||||||||
|
|
||||||||||||
| /** The reviewer's terminal API error, when it died before producing output. */ | ||||||||||||
| const terminalApiError = () => { | ||||||||||||
| const file = env('EXECUTION_FILE'); | ||||||||||||
|
|
@@ -226,7 +234,7 @@ | |||||||||||
| */ | ||||||||||||
| const dismissOwnStateReviews = async (own) => { | ||||||||||||
| try { | ||||||||||||
| const reviews = await paginate(`/pulls/${number}/reviews`); | ||||||||||||
| const reviews = await listReviews(); | ||||||||||||
| const stale = reviews.filter( | ||||||||||||
| (r) => | ||||||||||||
| ['CHANGES_REQUESTED', 'APPROVED'].includes(r.state) && | ||||||||||||
|
|
@@ -247,6 +255,45 @@ | |||||||||||
| } | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Asks the caller's org team for a review; the team's auto-assignment picks | ||||||||||||
| * the person. Once per PR: skipped when any reviewer is already requested | ||||||||||||
| * (CODEOWNERS included — after auto-assignment the team is often replaced by | ||||||||||||
| * a user, so checking for the team alone would assign a second person), or | ||||||||||||
| * when an earlier verdict recorded a request. Best effort. Returns the lines | ||||||||||||
| * for the review body. | ||||||||||||
| */ | ||||||||||||
| const requestTeamReview = async () => { | ||||||||||||
| if (!humanReviewTeam) return []; | ||||||||||||
| const team = `${repo.split('/')[0]}/${humanReviewTeam}`; | ||||||||||||
| if (!hasGithubToken) { | ||||||||||||
| console.log(`Not requesting ${team}: the job token cannot request team reviewers. Pass a github-token.`); | ||||||||||||
| return []; | ||||||||||||
| } | ||||||||||||
| try { | ||||||||||||
| const requested = await api(`/pulls/${number}/requested_reviewers`); | ||||||||||||
| if (requested.users?.length || requested.teams?.length) { | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW — This guard also disables the feature outright in any consumer with a Nothing produces a wrong result (someone is already requested, which is arguably the point), so this is LOW rather than a bug. Worth a sentence in the README bullet, or narrowing the guard to |
||||||||||||
| console.log('A reviewer is already requested; not requesting the team.'); | ||||||||||||
| return []; | ||||||||||||
| } | ||||||||||||
| const reviews = await listReviews(); | ||||||||||||
| if (reviews.some((r) => (r.body ?? '').includes(MARKER) && r.body.includes(REQUESTED_MARKER))) { | ||||||||||||
| console.log(`${team} was requested on an earlier run; not requesting again.`); | ||||||||||||
| return []; | ||||||||||||
| } | ||||||||||||
| await api(`/pulls/${number}/requested_reviewers`, { | ||||||||||||
| method: 'POST', | ||||||||||||
| body: JSON.stringify({ team_reviewers: [humanReviewTeam] }), | ||||||||||||
| }); | ||||||||||||
| console.log(`Requested a review from ${team}.`); | ||||||||||||
| // Backticks, not an @-mention: a mention would notify the whole team. | ||||||||||||
| return ['', `Requested a review from \`${team}\`.`, REQUESTED_MARKER]; | ||||||||||||
| } catch (error) { | ||||||||||||
| console.log(`::warning::could not request a review from ${team}: ${escapeData(error.message)}`); | ||||||||||||
| return []; | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| const approveHint = (error) => { | ||||||||||||
| if (!/HTTP 422/.test(error.message)) return; | ||||||||||||
| console.log( | ||||||||||||
|
|
@@ -284,12 +331,14 @@ | |||||||||||
| // On the flag, not the reason count: `required: true` with no reasons is | ||||||||||||
| // schema-valid and must not approve. | ||||||||||||
| if (humanReview.required || reasons.length > 0) { | ||||||||||||
| const requestLines = await requestTeamReview(); | ||||||||||||
|
Check notice on line 334 in review/submit-verdict.mjs
|
||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW — The "once per PR" record is written by the review posted after the request, so a request that lands while the review that would record it does not leaves no trace of itself. Sequence: run A requests the team, then Narrow, and the feature is best effort by design, so this is not worth much — but it is the one case the marker exists to cover and does not. Requesting after the review is posted, with the marker in the body either way, would close it. |
||||||||||||
| const own = await submit('COMMENT', [ | ||||||||||||
| `**Needs a human review.** ${countLine}`, | ||||||||||||
| '', | ||||||||||||
| 'Nothing blocks, but this change is one a person should decide on:', | ||||||||||||
| '', | ||||||||||||
| ...(reasons.length ? reasons : ['The reviewer flagged this as needing a person but gave no reason.']).map((r) => `- ${r}`), | ||||||||||||
| ...requestLines, | ||||||||||||
| ]); | ||||||||||||
| await dismissOwnStateReviews(own); | ||||||||||||
| console.log(`Human review required: ${reasons.length} reason(s).`); | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LOW — This bullet sits under "Making the review count is branch protection, per repo:" (line 327), but it is neither: code review auto-assignment is an org team setting, configured once per org rather than per repo, and it does not make the review count towards anything branch protection checks. Every other bullet in the list is a repository branch-protection or Actions setting. A reader following the section heading to their repo's protection rules will not find it there.
It reads as its own paragraph before the list — something like "The team request needs one setting outside this repo:" ahead of it, with the branch-protection list starting at Require 1 approval.