@codyswann/lisa 4.87.10 → 4.87.12
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/all/copy-overwrite/scripts/review-evidence-event.mjs +202 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +10 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +11 -5
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- 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-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-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-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-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-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-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-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-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-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-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-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-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-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-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-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-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-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-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/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +196 -41
- package/typescript/create-only/.github/required-checks.json +2 -1
- package/typescript/create-only/.github/workflows/review-evidence.yml +43 -1
|
@@ -75,6 +75,7 @@ scripts/check-skipped-required-checks.mjs
|
|
|
75
75
|
scripts/check-state-classification.mjs
|
|
76
76
|
scripts/check-third-party-action-pins.mjs
|
|
77
77
|
scripts/check-third-party-review-evidence.mjs
|
|
78
|
+
scripts/review-evidence-event.mjs
|
|
78
79
|
scripts/check-threshold-ratchet.mjs
|
|
79
80
|
scripts/check-verification-coverage.mjs
|
|
80
81
|
scripts/check-workflow-load-failures.mjs
|
|
@@ -1033,9 +1033,48 @@ export function loadDeclaration(rootDir) {
|
|
|
1033
1033
|
assertEnforcementMode(declaration);
|
|
1034
1034
|
assertSkipJobDeclarations(declaration.skip_job_declarations);
|
|
1035
1035
|
assertEvidenceBearingChecks(declaration.evidence_bearing_checks ?? {});
|
|
1036
|
+
registrationContexts(declaration, {
|
|
1037
|
+
contexts: declaration.required_contexts,
|
|
1038
|
+
});
|
|
1036
1039
|
return declaration;
|
|
1037
1040
|
}
|
|
1038
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Validate reasoned registration deferrals against the set actually judged.
|
|
1044
|
+
* This only removes absence/registration observations. GitHub still requires
|
|
1045
|
+
* each deferred check to pass; evidence-bearing checks can never be deferred.
|
|
1046
|
+
* @param {object} declaration - Reviewed repository declaration.
|
|
1047
|
+
* @param {{contexts: string[]}} requiredSet - Selected base branch or union.
|
|
1048
|
+
* @returns {string[]} Contexts whose registration must still be observed.
|
|
1049
|
+
*/
|
|
1050
|
+
export function registrationContexts(declaration, requiredSet) {
|
|
1051
|
+
const deferred = declaration.registration_deferred;
|
|
1052
|
+
if (deferred === undefined) return requiredSet.contexts;
|
|
1053
|
+
if (
|
|
1054
|
+
deferred === null ||
|
|
1055
|
+
typeof deferred !== "object" ||
|
|
1056
|
+
Array.isArray(deferred)
|
|
1057
|
+
)
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
"check-skipped-required-checks: `registration_deferred` must be an object mapping required check names to non-empty reasons."
|
|
1060
|
+
);
|
|
1061
|
+
for (const [name, reason] of Object.entries(deferred)) {
|
|
1062
|
+
if (typeof reason !== "string" || reason.trim() === "")
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`check-skipped-required-checks: registration_deferred entry ${JSON.stringify(name)} needs a non-empty reason.`
|
|
1065
|
+
);
|
|
1066
|
+
if (!requiredSet.contexts.includes(name))
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`check-skipped-required-checks: registration_deferred entry ${JSON.stringify(name)} is not in the selected required set. Refusing to judge this declaration.`
|
|
1069
|
+
);
|
|
1070
|
+
if (Object.hasOwn(declaration.evidence_bearing_checks ?? {}, name))
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
`check-skipped-required-checks: registration_deferred entry ${JSON.stringify(name)} is evidence-bearing. Review evidence must still be waited for and judged.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return requiredSet.contexts.filter(name => !Object.hasOwn(deferred, name));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1039
1078
|
/**
|
|
1040
1079
|
* Refuses a declaration that omits one of the guard's load-bearing snapshots.
|
|
1041
1080
|
*
|
|
@@ -1781,7 +1820,7 @@ export function evaluateAbsentRequiredChecks(
|
|
|
1781
1820
|
declaration,
|
|
1782
1821
|
options.base ?? { base: null, source: null }
|
|
1783
1822
|
);
|
|
1784
|
-
const required = requiredSet
|
|
1823
|
+
const required = registrationContexts(declaration, requiredSet);
|
|
1785
1824
|
const judgedFor =
|
|
1786
1825
|
requiredSet.source === "branch"
|
|
1787
1826
|
? ` Judged against the contexts required on base \`${requiredSet.base}\`.`
|
|
@@ -1848,12 +1887,12 @@ function normaliseBaseRef(ref) {
|
|
|
1848
1887
|
* Resolves the branch the pull request under inspection merges INTO.
|
|
1849
1888
|
*
|
|
1850
1889
|
* Most-explicit-first, the same order {@link resolvePullRequestNumber} uses:
|
|
1851
|
-
* an operator's `--base`, then
|
|
1852
|
-
* `
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1890
|
+
* an operator's `--base`, then current metadata for the inspected PR, then
|
|
1891
|
+
* `GITHUB_BASE_REF` and the event payload's `pull_request.base.ref`. A rerun
|
|
1892
|
+
* replays its old event even after retargeting. The caller supplies metadata
|
|
1893
|
+
* from its existing head observation, so this pure selection adds no API call.
|
|
1894
|
+
* When current metadata is unavailable, the identity-checked event remains a
|
|
1895
|
+
* fallback. Returning null selects the reported union fallback.
|
|
1857
1896
|
*
|
|
1858
1897
|
* The run's own base describes the run's own pull request, and nothing else.
|
|
1859
1898
|
* When `options.pr` names the pull request being inspected, GITHUB_BASE_REF
|
|
@@ -1867,13 +1906,21 @@ function normaliseBaseRef(ref) {
|
|
|
1867
1906
|
*
|
|
1868
1907
|
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
1869
1908
|
* @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
|
|
1870
|
-
* @param {{pr?: string, repo?: string}} [options] -
|
|
1909
|
+
* @param {{pr?: string, repo?: string, currentPullRequest?: object}} [options] - Inspected identity and its current metadata, when available
|
|
1871
1910
|
* @returns {{base: string|null, source: string|null, ignoredEventBase?: string}} The base and where it came from
|
|
1872
1911
|
*/
|
|
1873
1912
|
export function resolveBaseRef(argv, env = process.env, options = {}) {
|
|
1874
1913
|
const explicit = normaliseBaseRef(readFlagValue(argv, "--base"));
|
|
1875
1914
|
if (explicit !== null) return { base: explicit, source: "--base" };
|
|
1876
1915
|
|
|
1916
|
+
if (
|
|
1917
|
+
isInspectedPullRequest(options.currentPullRequest, options.pr, options.repo)
|
|
1918
|
+
) {
|
|
1919
|
+
const current = normaliseBaseRef(options.currentPullRequest.baseRefName);
|
|
1920
|
+
if (current !== null)
|
|
1921
|
+
return { base: current, source: "current PR metadata" };
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1877
1924
|
const eventPath = env.GITHUB_EVENT_PATH;
|
|
1878
1925
|
const payload =
|
|
1879
1926
|
eventPath !== undefined && eventPath !== "" && existsSync(eventPath)
|
|
@@ -1897,6 +1944,34 @@ export function resolveBaseRef(argv, env = process.env, options = {}) {
|
|
|
1897
1944
|
: { base: null, source: null, ignoredEventBase: found.base };
|
|
1898
1945
|
}
|
|
1899
1946
|
|
|
1947
|
+
/**
|
|
1948
|
+
* Match both native metadata identities before trusting its current base.
|
|
1949
|
+
* @param {object|undefined} current - Native PR metadata
|
|
1950
|
+
* @param {string|number|undefined} pr - Inspected PR
|
|
1951
|
+
* @param {string|undefined} repo - Inspected repository
|
|
1952
|
+
* @returns {boolean} Whether this is the requested PR in the requested repo
|
|
1953
|
+
*/
|
|
1954
|
+
function isInspectedPullRequest(current, pr, repo) {
|
|
1955
|
+
if (
|
|
1956
|
+
!current ||
|
|
1957
|
+
pr === undefined ||
|
|
1958
|
+
repo === undefined ||
|
|
1959
|
+
String(current.number) !== String(pr) ||
|
|
1960
|
+
typeof current.url !== "string"
|
|
1961
|
+
)
|
|
1962
|
+
return false;
|
|
1963
|
+
try {
|
|
1964
|
+
const url = new URL(current.url);
|
|
1965
|
+
return (
|
|
1966
|
+
["https:", "http:"].includes(url.protocol) &&
|
|
1967
|
+
url.pathname.replace(/\/$/u, "").toLowerCase() ===
|
|
1968
|
+
`/${repo}/pull/${pr}`.toLowerCase()
|
|
1969
|
+
);
|
|
1970
|
+
} catch {
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1900
1975
|
/**
|
|
1901
1976
|
* True when the inspected repository is the one this run's event belongs to.
|
|
1902
1977
|
*
|
|
@@ -4051,6 +4126,45 @@ export function resolveHeadSha(pr, repo) {
|
|
|
4051
4126
|
}
|
|
4052
4127
|
}
|
|
4053
4128
|
|
|
4129
|
+
/**
|
|
4130
|
+
* Observe head and current base together using the existing PR metadata read.
|
|
4131
|
+
* A failed metadata read retains the old head probe, so an independently
|
|
4132
|
+
* readable head can still use the declared event/union base fallbacks.
|
|
4133
|
+
* @param {string|number} pr - Inspected PR
|
|
4134
|
+
* @param {string|undefined} repo - Inspected repository
|
|
4135
|
+
* @returns {{headSha: string|undefined, repo?: string, pullRequest?: object}} One observation
|
|
4136
|
+
*/
|
|
4137
|
+
function observePullRequest(pr, repo) {
|
|
4138
|
+
const slug = resolveRepoSlug(repo);
|
|
4139
|
+
if (slug === undefined) return { headSha: undefined };
|
|
4140
|
+
try {
|
|
4141
|
+
const current = JSON.parse(
|
|
4142
|
+
boundedExecFileSync(
|
|
4143
|
+
"gh",
|
|
4144
|
+
[
|
|
4145
|
+
"pr",
|
|
4146
|
+
"view",
|
|
4147
|
+
String(pr),
|
|
4148
|
+
"--repo",
|
|
4149
|
+
slug,
|
|
4150
|
+
"--json",
|
|
4151
|
+
"number,url,headRefOid,baseRefName",
|
|
4152
|
+
],
|
|
4153
|
+
{ encoding: "utf8" }
|
|
4154
|
+
)
|
|
4155
|
+
);
|
|
4156
|
+
if (
|
|
4157
|
+
isInspectedPullRequest(current, pr, slug) &&
|
|
4158
|
+
typeof current.headRefOid === "string" &&
|
|
4159
|
+
/^[a-f0-9]{40}$/iu.test(current.headRefOid)
|
|
4160
|
+
)
|
|
4161
|
+
return { headSha: current.headRefOid, repo: slug, pullRequest: current };
|
|
4162
|
+
} catch {
|
|
4163
|
+
// Current metadata is unavailable; preserve the existing head-only probe.
|
|
4164
|
+
}
|
|
4165
|
+
return { headSha: resolveHeadSha(pr, slug), repo: slug };
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4054
4168
|
/**
|
|
4055
4169
|
* How a finding cites the commit its evidence was read at.
|
|
4056
4170
|
*
|
|
@@ -4386,7 +4500,7 @@ function sleepSync(ms) {
|
|
|
4386
4500
|
* @param {object} declaration - The per-repo declaration
|
|
4387
4501
|
* @param {string|number} pr - Pull request number
|
|
4388
4502
|
* @param {string|undefined} repo - `OWNER/NAME`, or undefined
|
|
4389
|
-
* @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function, headSha?: Function, requiredSet?: object, substitutes?: Set<string>, substituteHead?: string}} [options] -
|
|
4503
|
+
* @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function, headSha?: Function, observe?: Function, initialObservation?: object, requiredSet?: object, requiredSetFor?: Function, substitutes?: Set<string>, substituteHead?: string}} [options] -
|
|
4390
4504
|
* Injection seams; the suite drives the whole loop through them so nothing
|
|
4391
4505
|
* here has to sleep in real time
|
|
4392
4506
|
* @returns {{checks: object[], settled: boolean, headSha: string|undefined, registration: object}} Rows, reviewer completion, head and required-registration observations
|
|
@@ -4411,16 +4525,20 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
|
|
|
4411
4525
|
);
|
|
4412
4526
|
});
|
|
4413
4527
|
const resolveHead = options.headSha ?? resolveHeadSha;
|
|
4528
|
+
const observe =
|
|
4529
|
+
options.observe ??
|
|
4530
|
+
((request, slug) => ({ headSha: resolveHead(request, slug) }));
|
|
4414
4531
|
const clock = options.now ?? Date.now;
|
|
4415
4532
|
const sleep = options.sleep ?? sleepSync;
|
|
4416
4533
|
const started = clock();
|
|
4417
4534
|
const deadline = started + timeoutMs;
|
|
4418
|
-
const
|
|
4535
|
+
const defaultRequiredSet =
|
|
4419
4536
|
options.requiredSet ??
|
|
4420
4537
|
selectRequiredContexts(declaration, { base: null, source: null });
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4538
|
+
const selectSet = options.requiredSetFor ?? (() => defaultRequiredSet);
|
|
4539
|
+
const missing = observed =>
|
|
4540
|
+
registrationContexts(declaration, observed.requiredSet).filter(
|
|
4541
|
+
name => !observed.checks.some(check => check.name === name)
|
|
4424
4542
|
);
|
|
4425
4543
|
// A substitute is useful only at the head it was actually read for. Pending
|
|
4426
4544
|
// rows still wait, and a missing required reviewer still needs a real row.
|
|
@@ -4435,29 +4553,41 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
|
|
|
4435
4553
|
);
|
|
4436
4554
|
});
|
|
4437
4555
|
|
|
4438
|
-
// Bracket
|
|
4439
|
-
//
|
|
4440
|
-
//
|
|
4441
|
-
|
|
4442
|
-
// were never evaluated. Matching observations on both sides bind the roster
|
|
4443
|
-
// to that head; a mismatch is discarded and re-read, never reported.
|
|
4556
|
+
// Bracket checks with coherent head/base observations. The early substitute
|
|
4557
|
+
// read can supply the first observation without another metadata request.
|
|
4558
|
+
// A push or retarget during the read is discarded within this same deadline.
|
|
4559
|
+
let initial = options.initialObservation;
|
|
4444
4560
|
const snapshot = () => {
|
|
4445
|
-
const before =
|
|
4446
|
-
|
|
4561
|
+
const before = initial?.headSha === undefined ? observe(pr, repo) : initial;
|
|
4562
|
+
initial = undefined;
|
|
4563
|
+
const beforeSet = selectSet(before);
|
|
4564
|
+
registrationContexts(declaration, beforeSet);
|
|
4565
|
+
if (before.headSha === undefined) {
|
|
4447
4566
|
return {
|
|
4448
4567
|
checks: [],
|
|
4449
4568
|
headSha: undefined,
|
|
4450
4569
|
stable: false,
|
|
4451
4570
|
after: undefined,
|
|
4571
|
+
requiredSet: beforeSet,
|
|
4572
|
+
baseBefore: beforeSet.base,
|
|
4573
|
+
baseAfter: undefined,
|
|
4452
4574
|
};
|
|
4453
4575
|
}
|
|
4454
|
-
const checks = read(pr, repo, before);
|
|
4455
|
-
const after =
|
|
4576
|
+
const checks = read(pr, repo, before.headSha);
|
|
4577
|
+
const after = observe(pr, repo);
|
|
4578
|
+
const afterSet = selectSet(after);
|
|
4579
|
+
registrationContexts(declaration, afterSet);
|
|
4456
4580
|
return {
|
|
4457
4581
|
checks,
|
|
4458
|
-
headSha: before,
|
|
4459
|
-
stable:
|
|
4460
|
-
|
|
4582
|
+
headSha: before.headSha,
|
|
4583
|
+
stable:
|
|
4584
|
+
after.headSha !== undefined &&
|
|
4585
|
+
before.headSha === after.headSha &&
|
|
4586
|
+
beforeSet.base === afterSet.base,
|
|
4587
|
+
after: after.headSha,
|
|
4588
|
+
requiredSet: afterSet,
|
|
4589
|
+
baseBefore: beforeSet.base,
|
|
4590
|
+
baseAfter: afterSet.base,
|
|
4461
4591
|
};
|
|
4462
4592
|
};
|
|
4463
4593
|
|
|
@@ -4465,20 +4595,29 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
|
|
|
4465
4595
|
while (
|
|
4466
4596
|
(!observed.stable ||
|
|
4467
4597
|
!evidenceReady(observed) ||
|
|
4468
|
-
missing(observed
|
|
4598
|
+
missing(observed).length > 0) &&
|
|
4469
4599
|
clock() < deadline
|
|
4470
4600
|
) {
|
|
4471
4601
|
sleep(Math.min(intervalMs, deadline - clock()));
|
|
4472
4602
|
observed = snapshot();
|
|
4473
4603
|
}
|
|
4474
4604
|
if (!observed.stable) {
|
|
4605
|
+
if (observed.headSha !== undefined && observed.headSha === observed.after)
|
|
4606
|
+
throw new Error(
|
|
4607
|
+
`check-skipped-required-checks: the selected pull request base changed while its checks were being read (${observed.baseBefore ?? "unresolved"} -> ${observed.baseAfter ?? "unresolved"}). Refusing to judge checks against another base; re-read the pull request after retargeting settles.`
|
|
4608
|
+
);
|
|
4475
4609
|
throw new Error(
|
|
4476
4610
|
`check-skipped-required-checks: the pull request head changed while its checks were being read (${observed.headSha ?? "unresolved"} -> ${observed.after ?? "unresolved"}). Refusing to attach one commit's review evidence to another; re-read the pull request after the push settles.`
|
|
4477
4611
|
);
|
|
4478
4612
|
}
|
|
4479
4613
|
const elapsedSeconds = Math.max(0, clock() - started) / 1000;
|
|
4480
|
-
const unregistered = missing(observed
|
|
4481
|
-
const total = requiredSet.
|
|
4614
|
+
const unregistered = missing(observed);
|
|
4615
|
+
const total = registrationContexts(declaration, observed.requiredSet).length;
|
|
4616
|
+
const deferred = Object.keys(declaration.registration_deferred ?? {});
|
|
4617
|
+
const deferralNote =
|
|
4618
|
+
deferred.length === 0
|
|
4619
|
+
? ""
|
|
4620
|
+
: ` (${deferred.length} deferred by declaration: ${deferred.join(", ")})`;
|
|
4482
4621
|
const present = total - unregistered.length;
|
|
4483
4622
|
const expired = unregistered.length > 0;
|
|
4484
4623
|
const settled = checksSettled(declaration, observed.checks);
|
|
@@ -4496,9 +4635,10 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
|
|
|
4496
4635
|
total,
|
|
4497
4636
|
missing: unregistered,
|
|
4498
4637
|
expired,
|
|
4638
|
+
...(deferred.length === 0 ? {} : { deferred }),
|
|
4499
4639
|
line: expired
|
|
4500
|
-
? `Required registration wait EXPIRED: the gate stopped waiting after ${elapsedSeconds}s; ${present} of ${total} required contexts observed; missing: ${unregistered.join(", ")}. RE-RUN this job before treating this as a finding about the change.`
|
|
4501
|
-
: `Required registration settled after ${elapsedSeconds}s; ${present} of ${total} required contexts observed.`,
|
|
4640
|
+
? `Required registration wait EXPIRED: the gate stopped waiting after ${elapsedSeconds}s; ${present} of ${total} required contexts observed${deferralNote}; missing: ${unregistered.join(", ")}. RE-RUN this job before treating this as a finding about the change.`
|
|
4641
|
+
: `Required registration settled after ${elapsedSeconds}s; ${present} of ${total} required contexts observed${deferralNote}.`,
|
|
4502
4642
|
};
|
|
4503
4643
|
return {
|
|
4504
4644
|
checks: observed.checks,
|
|
@@ -4506,6 +4646,7 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
|
|
|
4506
4646
|
headSha: observed.headSha,
|
|
4507
4647
|
registration,
|
|
4508
4648
|
evidence,
|
|
4649
|
+
requiredSet: observed.requiredSet,
|
|
4509
4650
|
};
|
|
4510
4651
|
}
|
|
4511
4652
|
|
|
@@ -4701,7 +4842,7 @@ function readSecondsFlag(argv, name, fallback) {
|
|
|
4701
4842
|
*
|
|
4702
4843
|
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
4703
4844
|
* @param {object} declaration - The per-repo declaration
|
|
4704
|
-
* @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function, reviewActivity?: Function}} [options] -
|
|
4845
|
+
* @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function, observe?: Function, reviewActivity?: Function}} [options] -
|
|
4705
4846
|
* Injection seams for the suite
|
|
4706
4847
|
* @returns {{pr: string|undefined, prSource: string|null, headSha: string|undefined, checked: number, violations: object[], settled: boolean, refusal: {kind: string, reason: string}|null}|undefined} The inspection
|
|
4707
4848
|
*/
|
|
@@ -4755,19 +4896,30 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4755
4896
|
const declaredReviewers = Object.keys(
|
|
4756
4897
|
declaration.evidence_bearing_checks ?? {}
|
|
4757
4898
|
);
|
|
4758
|
-
const
|
|
4759
|
-
|
|
4899
|
+
const injectedHead = options.headSha;
|
|
4900
|
+
const observe =
|
|
4901
|
+
options.observe ??
|
|
4902
|
+
(injectedHead
|
|
4903
|
+
? (request, slug) => ({ headSha: injectedHead(request, slug) })
|
|
4904
|
+
: observePullRequest);
|
|
4905
|
+
const earlyObservation = requireReviewEvidence
|
|
4906
|
+
? observe(pr, repo)
|
|
4760
4907
|
: undefined;
|
|
4908
|
+
const earlyHead = earlyObservation?.headSha;
|
|
4761
4909
|
const earlySubstitutes = readSubstitutes(earlyHead);
|
|
4762
4910
|
const allStoodIn =
|
|
4763
4911
|
declaredReviewers.length > 0 &&
|
|
4764
4912
|
declaredReviewers.every(name => earlySubstitutes.has(name));
|
|
4765
|
-
//
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4913
|
+
// Registration and absence share the selection returned with stable checks.
|
|
4914
|
+
const requiredSetFor = observed =>
|
|
4915
|
+
selectRequiredContexts(
|
|
4916
|
+
declaration,
|
|
4917
|
+
resolveBaseRef(argv, options.env ?? process.env, {
|
|
4918
|
+
pr,
|
|
4919
|
+
repo: repo ?? observed.repo,
|
|
4920
|
+
currentPullRequest: observed.pullRequest,
|
|
4921
|
+
})
|
|
4922
|
+
);
|
|
4771
4923
|
let read;
|
|
4772
4924
|
try {
|
|
4773
4925
|
read = fetchSettledChecks(declaration, pr, repo, {
|
|
@@ -4785,13 +4937,16 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4785
4937
|
now: options.now,
|
|
4786
4938
|
sleep: options.sleep,
|
|
4787
4939
|
headSha: options.headSha,
|
|
4788
|
-
|
|
4940
|
+
observe,
|
|
4941
|
+
initialObservation: earlyObservation,
|
|
4942
|
+
requiredSetFor,
|
|
4789
4943
|
substitutes: allStoodIn ? earlySubstitutes : undefined,
|
|
4790
4944
|
substituteHead: earlyHead,
|
|
4791
4945
|
});
|
|
4792
4946
|
} catch (error) {
|
|
4793
4947
|
return refused(vacuityRefusal({ declaration, pr, error }));
|
|
4794
4948
|
}
|
|
4949
|
+
const requiredSet = read.requiredSet;
|
|
4795
4950
|
|
|
4796
4951
|
const refusal = vacuityRefusal({ declaration, pr, checks: read.checks });
|
|
4797
4952
|
if (refusal !== null) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_readme": [
|
|
3
|
+
"Optional `registration_deferred` (#4622) maps exact required context names to non-empty reasons when those jobs register after the bounded five-minute wait introduced by #4527. The registration/absence probe excludes only these names; GitHub still requires them to pass before merge. Entries outside the selected base branch's required set, malformed reasons, and evidence-bearing checks are refused. Omit the key for unchanged behavior. A slow reviewer is never deferred: the review-evidence workflow re-evaluates on its terminal check_run or status event, using default-branch code and a fresh current-head read. This workflow is create-only; existing hosts must adopt the event job explicitly to receive the wake-up behavior.",
|
|
3
4
|
"Two REVIEWED SNAPSHOTS that scripts/check-skipped-required-checks.mjs makes police each other.",
|
|
4
5
|
"Neither is derivable from this repository: `required_contexts` lives in a GitHub ruleset that humans edit in an admin console, and the skip_jobs token -> job name map lives in Lisa's quality.yml, which is not vendored here.",
|
|
5
6
|
"Why it matters: GitHub counts a SKIPPED required status check as SATISFIED. A job named in `skip_jobs` still reports green against its required context having run zero steps, so the merge gate can never be red and therefore can never block anything.",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"IT REFUSES RATHER THAN REPORTING AN EMPTY INSPECTION. An unresolvable pull request, a `gh` it could not read, a roster with no checks in it, or an empty `evidence_bearing_checks` all print the same \"nothing vacuous here\" that a genuinely clean run prints — so the guard fails instead and names which of the four it hit (`vacuity_pr_unresolved`, `vacuity_checks_unreadable`, `vacuity_no_checks_reported`, `vacuity_none_declared`). A refusal is NOT a finding: NOBODY LOOKED and THE REVIEW WAS FAKE are opposite facts, and a single red job would otherwise report them identically.",
|
|
19
20
|
"`evidence_bearing_checks` names the checks whose GREEN is supposed to mean something reviewed the code. Use `{}` to accept the shipped description vocabulary, or add `proof` / `no_work` arrays to extend it — extensions ADD to the defaults, they do not replace them. Undeclared checks are never examined, because most CI jobs ship an empty description and flagging them all would bury the one finding that matters.",
|
|
20
21
|
"This arm REPORTS AND NEVER BLOCKS, in every enforcement mode. A review bot can go hollow because an org-wide SPENDING CAP was hit, and a gate that reddens every PR on a billing state is worse than the one it criticises. What it changes is what you may CLAIM: a PR carrying a `vacuous_required_check` finding has not been shown to be reviewed, so do not record it as reviewed. Once you have made the governance call, `--fail-on-vacuous` is the supported way to ask for an exit code — add it to the command in `review-evidence.yml`, instead of writing a wrapper that reads `--json`.",
|
|
21
|
-
"PER-BRANCH SETS (CodySwannGT/lisa#4484). `required_contexts` is the UNION across every protected branch, which is the wrong set for judging which required contexts did NOT REPORT on one pull request: a context only `dev` requires was reported absent on every staging-base pull request. Optionally add `required_contexts_by_branch: { \"<branch>\": [ ...contexts ] }`, each set transcribed byte for byte from `gh api repos/OWNER/NAME/rules/branches/<branch> --jq '.[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context'` (read access only). The absent arm
|
|
22
|
+
"PER-BRANCH SETS (CodySwannGT/lisa#4484). `required_contexts` is the UNION across every protected branch, which is the wrong set for judging which required contexts did NOT REPORT on one pull request: a context only `dev` requires was reported absent on every staging-base pull request. Optionally add `required_contexts_by_branch: { \"<branch>\": [ ...contexts ] }`, each set transcribed byte for byte from `gh api repos/OWNER/NAME/rules/branches/<branch> --jq '.[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context'` (read access only). The absent arm judges the PR against its current base (`--base`, else current metadata from the existing PR head read). If current metadata is unreadable it uses only a matching PR/repository event (`GITHUB_BASE_REF`, else `pull_request.base.ref`). A rerun replays the old event, so matching identity alone does not establish the current base. Head/base observations bracket each check read; a retarget retries within the same settlement deadline, and registration and absence share the resulting selection. Without a set for that base it falls back to the union — same findings as before — and says so as `required_contexts_branch_fallback`; every run prints which set it used. Every per-branch context must also appear in `required_contexts`, and the one `baseline_fetched_at` stamp covers both."
|
|
22
23
|
],
|
|
23
24
|
"enforcement": "warn",
|
|
24
25
|
"ruleset": {
|
|
@@ -122,6 +122,9 @@ name: 🕵️ Review Evidence
|
|
|
122
122
|
on:
|
|
123
123
|
pull_request:
|
|
124
124
|
types: [opened, synchronize, reopened, ready_for_review]
|
|
125
|
+
check_run:
|
|
126
|
+
types: [completed]
|
|
127
|
+
status:
|
|
125
128
|
workflow_dispatch:
|
|
126
129
|
inputs:
|
|
127
130
|
pr:
|
|
@@ -147,11 +150,50 @@ permissions:
|
|
|
147
150
|
pull-requests: read
|
|
148
151
|
|
|
149
152
|
concurrency:
|
|
150
|
-
group: review-evidence-${{ github.event.pull_request.number || inputs.pr || github.ref }}
|
|
153
|
+
group: review-evidence-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.event.check_run.head_sha || github.event.sha || github.ref }}-${{ github.event.check_run.name || github.event.context }}-${{ github.event.check_run.app.id || github.event.creator.id }}
|
|
151
154
|
cancel-in-progress: true
|
|
152
155
|
|
|
153
156
|
jobs:
|
|
157
|
+
# Reviewer completion can outlast the bounded pull_request run. Dispatch the
|
|
158
|
+
# normal gate; the event is a wake-up and never proof that a review occurred.
|
|
159
|
+
# check_run requires this file on the default branch and GitHub suppresses
|
|
160
|
+
# Actions-created suites/heads; status supports legacy reviewer statuses.
|
|
161
|
+
recheck:
|
|
162
|
+
name: 🕵️ Re-evaluate a completed reviewer
|
|
163
|
+
if: github.event_name == 'check_run' || (github.event_name == 'status' && github.event.state != 'pending')
|
|
164
|
+
runs-on: ubuntu-latest
|
|
165
|
+
timeout-minutes: 5
|
|
166
|
+
permissions:
|
|
167
|
+
contents: read
|
|
168
|
+
checks: read
|
|
169
|
+
statuses: read
|
|
170
|
+
pull-requests: read
|
|
171
|
+
actions: write
|
|
172
|
+
steps:
|
|
173
|
+
# No head ref: never execute PR files with the dispatch token.
|
|
174
|
+
- name: 📥 Checkout default branch
|
|
175
|
+
uses: actions/checkout@v6
|
|
176
|
+
with:
|
|
177
|
+
persist-credentials: false
|
|
178
|
+
- name: 🔧 Setup Node.js
|
|
179
|
+
uses: actions/setup-node@v6
|
|
180
|
+
with:
|
|
181
|
+
package-manager-cache: false
|
|
182
|
+
node-version: ${{ vars.NODE_VERSION || '24.21.0' }}
|
|
183
|
+
- name: 🕵️ Dispatch the gate for current declared reviewer heads
|
|
184
|
+
env:
|
|
185
|
+
GH_TOKEN: ${{ github.token }}
|
|
186
|
+
run: |
|
|
187
|
+
set -eu
|
|
188
|
+
script=scripts/review-evidence-event.mjs
|
|
189
|
+
if [ ! -f "$script" ]; then
|
|
190
|
+
echo "::error::Reviewer event resolver missing; no gate was dispatched."
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
node "$script" --dispatch
|
|
194
|
+
|
|
154
195
|
vacuity:
|
|
196
|
+
if: github.event_name != 'check_run' && github.event_name != 'status'
|
|
155
197
|
# DECLARATIVE, NOT A QUESTION (#3917). This was `🕵️ Did the required review
|
|
156
198
|
# checks do any work?`, and an unsatisfied verdict exits non-zero, so the
|
|
157
199
|
# row rendered `Did the required review checks do any work? fail`. A yes/no
|