@codyswann/lisa 4.4.18 → 4.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +1 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +10 -5
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/required-checks.json +4 -2
- package/nestjs/create-only/.github/required-checks.json +4 -2
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +593 -24
- package/typescript/create-only/.github/required-checks.json +4 -2
- package/typescript/create-only/.github/workflows/review-evidence.yml +122 -0
- package/typescript/package-lisa/package.lisa.json +1 -1
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
15
|
* node scripts/check-skipped-required-checks.mjs [rootDir] [--remote] [--json]
|
|
16
|
+
* node scripts/check-skipped-required-checks.mjs --vacuity [--fail-on-vacuous]
|
|
16
17
|
* node scripts/check-skipped-required-checks.mjs --pr=1234 [--repo=OWNER/NAME]
|
|
17
18
|
*
|
|
19
|
+
* `--vacuity` is the WIRED form of the third bullet: it resolves the pull
|
|
20
|
+
* request itself (`--pr`, else the Actions event payload, else `GITHUB_REF`,
|
|
21
|
+
* else the current branch), waits for the declared checks to settle, and
|
|
22
|
+
* REFUSES rather than reporting all-clear when it inspected nothing. See
|
|
23
|
+
* "`--vacuity` — why a flag nobody passes is the same defect" below.
|
|
24
|
+
*
|
|
18
25
|
* ## The family this guard covers
|
|
19
26
|
*
|
|
20
27
|
* **Required-and-red is loud; required-and-vacuous is not; advisory-and-stale is
|
|
@@ -136,6 +143,45 @@
|
|
|
136
143
|
* distinguishes them.** So anything gating on such a check must read the
|
|
137
144
|
* description, and `--pr` is the machine-readable form of that one-line triage.
|
|
138
145
|
*
|
|
146
|
+
* ## `--vacuity` — why a flag nobody passes is the same defect
|
|
147
|
+
*
|
|
148
|
+
* MEASURED (CodySwannGT/lisa#2928): for the whole life of this arm, NOTHING
|
|
149
|
+
* INVOKED IT. `quality.yml` ran the offline arm with no `--pr`, the shipped
|
|
150
|
+
* `required-checks-drift.yml` ran `--remote`, and the package script named
|
|
151
|
+
* `check:vacuous-required-checks` was a bare invocation — so the command named
|
|
152
|
+
* for the vacuous check reported on SKIPS and said nothing about vacuity unless
|
|
153
|
+
* a caller happened to remember `-- --pr=1234`. A rule that runs only when
|
|
154
|
+
* somebody types a flag is `declared-but-uncallable`: exactly the family this
|
|
155
|
+
* file exists to describe, reproduced one level up inside its own shipping.
|
|
156
|
+
*
|
|
157
|
+
* `--vacuity` closes it, and three properties are what make it a gate rather
|
|
158
|
+
* than a second flag to forget:
|
|
159
|
+
*
|
|
160
|
+
* 1. **It resolves the pull request itself.** `--pr`, else the `pull_request`
|
|
161
|
+
* event payload at `GITHUB_EVENT_PATH`, else `refs/pull/N/merge` in
|
|
162
|
+
* `GITHUB_REF`, else `gh pr view` on the current branch.
|
|
163
|
+
* 2. **It waits for the declared checks to SETTLE.** A review bot posts
|
|
164
|
+
* `pending — "Review queued"` and `pending — "Review in progress"` before
|
|
165
|
+
* it settles, in about nine seconds, and identically on a pull request it
|
|
166
|
+
* reviews and one it rate-limits. Both intermediate strings are in the
|
|
167
|
+
* `no_work` vocabulary, so evaluating on the `pull_request` event without
|
|
168
|
+
* waiting manufactures a finding on EVERY pull request — and a guard that
|
|
169
|
+
* fires every time gets deleted rather than read.
|
|
170
|
+
* 3. **It REFUSES rather than reporting all-clear from an empty inspection.**
|
|
171
|
+
* An unresolvable pull request, a `gh` that could not be read, a roster
|
|
172
|
+
* with no checks in it, or a declaration naming no evidence-bearing check
|
|
173
|
+
* all print the same "nothing vacuous here" as a genuinely clean run. That
|
|
174
|
+
* collapse is this file's own thesis, so the four causes are separated and
|
|
175
|
+
* named — see `VACUITY_REFUSALS`. `gh pr checks` in particular resolves the
|
|
176
|
+
* rollup through `checkSuite.workflowRun` and therefore needs `actions:
|
|
177
|
+
* read`; without it `gh` exits non-zero with EMPTY STDOUT, which reads as a
|
|
178
|
+
* content problem and never says the word "permission".
|
|
179
|
+
*
|
|
180
|
+
* A refusal is not a finding: it fails, and under `"enforcement": "warn"` it is
|
|
181
|
+
* loud and exits 0, matching what an untranscribed snapshot already does.
|
|
182
|
+
*
|
|
183
|
+
* ## `--fail-on-vacuous` — the supported exit code
|
|
184
|
+
*
|
|
139
185
|
* This arm NEVER blocks — `NEVER_BLOCKING`, enforced regardless of the
|
|
140
186
|
* declaration's `enforcement` mode. Two independent reasons, both load-bearing:
|
|
141
187
|
*
|
|
@@ -146,6 +192,13 @@
|
|
|
146
192
|
* decision an owner has to make. Shipping the gate before the decision
|
|
147
193
|
* would pre-empt it. Detection is what is uncontroversial; act on it.
|
|
148
194
|
*
|
|
195
|
+
* Neither argument says the finding must be UNREPORTABLE. A consumer who has
|
|
196
|
+
* made the governance call and wants a red job asks for one with
|
|
197
|
+
* `--fail-on-vacuous`, which is opt-in and changes nothing for anybody who does
|
|
198
|
+
* not pass it. Before that flag existed the only way to get an exit code was a
|
|
199
|
+
* wrapper reading `--json` — every consumer writing the same twelve lines, and
|
|
200
|
+
* the ones who did not write them got a finding that lived only in a log.
|
|
201
|
+
*
|
|
149
202
|
* ## Proof is matched STRICTLY, no-work LOOSELY
|
|
150
203
|
*
|
|
151
204
|
* The two description lists are deliberately asymmetric, because their errors
|
|
@@ -363,6 +416,45 @@ export const NEVER_BLOCKING = Object.freeze([
|
|
|
363
416
|
VIOLATIONS.unproven,
|
|
364
417
|
]);
|
|
365
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Why the vacuity arm could not inspect anything, as stable tokens.
|
|
421
|
+
*
|
|
422
|
+
* A REFUSAL IS NOT A FINDING, and the distinction is the whole point.
|
|
423
|
+
* `NEVER_BLOCKING` covers the findings — a check that reported success having
|
|
424
|
+
* done no work — and it is deliberate that those never redden a build. These
|
|
425
|
+
* are the other thing entirely: the arm did not run. An empty inspection and a
|
|
426
|
+
* genuinely clean pull request print the same "nothing vacuous here", which is
|
|
427
|
+
* this file's own thesis applied to itself, so the causes are separated and
|
|
428
|
+
* each one names itself (#2928).
|
|
429
|
+
*
|
|
430
|
+
* They are separated from each other for the same reason. A red job caused by a
|
|
431
|
+
* missing `actions: read` means NOBODY LOOKED; a red job caused by a hollow
|
|
432
|
+
* review means the review was fake. Reported through one message they are
|
|
433
|
+
* indistinguishable, and the first gets misreported as the second.
|
|
434
|
+
*/
|
|
435
|
+
export const VACUITY_REFUSALS = Object.freeze({
|
|
436
|
+
unresolvedPr: "vacuity_pr_unresolved",
|
|
437
|
+
unreadableChecks: "vacuity_checks_unreadable",
|
|
438
|
+
emptyRoster: "vacuity_no_checks_reported",
|
|
439
|
+
noneDeclared: "vacuity_none_declared",
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Ceiling, in seconds, on waiting for the declared checks to settle.
|
|
444
|
+
*
|
|
445
|
+
* Only `--vacuity` waits. An ad-hoc `--pr=1234` is a human triaging one pull
|
|
446
|
+
* request and answers immediately, exactly as it always has; the wait exists
|
|
447
|
+
* for the unattended run that starts the instant the pull request opens, when
|
|
448
|
+
* the bot has not posted anything yet.
|
|
449
|
+
*/
|
|
450
|
+
export const SETTLE_TIMEOUT_SECONDS = 300;
|
|
451
|
+
|
|
452
|
+
/** Seconds between polls while waiting for the declared checks to settle. */
|
|
453
|
+
export const SETTLE_INTERVAL_SECONDS = 15;
|
|
454
|
+
|
|
455
|
+
/** Matches the `GITHUB_REF` a `pull_request` event run carries. */
|
|
456
|
+
const REF_PULL = /^refs\/pull\/(\d+)\/(?:merge|head)$/u;
|
|
457
|
+
|
|
366
458
|
/**
|
|
367
459
|
* Reads `--name=value` or `--name value` out of argv.
|
|
368
460
|
*
|
|
@@ -992,9 +1084,13 @@ export function fetchPullRequestChecks(pr, repo) {
|
|
|
992
1084
|
} catch (error) {
|
|
993
1085
|
raw = typeof error?.stdout === "string" ? error.stdout : "";
|
|
994
1086
|
if (raw.trim() === "") {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1087
|
+
// MEASURED (#2928): this is what a missing `actions: read` looks like.
|
|
1088
|
+
// `gh pr checks` resolves the rollup through `checkSuite.workflowRun`, so
|
|
1089
|
+
// without that scope it exits non-zero with EMPTY STDOUT — a failure
|
|
1090
|
+
// shaped exactly like unreadable content, which never says the word
|
|
1091
|
+
// "permission". The commit-status route below needs no `actions` scope
|
|
1092
|
+
// and carries the description this arm reads, so try it before refusing.
|
|
1093
|
+
return fetchChecksViaApi(pr, repo, error);
|
|
998
1094
|
}
|
|
999
1095
|
}
|
|
1000
1096
|
try {
|
|
@@ -1008,6 +1104,355 @@ export function fetchPullRequestChecks(pr, repo) {
|
|
|
1008
1104
|
}
|
|
1009
1105
|
}
|
|
1010
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* Resolves `OWNER/NAME` for the repository this run is about.
|
|
1109
|
+
*
|
|
1110
|
+
* @param {string} [repo] - An explicit `--repo` value, returned unchanged
|
|
1111
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
|
|
1112
|
+
* @returns {string|undefined} The slug, or undefined when nothing resolves it
|
|
1113
|
+
*/
|
|
1114
|
+
export function resolveRepoSlug(repo, env = process.env) {
|
|
1115
|
+
if (repo) return repo;
|
|
1116
|
+
if (env.GITHUB_REPOSITORY) return env.GITHUB_REPOSITORY;
|
|
1117
|
+
try {
|
|
1118
|
+
const raw = boundedExecFileSync(
|
|
1119
|
+
"gh",
|
|
1120
|
+
["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
|
|
1121
|
+
{ encoding: "utf8" }
|
|
1122
|
+
).trim();
|
|
1123
|
+
return raw === "" ? undefined : raw;
|
|
1124
|
+
} catch {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Reads one `gh api` call that yields a JSON array, or throws with context.
|
|
1131
|
+
*
|
|
1132
|
+
* @param {ReadonlyArray<string>} args - Arguments after `gh`
|
|
1133
|
+
* @returns {object[]} The parsed array
|
|
1134
|
+
*/
|
|
1135
|
+
function ghApiArray(args) {
|
|
1136
|
+
const parsed = JSON.parse(
|
|
1137
|
+
boundedExecFileSync("gh", args, { encoding: "utf8" })
|
|
1138
|
+
);
|
|
1139
|
+
if (!Array.isArray(parsed)) throw new TypeError("not an array");
|
|
1140
|
+
return parsed;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* The permission-light route to the same rows, for when `gh pr checks` cannot.
|
|
1145
|
+
*
|
|
1146
|
+
* Two calls, neither of which needs `actions: read`: the combined commit status
|
|
1147
|
+
* (`statuses: read`), which is where a legacy status like CodeRabbit's lives
|
|
1148
|
+
* and is the ONLY route that carries its description, and the check-run list
|
|
1149
|
+
* (`checks: read`). `gh pr checks` is still the primary because it is one call
|
|
1150
|
+
* and already normalises both families; this exists so a repository that has
|
|
1151
|
+
* not granted the rollup scope gets an ANSWER rather than a refusal it will
|
|
1152
|
+
* misread as a detection.
|
|
1153
|
+
*
|
|
1154
|
+
* A check run with no conclusion yet is reported `PENDING` rather than by its
|
|
1155
|
+
* `status` string, so the settle test sees one vocabulary from both routes.
|
|
1156
|
+
*
|
|
1157
|
+
* @param {string|number} pr - Pull request number
|
|
1158
|
+
* @param {string|undefined} repo - `OWNER/NAME`, or undefined to resolve it
|
|
1159
|
+
* @param {unknown} cause - The `gh pr checks` failure this is falling back from
|
|
1160
|
+
* @returns {Array<{name: string, state: string, bucket?: string, description?: string}>} The checks
|
|
1161
|
+
* @throws {Error} When the fallback cannot answer either
|
|
1162
|
+
*/
|
|
1163
|
+
export function fetchChecksViaApi(pr, repo, cause) {
|
|
1164
|
+
const slug = resolveRepoSlug(repo);
|
|
1165
|
+
const why = cause instanceof Error ? cause.message : String(cause ?? "");
|
|
1166
|
+
if (slug === undefined) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`check-skipped-required-checks: could not read checks for PR ${pr} — \`gh pr checks\` failed (${why}) and the commit-status fallback has no OWNER/NAME to query. Pass \`--repo=OWNER/NAME\`, or set GITHUB_REPOSITORY.`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
const sha = boundedExecFileSync(
|
|
1173
|
+
"gh",
|
|
1174
|
+
[
|
|
1175
|
+
"pr",
|
|
1176
|
+
"view",
|
|
1177
|
+
String(pr),
|
|
1178
|
+
"--repo",
|
|
1179
|
+
slug,
|
|
1180
|
+
"--json",
|
|
1181
|
+
"headRefOid",
|
|
1182
|
+
"--jq",
|
|
1183
|
+
".headRefOid",
|
|
1184
|
+
],
|
|
1185
|
+
{ encoding: "utf8" }
|
|
1186
|
+
).trim();
|
|
1187
|
+
const statuses = ghApiArray([
|
|
1188
|
+
"api",
|
|
1189
|
+
`repos/${slug}/commits/${sha}/status`,
|
|
1190
|
+
"--jq",
|
|
1191
|
+
"[.statuses[] | {name: .context, state: .state, description: .description}]",
|
|
1192
|
+
]);
|
|
1193
|
+
const runs = ghApiArray([
|
|
1194
|
+
"api",
|
|
1195
|
+
`repos/${slug}/commits/${sha}/check-runs?per_page=100`,
|
|
1196
|
+
"--jq",
|
|
1197
|
+
'[.check_runs[] | {name: .name, conclusion: .conclusion, description: (.output.title // "")}]',
|
|
1198
|
+
]);
|
|
1199
|
+
return [
|
|
1200
|
+
...statuses.map(row =>
|
|
1201
|
+
normalizeCheckRow(row.name, row.state, row.description)
|
|
1202
|
+
),
|
|
1203
|
+
...runs.map(row =>
|
|
1204
|
+
normalizeCheckRow(row.name, row.conclusion, row.description)
|
|
1205
|
+
),
|
|
1206
|
+
];
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`check-skipped-required-checks: could not read checks for PR ${pr} in ${slug}. \`gh pr checks\` failed (${why}) — which is what a missing \`actions: read\` looks like, because it resolves the rollup through \`checkSuite.workflowRun\` and exits non-zero with empty output — and the commit-status fallback also failed (${error instanceof Error ? error.message : String(error)}). NOBODY LOOKED at this pull request; that is not the same as nothing being wrong with it.`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Puts one row from either API route into the shape this file evaluates.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {string} name - Context or check-run name
|
|
1218
|
+
* @param {string|null|undefined} outcome - Status state, or a run's conclusion
|
|
1219
|
+
* @param {string|undefined} description - The description, verbatim
|
|
1220
|
+
* @returns {{name: string, state: string, bucket: string, description: string}} One row
|
|
1221
|
+
*/
|
|
1222
|
+
function normalizeCheckRow(name, outcome, description) {
|
|
1223
|
+
const state =
|
|
1224
|
+
outcome === null || outcome === undefined || outcome === ""
|
|
1225
|
+
? "PENDING"
|
|
1226
|
+
: String(outcome).toUpperCase();
|
|
1227
|
+
const bucket =
|
|
1228
|
+
state === "PENDING" ? "pending" : state === "SUCCESS" ? "pass" : "fail";
|
|
1229
|
+
return { name: String(name), state, bucket, description: description ?? "" };
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* The check names a declaration says carry evidence.
|
|
1234
|
+
*
|
|
1235
|
+
* @param {object} declaration - The per-repo declaration
|
|
1236
|
+
* @returns {string[]} Declared evidence-bearing check names
|
|
1237
|
+
*/
|
|
1238
|
+
export function declaredEvidenceChecks(declaration) {
|
|
1239
|
+
return Object.keys(declaration.evidence_bearing_checks ?? {});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* True when every declared evidence-bearing check has reached a terminal state.
|
|
1244
|
+
*
|
|
1245
|
+
* MEASURED (#2928): a review bot posts `pending — "Review queued"`, then
|
|
1246
|
+
* `pending — "Review in progress"`, and only then settles — in about nine
|
|
1247
|
+
* seconds, and IDENTICALLY on a pull request it goes on to review and one it
|
|
1248
|
+
* rate-limits. Both intermediate descriptions are in the shipped `no_work`
|
|
1249
|
+
* vocabulary, so an arm that evaluates the moment a pull request opens
|
|
1250
|
+
* manufactures a `vacuous_required_check` on EVERY pull request. A guard that
|
|
1251
|
+
* fires every time is indistinguishable from one that fires at random.
|
|
1252
|
+
*
|
|
1253
|
+
* A check that has posted NOTHING is unsettled for the same reason rather than
|
|
1254
|
+
* a different one: the bot has seconds of latency before its first status
|
|
1255
|
+
* exists, and "absent" is the shape that latency takes. Absent AFTER the wait
|
|
1256
|
+
* is a real finding — that is the measured #2493/#2491/#2488 case — so this
|
|
1257
|
+
* only decides when to stop waiting, never what the verdict is.
|
|
1258
|
+
*
|
|
1259
|
+
* @param {object} declaration - The per-repo declaration
|
|
1260
|
+
* @param {ReadonlyArray<{name: string, state: string, bucket?: string}>} checks - Rows
|
|
1261
|
+
* @returns {boolean} True when nothing declared is still in flight
|
|
1262
|
+
*/
|
|
1263
|
+
export function checksSettled(declaration, checks) {
|
|
1264
|
+
return declaredEvidenceChecks(declaration).every(name => {
|
|
1265
|
+
const found = checks.find(check => check.name === name);
|
|
1266
|
+
if (found === undefined) return false;
|
|
1267
|
+
if (String(found.bucket ?? "").toLowerCase() === "pending") return false;
|
|
1268
|
+
return String(found.state ?? "").toUpperCase() !== "PENDING";
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Blocks this thread for `ms` without turning the event loop.
|
|
1274
|
+
*
|
|
1275
|
+
* `Atomics.wait` rather than a busy loop or an `await`: every other child start
|
|
1276
|
+
* in this file is synchronous, and making `main` async to sleep would change
|
|
1277
|
+
* the exit-code path that the rest of it works to keep meaningful.
|
|
1278
|
+
*
|
|
1279
|
+
* @param {number} ms - Milliseconds to sleep; non-positive returns at once
|
|
1280
|
+
* @returns {void}
|
|
1281
|
+
*/
|
|
1282
|
+
function sleepSync(ms) {
|
|
1283
|
+
if (ms <= 0) return;
|
|
1284
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Reads a pull request's checks, re-reading until the declared ones settle.
|
|
1289
|
+
*
|
|
1290
|
+
* @param {object} declaration - The per-repo declaration
|
|
1291
|
+
* @param {string|number} pr - Pull request number
|
|
1292
|
+
* @param {string|undefined} repo - `OWNER/NAME`, or undefined
|
|
1293
|
+
* @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function}} [options] -
|
|
1294
|
+
* Injection seams; the suite drives the whole loop through them so nothing
|
|
1295
|
+
* here has to sleep in real time
|
|
1296
|
+
* @returns {{checks: object[], settled: boolean}} The rows, and whether they settled
|
|
1297
|
+
*/
|
|
1298
|
+
export function fetchSettledChecks(declaration, pr, repo, options = {}) {
|
|
1299
|
+
const timeoutMs = (options.timeoutSeconds ?? SETTLE_TIMEOUT_SECONDS) * 1000;
|
|
1300
|
+
const intervalMs =
|
|
1301
|
+
(options.intervalSeconds ?? SETTLE_INTERVAL_SECONDS) * 1000;
|
|
1302
|
+
const read = options.fetch ?? fetchPullRequestChecks;
|
|
1303
|
+
const clock = options.now ?? Date.now;
|
|
1304
|
+
const sleep = options.sleep ?? sleepSync;
|
|
1305
|
+
const deadline = clock() + timeoutMs;
|
|
1306
|
+
let checks = read(pr, repo);
|
|
1307
|
+
while (!checksSettled(declaration, checks) && clock() < deadline) {
|
|
1308
|
+
sleep(Math.min(intervalMs, deadline - clock()));
|
|
1309
|
+
checks = read(pr, repo);
|
|
1310
|
+
}
|
|
1311
|
+
return { checks, settled: checksSettled(declaration, checks) };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Finds the open pull request for the checked-out branch, when nothing named one.
|
|
1316
|
+
*
|
|
1317
|
+
* `gh pr list --head` rather than `gh pr view`: the latter infers the branch
|
|
1318
|
+
* only when no `--repo` is passed, and REJECTS the combination outright, so the
|
|
1319
|
+
* one call that has to tolerate both shapes cannot use it.
|
|
1320
|
+
*
|
|
1321
|
+
* Streams are captured rather than inherited. The failure here is expected and
|
|
1322
|
+
* ordinary — a branch with no pull request is most of the branches there are —
|
|
1323
|
+
* and letting `gh`'s usage text reach stderr would put a scary block above a
|
|
1324
|
+
* report that is about to explain the situation in a sentence.
|
|
1325
|
+
*
|
|
1326
|
+
* @param {string} [repo] - `OWNER/NAME`, or undefined for the current repo
|
|
1327
|
+
* @returns {string|undefined} The number as written, or undefined
|
|
1328
|
+
*/
|
|
1329
|
+
function currentBranchPullRequest(repo) {
|
|
1330
|
+
try {
|
|
1331
|
+
const branch = boundedExecFileSync(
|
|
1332
|
+
"git",
|
|
1333
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
1334
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
1335
|
+
).trim();
|
|
1336
|
+
if (branch === "" || branch === "HEAD") return undefined;
|
|
1337
|
+
const args = [
|
|
1338
|
+
"pr",
|
|
1339
|
+
"list",
|
|
1340
|
+
"--head",
|
|
1341
|
+
branch,
|
|
1342
|
+
"--state",
|
|
1343
|
+
"open",
|
|
1344
|
+
"--limit",
|
|
1345
|
+
"1",
|
|
1346
|
+
"--json",
|
|
1347
|
+
"number",
|
|
1348
|
+
"--jq",
|
|
1349
|
+
".[0].number // empty",
|
|
1350
|
+
];
|
|
1351
|
+
if (repo) args.push("--repo", repo);
|
|
1352
|
+
const raw = boundedExecFileSync("gh", args, {
|
|
1353
|
+
encoding: "utf8",
|
|
1354
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1355
|
+
}).trim();
|
|
1356
|
+
return /^\d+$/u.test(raw) ? raw : undefined;
|
|
1357
|
+
} catch {
|
|
1358
|
+
return undefined;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Parses a JSON file, returning null rather than throwing.
|
|
1364
|
+
*
|
|
1365
|
+
* @param {string} file - Absolute path
|
|
1366
|
+
* @returns {object|null} The parsed value, or null
|
|
1367
|
+
*/
|
|
1368
|
+
function readJsonOrNull(file) {
|
|
1369
|
+
try {
|
|
1370
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
1371
|
+
} catch {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Resolves the pull request this run is about, without being told.
|
|
1378
|
+
*
|
|
1379
|
+
* The order is most-explicit-first, and every step is a fact about the run
|
|
1380
|
+
* rather than a guess: an operator's `--pr`, the event payload Actions wrote
|
|
1381
|
+
* for THIS run, the ref that run checked out, and finally the branch a human
|
|
1382
|
+
* has checked out locally. Returning `undefined` is a real answer — the caller
|
|
1383
|
+
* refuses on it rather than examining a pull request it picked.
|
|
1384
|
+
*
|
|
1385
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
1386
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
|
|
1387
|
+
* @param {{probeBranch?: Function}} [options] - Injection seam for the last step
|
|
1388
|
+
* @returns {{pr: string|undefined, source: string|null}} The number and where it came from
|
|
1389
|
+
*/
|
|
1390
|
+
export function resolvePullRequestNumber(
|
|
1391
|
+
argv,
|
|
1392
|
+
env = process.env,
|
|
1393
|
+
options = {}
|
|
1394
|
+
) {
|
|
1395
|
+
const explicit = readFlagValue(argv, "--pr");
|
|
1396
|
+
if (explicit !== undefined) return { pr: explicit, source: "--pr" };
|
|
1397
|
+
|
|
1398
|
+
const eventPath = env.GITHUB_EVENT_PATH;
|
|
1399
|
+
if (eventPath !== undefined && eventPath !== "" && existsSync(eventPath)) {
|
|
1400
|
+
const payload = readJsonOrNull(eventPath);
|
|
1401
|
+
const number = payload?.pull_request?.number ?? payload?.number;
|
|
1402
|
+
if (Number.isInteger(number)) {
|
|
1403
|
+
return { pr: String(number), source: "GITHUB_EVENT_PATH" };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const ref = REF_PULL.exec(env.GITHUB_REF ?? "");
|
|
1408
|
+
if (ref !== null) return { pr: ref[1], source: "GITHUB_REF" };
|
|
1409
|
+
|
|
1410
|
+
const probe = options.probeBranch ?? currentBranchPullRequest;
|
|
1411
|
+
const found = probe(readFlagValue(argv, "--repo"));
|
|
1412
|
+
return found === undefined
|
|
1413
|
+
? { pr: undefined, source: null }
|
|
1414
|
+
: { pr: found, source: "gh pr view" };
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Names the reason this run inspected nothing, or null when it inspected.
|
|
1419
|
+
*
|
|
1420
|
+
* Ordered by how early the inspection died, so the message names the FIRST
|
|
1421
|
+
* thing that went wrong rather than a downstream symptom of it.
|
|
1422
|
+
*
|
|
1423
|
+
* @param {{declaration: object, pr?: string, checks?: ReadonlyArray<object>, error?: unknown}} input -
|
|
1424
|
+
* What the arm managed to obtain
|
|
1425
|
+
* @returns {{kind: string, reason: string}|null} The refusal, or null
|
|
1426
|
+
*/
|
|
1427
|
+
export function vacuityRefusal(input) {
|
|
1428
|
+
const { declaration, pr, checks, error } = input;
|
|
1429
|
+
if (pr === undefined) {
|
|
1430
|
+
return {
|
|
1431
|
+
kind: VACUITY_REFUSALS.unresolvedPr,
|
|
1432
|
+
reason: `No pull request could be resolved, so no check was read and this arm examined NOTHING. It looked at \`--pr\`, the \`pull_request\` payload at GITHUB_EVENT_PATH, \`refs/pull/N/merge\` in GITHUB_REF, and \`gh pr list --head\` for the checked-out branch. Reporting a clean bill of health here would be indistinguishable from a pull request whose review really did happen — which is the exact collapse this guard exists to refuse.`,
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
if (error !== undefined) {
|
|
1436
|
+
return {
|
|
1437
|
+
kind: VACUITY_REFUSALS.unreadableChecks,
|
|
1438
|
+
reason: `${error instanceof Error ? error.message : String(error)}\n NOBODY LOOKED at PR #${pr}. That is a different fact from "the review was hollow", and this job being red says only the first.`,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
if ((checks ?? []).length === 0) {
|
|
1442
|
+
return {
|
|
1443
|
+
kind: VACUITY_REFUSALS.emptyRoster,
|
|
1444
|
+
reason: `PR #${pr} reported ZERO checks of any kind. A pull request with no checks and a pull request whose checks are all honest produce the same silence from this arm, so it refuses rather than picking one. Confirm the token can read checks (\`actions: read\` for the \`gh pr checks\` rollup, or \`checks: read\` + \`statuses: read\` for the fallback) before believing this.`,
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (declaredEvidenceChecks(declaration).length === 0) {
|
|
1448
|
+
return {
|
|
1449
|
+
kind: VACUITY_REFUSALS.noneDeclared,
|
|
1450
|
+
reason: `\`evidence_bearing_checks\` in ${DECLARATION_PATH} is empty, so this repository has declared that NO check's green means anything reviewed the code — and the vacuity arm examined nothing on PR #${pr}. If that is true, say so by removing the arm from your workflow. If it is not, name the checks: \`"evidence_bearing_checks": { "CodeRabbit": {} }\`.`,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1011
1456
|
/**
|
|
1012
1457
|
* Fetches the live required contexts for every declared ruleset.
|
|
1013
1458
|
*
|
|
@@ -1069,6 +1514,99 @@ export function compareRulesetBaseline(snapshot, live) {
|
|
|
1069
1514
|
];
|
|
1070
1515
|
}
|
|
1071
1516
|
|
|
1517
|
+
/**
|
|
1518
|
+
* Reads a `--name=<seconds>` flag, falling back when absent or unreadable.
|
|
1519
|
+
*
|
|
1520
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
1521
|
+
* @param {string} name - The flag, including its leading dashes
|
|
1522
|
+
* @param {number} fallback - Value to use when the flag is absent
|
|
1523
|
+
* @returns {number} A non-negative number of seconds
|
|
1524
|
+
*/
|
|
1525
|
+
function readSecondsFlag(argv, name, fallback) {
|
|
1526
|
+
const raw = readFlagValue(argv, name);
|
|
1527
|
+
if (raw === undefined) return fallback;
|
|
1528
|
+
const value = Number(raw);
|
|
1529
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Runs the vacuity arm, refusing rather than reporting an empty inspection.
|
|
1534
|
+
*
|
|
1535
|
+
* Returns `undefined` when the arm was not asked for at all — which is the only
|
|
1536
|
+
* way this stays silent. Every other outcome, including "I could not look", is
|
|
1537
|
+
* a reported one.
|
|
1538
|
+
*
|
|
1539
|
+
* The settle wait is armed by `--vacuity` and NOT by a bare `--pr`, because the
|
|
1540
|
+
* two are different situations: `--pr=1234` is a human triaging one pull
|
|
1541
|
+
* request who wants today's instant answer, and `--vacuity` is an unattended
|
|
1542
|
+
* run that starts the moment a pull request opens, before the bot has posted
|
|
1543
|
+
* anything at all.
|
|
1544
|
+
*
|
|
1545
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
1546
|
+
* @param {object} declaration - The per-repo declaration
|
|
1547
|
+
* @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function}} [options] -
|
|
1548
|
+
* Injection seams for the suite
|
|
1549
|
+
* @returns {{pr: string|undefined, prSource: string|null, checked: number, violations: object[], settled: boolean, refusal: {kind: string, reason: string}|null}|undefined} The inspection
|
|
1550
|
+
*/
|
|
1551
|
+
export function inspectVacuity(argv, declaration, options = {}) {
|
|
1552
|
+
const wired = argv.includes("--vacuity");
|
|
1553
|
+
if (!wired && readFlagValue(argv, "--pr") === undefined) return undefined;
|
|
1554
|
+
|
|
1555
|
+
const { pr, source } = resolvePullRequestNumber(
|
|
1556
|
+
argv,
|
|
1557
|
+
options.env ?? process.env,
|
|
1558
|
+
options
|
|
1559
|
+
);
|
|
1560
|
+
const empty = {
|
|
1561
|
+
pr,
|
|
1562
|
+
prSource: source,
|
|
1563
|
+
checked: 0,
|
|
1564
|
+
violations: [],
|
|
1565
|
+
settled: false,
|
|
1566
|
+
};
|
|
1567
|
+
if (pr === undefined) {
|
|
1568
|
+
return { ...empty, refusal: vacuityRefusal({ declaration, pr }) };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const repo = readFlagValue(argv, "--repo");
|
|
1572
|
+
let read;
|
|
1573
|
+
try {
|
|
1574
|
+
read = fetchSettledChecks(declaration, pr, repo, {
|
|
1575
|
+
timeoutSeconds: readSecondsFlag(
|
|
1576
|
+
argv,
|
|
1577
|
+
"--settle-timeout",
|
|
1578
|
+
wired ? SETTLE_TIMEOUT_SECONDS : 0
|
|
1579
|
+
),
|
|
1580
|
+
intervalSeconds: readSecondsFlag(
|
|
1581
|
+
argv,
|
|
1582
|
+
"--settle-interval",
|
|
1583
|
+
SETTLE_INTERVAL_SECONDS
|
|
1584
|
+
),
|
|
1585
|
+
fetch: options.fetch,
|
|
1586
|
+
now: options.now,
|
|
1587
|
+
sleep: options.sleep,
|
|
1588
|
+
});
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
return { ...empty, refusal: vacuityRefusal({ declaration, pr, error }) };
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const refusal = vacuityRefusal({ declaration, pr, checks: read.checks });
|
|
1594
|
+
if (refusal !== null) {
|
|
1595
|
+
return { ...empty, settled: read.settled, refusal };
|
|
1596
|
+
}
|
|
1597
|
+
const evaluated = evaluateVacuousChecks(declaration, read.checks, {
|
|
1598
|
+
trustRequiredContexts: options.trustRequiredContexts,
|
|
1599
|
+
});
|
|
1600
|
+
return {
|
|
1601
|
+
pr,
|
|
1602
|
+
prSource: source,
|
|
1603
|
+
checked: evaluated.checked,
|
|
1604
|
+
violations: evaluated.violations,
|
|
1605
|
+
settled: read.settled,
|
|
1606
|
+
refusal: null,
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1072
1610
|
/**
|
|
1073
1611
|
* Runs the guard.
|
|
1074
1612
|
*
|
|
@@ -1079,13 +1617,13 @@ export function compareRulesetBaseline(snapshot, live) {
|
|
|
1079
1617
|
* about skips.
|
|
1080
1618
|
*
|
|
1081
1619
|
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
1082
|
-
* @
|
|
1620
|
+
* @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}
|
|
1621
|
+
* @returns {{violations: object[], checked: number, tokens: string[], enforcement: string, trust: {trusted: boolean, reason: string}, recipe: string, pr: string|undefined, evidenceChecked: number, vacuity: object|undefined}} The result
|
|
1083
1622
|
*/
|
|
1084
|
-
export function runGuard(argv) {
|
|
1623
|
+
export function runGuard(argv, options = {}) {
|
|
1085
1624
|
const positional = argv.filter(arg => !arg.startsWith("--"));
|
|
1086
1625
|
const rootDir = positional[0] ?? process.cwd();
|
|
1087
1626
|
const declaration = loadDeclaration(rootDir);
|
|
1088
|
-
const pr = readFlagValue(argv, "--pr");
|
|
1089
1627
|
const collected = collectSkipJobTokens(rootDir, declaration.workflows);
|
|
1090
1628
|
const remote = argv.includes("--remote");
|
|
1091
1629
|
const live = remote
|
|
@@ -1110,16 +1648,13 @@ export function runGuard(argv) {
|
|
|
1110
1648
|
|
|
1111
1649
|
// The vacuity arm is layered ON TOP of the offline run rather than replacing
|
|
1112
1650
|
// it: it is a third variant of one family, so it belongs in one report. Its
|
|
1113
|
-
//
|
|
1114
|
-
// the offline arm would have produced on its own.
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
fetchPullRequestChecks(pr, readFlagValue(argv, "--repo")),
|
|
1121
|
-
{ trustRequiredContexts: trust.trusted }
|
|
1122
|
-
);
|
|
1651
|
+
// FINDINGS are `NEVER_BLOCKING`, so adding them cannot change the exit code
|
|
1652
|
+
// the offline arm would have produced on its own. Its REFUSAL is not a
|
|
1653
|
+
// finding and does change it — see `VACUITY_REFUSALS`.
|
|
1654
|
+
const vacuity = inspectVacuity(argv, declaration, {
|
|
1655
|
+
...options,
|
|
1656
|
+
trustRequiredContexts: trust.trusted,
|
|
1657
|
+
});
|
|
1123
1658
|
if (vacuity !== undefined) violations.push(...vacuity.violations);
|
|
1124
1659
|
|
|
1125
1660
|
return {
|
|
@@ -1129,8 +1664,9 @@ export function runGuard(argv) {
|
|
|
1129
1664
|
enforcement: declaration.enforcement ?? "error",
|
|
1130
1665
|
trust,
|
|
1131
1666
|
recipe: transcriptionRecipe(declaration),
|
|
1132
|
-
pr,
|
|
1667
|
+
pr: vacuity?.pr,
|
|
1133
1668
|
evidenceChecked: vacuity?.checked ?? 0,
|
|
1669
|
+
vacuity,
|
|
1134
1670
|
};
|
|
1135
1671
|
}
|
|
1136
1672
|
|
|
@@ -1165,8 +1701,13 @@ function main(argv) {
|
|
|
1165
1701
|
process.stdout.write(
|
|
1166
1702
|
`${JSON.stringify(
|
|
1167
1703
|
{
|
|
1168
|
-
ok:
|
|
1704
|
+
ok:
|
|
1705
|
+
result.violations.length === 0 &&
|
|
1706
|
+
result.trust.trusted &&
|
|
1707
|
+
result.vacuity?.refusal == null,
|
|
1169
1708
|
answered: result.trust.trusted,
|
|
1709
|
+
inspected:
|
|
1710
|
+
result.vacuity === undefined || result.vacuity.refusal === null,
|
|
1170
1711
|
...result,
|
|
1171
1712
|
},
|
|
1172
1713
|
null,
|
|
@@ -1177,6 +1718,9 @@ function main(argv) {
|
|
|
1177
1718
|
}
|
|
1178
1719
|
|
|
1179
1720
|
const warnOnly = result.enforcement === "warn";
|
|
1721
|
+
// The supported alternative to a per-consumer `--json` wrapper. Opt-in, so
|
|
1722
|
+
// the shipped default is unchanged for everyone who does not pass it.
|
|
1723
|
+
const failOnVacuous = argv.includes("--fail-on-vacuous");
|
|
1180
1724
|
/**
|
|
1181
1725
|
* True when a violation still fails the build under the active mode.
|
|
1182
1726
|
*
|
|
@@ -1184,9 +1728,11 @@ function main(argv) {
|
|
|
1184
1728
|
* @returns {boolean} True when it blocks
|
|
1185
1729
|
*/
|
|
1186
1730
|
const blocks = violation =>
|
|
1187
|
-
|
|
1188
|
-
|
|
1731
|
+
NEVER_BLOCKING.includes(violation.kind)
|
|
1732
|
+
? failOnVacuous
|
|
1733
|
+
: !warnOnly || ALWAYS_BLOCKING.includes(violation.kind);
|
|
1189
1734
|
const blocking = result.violations.filter(blocks);
|
|
1735
|
+
const refusal = result.vacuity?.refusal ?? null;
|
|
1190
1736
|
const lines = ["## 🔒 Required checks that prove nothing", ""];
|
|
1191
1737
|
|
|
1192
1738
|
// The refusal comes FIRST and replaces the verdict. Printing "✅ none
|
|
@@ -1211,14 +1757,32 @@ function main(argv) {
|
|
|
1211
1757
|
);
|
|
1212
1758
|
}
|
|
1213
1759
|
|
|
1760
|
+
// Second refusal, same shape and the same reason as the first: an inspection
|
|
1761
|
+
// that never happened must not print the sentence a clean one prints.
|
|
1762
|
+
if (refusal !== null) {
|
|
1763
|
+
lines.push(
|
|
1764
|
+
`⛔ **NOT INSPECTED** (\`${refusal.kind}\`) — the vacuity arm did not examine a single check, and will not report that nothing was vacuous.`,
|
|
1765
|
+
"",
|
|
1766
|
+
refusal.reason,
|
|
1767
|
+
""
|
|
1768
|
+
);
|
|
1769
|
+
process.stderr.write(
|
|
1770
|
+
`::${warnOnly ? "warning" : "error"} title=${refusal.kind}::${refusal.reason.split("\n")[0]}\n`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1214
1774
|
if (result.violations.length === 0) {
|
|
1215
1775
|
if (result.trust.trusted) {
|
|
1216
1776
|
lines.push(
|
|
1217
1777
|
`✅ ${result.checked} \`skip_jobs\` token(s) examined; none silences a ruleset-required status check.`,
|
|
1218
|
-
...(result.pr === undefined
|
|
1778
|
+
...(result.pr === undefined || refusal !== null
|
|
1219
1779
|
? []
|
|
1220
1780
|
: [
|
|
1221
|
-
`✅ ${result.evidenceChecked} evidence-bearing check(s) examined on PR #${result.pr}; each proved it did work
|
|
1781
|
+
`✅ ${result.evidenceChecked} evidence-bearing check(s) examined on PR #${result.pr}; each proved it did work.${
|
|
1782
|
+
result.vacuity?.settled === false
|
|
1783
|
+
? " (One or more had not settled when the wait expired, so this is what was true at that moment.)"
|
|
1784
|
+
: ""
|
|
1785
|
+
}`,
|
|
1222
1786
|
])
|
|
1223
1787
|
);
|
|
1224
1788
|
} else {
|
|
@@ -1250,7 +1814,9 @@ function main(argv) {
|
|
|
1250
1814
|
) {
|
|
1251
1815
|
lines.push(
|
|
1252
1816
|
"",
|
|
1253
|
-
|
|
1817
|
+
failOnVacuous
|
|
1818
|
+
? `\`${VIOLATIONS.vacuous}\` and \`${VIOLATIONS.unproven}\` are REPORT-ONLY by default; this run passed \`--fail-on-vacuous\`, which is the supported way to ask for an exit code once the governance call has been made. What they mean is unchanged: a PR carrying either finding has not been shown to be reviewed, so do not record it as reviewed.`
|
|
1819
|
+
: `\`${VIOLATIONS.vacuous}\` and \`${VIOLATIONS.unproven}\` are REPORT-ONLY in every enforcement mode — they never fail a build. A required check can go hollow because a vendor hit an org-wide spending cap, and reddening every PR on a billing state would be a worse gate than the one being criticised. What they change is what you may CLAIM: a PR carrying either finding has not been shown to be reviewed, so do not record it as reviewed. Pass \`--fail-on-vacuous\` to make them block.`
|
|
1254
1820
|
);
|
|
1255
1821
|
}
|
|
1256
1822
|
}
|
|
@@ -1276,7 +1842,10 @@ function main(argv) {
|
|
|
1276
1842
|
// for a clean bill of health. `warn` — which only Lisa's untranscribed seeds
|
|
1277
1843
|
// ship — downgrades it, because reddening a whole fleet the day a seed
|
|
1278
1844
|
// arrives is how a gate gets deleted instead of transcribed.
|
|
1279
|
-
if (
|
|
1845
|
+
if (
|
|
1846
|
+
blocking.length > 0 ||
|
|
1847
|
+
((!result.trust.trusted || refusal !== null) && !warnOnly)
|
|
1848
|
+
) {
|
|
1280
1849
|
process.exitCode = 1;
|
|
1281
1850
|
}
|
|
1282
1851
|
}
|