feat(review): a WARN opens the lock only when every counting MAJOR is answered
This commit is contained in:
parent
ce64851a0d
commit
ce7dbc27a9
8 changed files with 1044 additions and 64 deletions
|
|
@ -18,7 +18,9 @@
|
|||
// other than HEAD
|
||||
// suite `npm test` exits 0 at HEAD (run for real, never read from a log)
|
||||
// review lib/review/verdict-gate.mjs on the release's review.md: BLOCK
|
||||
// stops (1), undecided stops (2)
|
||||
// stops (1), undecided stops (2); an ALLOW/WARN with a counting
|
||||
// MAJOR not answered in the top-level approval log
|
||||
// (`decideOpenFindings`, `review-unanswered:`) is not ready (1)
|
||||
// coverage the review is of THIS history: its `scope_sha_end` is HEAD or an
|
||||
// ancestor of HEAD, and the commits after it touch only the release
|
||||
// bookkeeping (the CHANGELOG, the version in the version files,
|
||||
|
|
@ -39,11 +41,12 @@ import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|||
import { spawnSync } from 'node:child_process';
|
||||
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { decideVerdict } from '../lib/review/verdict-gate.mjs';
|
||||
import { decideVerdict, decideOpenFindings } from '../lib/review/verdict-gate.mjs';
|
||||
import { reviewedSha } from '../lib/review/reviewed-commit.mjs';
|
||||
import { commandEnv } from '../lib/util/command-env.mjs';
|
||||
import { DEFAULT_TIMEOUT_MS, stoppedBy } from '../lib/verification/criteria-runner.mjs';
|
||||
import { isMain } from '../lib/util/is-main.mjs';
|
||||
import { releaseSection, parseDoors } from '../lib/util/doors.mjs';
|
||||
|
||||
const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
|
|
@ -76,55 +79,12 @@ export function withoutVersion(text) {
|
|||
}
|
||||
return JSON.stringify(o);
|
||||
}
|
||||
const DOOR_LINE_RE = /^- (one-way|two-way): \S/;
|
||||
|
||||
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
|
||||
|
||||
/**
|
||||
* The CHANGELOG section for `version`, heading line excluded, or null. Matches
|
||||
* `## v1.2.3 …`, `## [1.2.3] …` and `## 1.2.3 …` exactly: 1.1.0 is not 1.10.0
|
||||
* and not 1.1.0-pre.
|
||||
*/
|
||||
export function releaseSection(text, version) {
|
||||
if (typeof text !== 'string' || !version) return null;
|
||||
const lines = text.split('\n');
|
||||
const headingVersion = (line) => {
|
||||
const m = line.match(/^##\s+\[?v?([^\]\s(]+)\]?/);
|
||||
return m ? m[1] : null;
|
||||
};
|
||||
let start = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/^##\s/.test(lines[i]) && headingVersion(lines[i]) === version) { start = i + 1; break; }
|
||||
}
|
||||
if (start === -1) return null;
|
||||
let end = lines.length;
|
||||
for (let i = start; i < lines.length; i++) {
|
||||
if (/^##\s/.test(lines[i])) { end = i; break; }
|
||||
}
|
||||
return lines.slice(start, end).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* The `### Doors` list of a release section. Top-level `- ` lines must read
|
||||
* `- one-way: …` or `- two-way: …`; indented lines continue the entry above.
|
||||
* @returns {{ found: boolean, entries: Array<{door: string, text: string}>, bad: string[] }}
|
||||
*/
|
||||
export function parseDoors(section) {
|
||||
const lines = String(section ?? '').split('\n');
|
||||
const start = lines.findIndex((l) => /^###\s+Doors\s*$/.test(l));
|
||||
if (start === -1) return { found: false, entries: [], bad: [] };
|
||||
const entries = [];
|
||||
const bad = [];
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (/^#{2,3}\s/.test(line)) break;
|
||||
if (!line.startsWith('- ')) continue;
|
||||
const m = line.match(DOOR_LINE_RE);
|
||||
if (m) entries.push({ door: m[1], text: line.slice(m[0].length - 1).trim() });
|
||||
else bad.push(line);
|
||||
}
|
||||
return { found: true, entries, bad };
|
||||
}
|
||||
// The CHANGELOG door helpers live in lib/util/doors.mjs (lib never imports
|
||||
// scripts); re-exported here under the same names for the callers of this module.
|
||||
export { DOOR_LINE_RE, releaseSection, parseDoors } from '../lib/util/doors.mjs';
|
||||
|
||||
// The +/- lines of a unified diff, read inside its hunks only: a content line
|
||||
// that starts with "++ " or "-- " looks like a file header but is not one.
|
||||
|
|
@ -201,7 +161,7 @@ function reviewPath(root, review) {
|
|||
return { path: null, error: `--review ${review} names two files: ${inRepo} (in --repo) and ${fromHere} (from here); pass an absolute path` };
|
||||
}
|
||||
|
||||
function checkReview(reviewPath) {
|
||||
function checkReview(reviewPath, repo) {
|
||||
if (!reviewPath) return { check: { id: 'review', status: 'error', detail: 'no --review given — the release has no review verdict to read (K7)' }, text: null };
|
||||
if (!existsSync(reviewPath)) return { check: { id: 'review', status: 'error', detail: `${reviewPath} does not exist — no review, no release` }, text: null };
|
||||
let text;
|
||||
|
|
@ -211,6 +171,14 @@ function checkReview(reviewPath) {
|
|||
const v = decideVerdict(text);
|
||||
if (v.exit === 2) return { check: { id: 'review', status: 'error', detail: `verdict gate could not decide: ${v.reason}` }, text: null };
|
||||
if (v.exit === 1) return { check: { id: 'review', status: 'fail', detail: `review verdict BLOCK — ${v.reason}` }, text };
|
||||
// Every counting MAJOR must be answered, read against the same top-level
|
||||
// approval log the verdict gate reads (lib/review/verdict-gate.mjs
|
||||
// `decideOpenFindings`): unanswered is not ready; any other exit 2 is undecided.
|
||||
const open = decideOpenFindings(text, { repo, reviewPath });
|
||||
if (open.exit === 2 && String(open.reason).startsWith('review-unanswered:')) {
|
||||
return { check: { id: 'review', status: 'fail', detail: `review verdict ${v.verdict} with open findings — ${open.reason}` }, text };
|
||||
}
|
||||
if (open.exit !== 0) return { check: { id: 'review', status: 'error', detail: `the open-findings check could not decide: ${open.reason}` }, text: null };
|
||||
return { check: { id: 'review', status: 'pass', detail: `review verdict ${v.verdict}` }, text };
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +274,7 @@ export function releaseCheck({ repo, review, version: explicitVersion, suiteTime
|
|||
const where = reviewPath(root, review);
|
||||
const { check: rev, text } = where.error
|
||||
? { check: { id: 'review', status: 'error', detail: where.error }, text: null }
|
||||
: checkReview(where.path);
|
||||
: checkReview(where.path, root);
|
||||
const coverage = checkCoverage(root, text);
|
||||
const door = checkDoor(root, version);
|
||||
const undecided = [tree, rev, coverage, door].filter((c) => c.status === 'error');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue