fix(review): the verdict gate refuses, with --repo, a review of another commit than the one being merged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a9f5f6251a
commit
6c0718c8d8
6 changed files with 570 additions and 25 deletions
|
|
@ -4,7 +4,7 @@
|
|||
// One command decides on the verdict written in review.md, so whatever stops a
|
||||
// merge or a release reads a file and an exit code, never a model's summary:
|
||||
//
|
||||
// node lib/review/verdict-gate.mjs [--json] <review.md>
|
||||
// node lib/review/verdict-gate.mjs [--json] <review.md> [--repo <dir>]
|
||||
// 0 = ALLOW or WARN
|
||||
// 1 = BLOCK
|
||||
// 2 = could not decide: missing/unreadable file, not a review, no verdict,
|
||||
|
|
@ -13,7 +13,12 @@
|
|||
// shape (coordinator-contract `illegalBriefLowering`), or a
|
||||
// WARN because a reviewer or engine did not run (`missing-reviewer:` /
|
||||
// `unattributable-payload` in allow_blocked_by — operator decision
|
||||
// 2026-10-06: a review not performed in full stops a merge).
|
||||
// 2026-10-06: a review not performed in full stops a merge), a
|
||||
// `--repo` with no value, or, with `--repo <dir>`, an ALLOW/WARN
|
||||
// review of another commit than the one being merged (reason
|
||||
// `review-stale: …`: its `scope_sha_end` is not HEAD, or an ancestor
|
||||
// of HEAD whose later commits change only the review lessons and the
|
||||
// CLAUDE.md pointer; lib/review/reviewed-commit.mjs, R6).
|
||||
// Exit 2 is NEVER green.
|
||||
//
|
||||
// Fail-closed reading: a missing `verdict` is exit 2, never ALLOW. A verdict
|
||||
|
|
@ -32,6 +37,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|||
import { validateReviewContent, VERDICT_VALUES } from '../validators/review-validator.mjs';
|
||||
import { fencedBlocks, isJsonBlock } from './findings-schema.mjs';
|
||||
import { illegalBriefLowering } from './coordinator-contract.mjs';
|
||||
import { reviewedSha, checkReviewedCommit } from './reviewed-commit.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, UNDECIDED: 2 });
|
||||
|
|
@ -159,10 +165,12 @@ function blockerSectionEntries(body) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Decide on a review.md path.
|
||||
* Decide on a review.md path. With `repo`, an ALLOW/WARN review must also be
|
||||
* of the commit being merged there (R6); BLOCK and exit 2 are returned as they are.
|
||||
* @param {string} filePath
|
||||
* @param {{ repo?: string }} [opts]
|
||||
*/
|
||||
export function decideVerdictFile(filePath) {
|
||||
export function decideVerdictFile(filePath, { repo } = {}) {
|
||||
if (!filePath || !existsSync(filePath)) return undecided(`file not found: ${filePath}`);
|
||||
let text;
|
||||
try {
|
||||
|
|
@ -170,21 +178,44 @@ export function decideVerdictFile(filePath) {
|
|||
} catch (e) {
|
||||
return undecided(`cannot read ${filePath}: ${e.message}`);
|
||||
}
|
||||
return decideVerdict(text);
|
||||
const decided = decideVerdict(text);
|
||||
if (repo === undefined || decided.exit !== EXIT.PASS) return decided;
|
||||
const sha = reviewedSha(text);
|
||||
if (!sha) return undecided('review-stale: review.md has no scope_sha_end that is a commit sha', decided.verdict);
|
||||
const commit = checkReviewedCommit({ repo, sha });
|
||||
if (!commit.ok) return undecided(commit.reason, decided.verdict);
|
||||
return decided;
|
||||
}
|
||||
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const files = args.filter((a) => !a.startsWith('--'));
|
||||
const usage = 'Usage: verdict-gate.mjs [--json] <review.md> [--repo <dir>] (exactly one file)\n';
|
||||
// --repo consumes its value before the file argument is collected, so the
|
||||
// value is never read as a second file.
|
||||
const files = [];
|
||||
let repo;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === '--repo') {
|
||||
const v = args[++i];
|
||||
if (v === undefined || v.startsWith('--')) {
|
||||
process.stderr.write(`verdict-gate: --repo needs a value\n${usage}`);
|
||||
process.exit(EXIT.UNDECIDED);
|
||||
}
|
||||
repo = v;
|
||||
} else if (!a.startsWith('--')) {
|
||||
files.push(a);
|
||||
}
|
||||
}
|
||||
const filePath = files[0];
|
||||
if (files.length !== 1) {
|
||||
// Exactly one file: a glob would otherwise gate on its first match only.
|
||||
process.stderr.write('Usage: verdict-gate.mjs [--json] <review.md> (exactly one file)\n');
|
||||
process.stderr.write(usage);
|
||||
process.exit(EXIT.UNDECIDED);
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = decideVerdictFile(filePath);
|
||||
result = decideVerdictFile(filePath, { repo });
|
||||
} catch (e) {
|
||||
result = undecided(`gate crashed: ${e.message}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue