@codyswann/lisa 4.81.8 → 4.82.0
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/cli/lisa-test-run-jest-merge.d.ts +11 -0
- package/dist/cli/lisa-test-run-jest-merge.d.ts.map +1 -0
- package/dist/cli/lisa-test-run-jest-merge.js +152 -0
- package/dist/cli/lisa-test-run-jest-merge.js.map +1 -0
- package/dist/cli/lisa-test-run-shard.d.ts +123 -0
- package/dist/cli/lisa-test-run-shard.d.ts.map +1 -0
- package/dist/cli/lisa-test-run-shard.js +333 -0
- package/dist/cli/lisa-test-run-shard.js.map +1 -0
- package/dist/cli/lisa-test-run.d.ts.map +1 -1
- package/dist/cli/lisa-test-run.js +38 -1
- package/dist/cli/lisa-test-run.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -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 +16 -5
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/required-checks.json +2 -1
- package/nestjs/create-only/.github/required-checks.json +2 -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/scripts/check-skipped-required-checks.mjs +299 -3
- package/typescript/create-only/.github/required-checks.json +2 -1
|
@@ -215,6 +215,29 @@
|
|
|
215
215
|
* credential. It is stated here rather than hidden, and it is not an invitation
|
|
216
216
|
* to build a lighter-weight version of the same thing.
|
|
217
217
|
*
|
|
218
|
+
* ## `required_contexts_by_branch` — absence is judged per BASE branch (#4484)
|
|
219
|
+
*
|
|
220
|
+
* `required_contexts` is the UNION across every protected branch. That is the
|
|
221
|
+
* right set for the skip and outcome arms, which ask "could this context gate
|
|
222
|
+
* a merge anywhere?", and the WRONG one for the absent arm, which asks "did a
|
|
223
|
+
* context THIS pull request's merge depends on fail to report?". Measured in a
|
|
224
|
+
* caller repository: two contexts required on `dev` only, so every
|
|
225
|
+
* staging-base pull request — green on everything staging requires — failed
|
|
226
|
+
* the evidence job with `absent_required_check` for a dev-only gate, and
|
|
227
|
+
* operators learned to ignore the job.
|
|
228
|
+
*
|
|
229
|
+
* So the declaration MAY carry `required_contexts_by_branch: { <branch>: [...] }`
|
|
230
|
+
* — transcribed from `rules/branches/<branch>`, which needs only read access,
|
|
231
|
+
* by hand or by a host refresh where a repository has one. Lisa ships no
|
|
232
|
+
* refresh and reads nothing live (#3599 stands). The absent arm selects the
|
|
233
|
+
* set for the base ref (`--base`, else `GITHUB_BASE_REF`, else the payload's
|
|
234
|
+
* `pull_request.base.ref`, the last two only for the event's own pull
|
|
235
|
+
* request). With no set
|
|
236
|
+
* for that base it falls back to the union — findings and exit code exactly as
|
|
237
|
+
* before — and REPORTS it as `required_contexts_branch_fallback`, never
|
|
238
|
+
* silently. Every run that reaches the arm prints which set it used. Trust is
|
|
239
|
+
* unchanged: the one `ruleset.baseline_fetched_at` stamp covers both lists.
|
|
240
|
+
*
|
|
218
241
|
* ## `--pr` — the VACUOUS arm, and why it only ever reports
|
|
219
242
|
*
|
|
220
243
|
* Measured (CodySwannGT/lisa#2497): `CodeRabbit` was in this repository's
|
|
@@ -473,6 +496,22 @@ export const VIOLATIONS = Object.freeze({
|
|
|
473
496
|
requiredAmbiguous: "required_context_ambiguous",
|
|
474
497
|
});
|
|
475
498
|
|
|
499
|
+
/**
|
|
500
|
+
* The named notice a run carries when it judged absence against the UNION.
|
|
501
|
+
*
|
|
502
|
+
* `required_contexts` is the union of what every protected branch requires,
|
|
503
|
+
* which is the wrong set for any one pull request whose base requires less
|
|
504
|
+
* (CodySwannGT/lisa#4484): a staging-base pull request was flagged for a
|
|
505
|
+
* dev-only context on every run. `required_contexts_by_branch` carries the
|
|
506
|
+
* per-branch sets; when the base has none, the absent arm still falls back to
|
|
507
|
+
* the union — the pre-#4484 behaviour, findings and exit code unchanged — and
|
|
508
|
+
* this token is what makes that fallback impossible to miss. It is a notice
|
|
509
|
+
* about the DECLARATION'S coverage, not a finding about the pull request, so it
|
|
510
|
+
* travels beside the violations rather than among them and never changes a
|
|
511
|
+
* verdict on its own.
|
|
512
|
+
*/
|
|
513
|
+
export const REQUIRED_SET_FALLBACK = "required_contexts_branch_fallback";
|
|
514
|
+
|
|
476
515
|
/**
|
|
477
516
|
* The shipped description vocabulary for review-bot style checks.
|
|
478
517
|
*
|
|
@@ -990,6 +1029,7 @@ export function loadDeclaration(rootDir) {
|
|
|
990
1029
|
const declaration = JSON.parse(readFileSync(path, "utf8"));
|
|
991
1030
|
assertRequiredDeclarationKeys(declaration);
|
|
992
1031
|
assertDeclarationCollections(declaration);
|
|
1032
|
+
assertRequiredContextsByBranch(declaration);
|
|
993
1033
|
assertEnforcementMode(declaration);
|
|
994
1034
|
assertSkipJobDeclarations(declaration.skip_job_declarations);
|
|
995
1035
|
assertEvidenceBearingChecks(declaration.evidence_bearing_checks ?? {});
|
|
@@ -1038,6 +1078,49 @@ function assertDeclarationCollections(declaration) {
|
|
|
1038
1078
|
}
|
|
1039
1079
|
}
|
|
1040
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* Validates the optional per-branch required sets (CodySwannGT/lisa#4484).
|
|
1083
|
+
*
|
|
1084
|
+
* Optional so a declaration written before #4484 keeps loading. When present it
|
|
1085
|
+
* must map each branch to an array of context strings, and every one of those
|
|
1086
|
+
* contexts must also be in `required_contexts`: the union is DEFINED as the
|
|
1087
|
+
* union of these sets, so a member missing from it means the two were written
|
|
1088
|
+
* at different times and one is stale. Naming it beats picking one silently.
|
|
1089
|
+
*
|
|
1090
|
+
* @param {object} declaration - Parsed declaration
|
|
1091
|
+
* @returns {void}
|
|
1092
|
+
*/
|
|
1093
|
+
function assertRequiredContextsByBranch(declaration) {
|
|
1094
|
+
const byBranch = declaration.required_contexts_by_branch;
|
|
1095
|
+
if (byBranch === undefined) return;
|
|
1096
|
+
if (
|
|
1097
|
+
byBranch === null ||
|
|
1098
|
+
typeof byBranch !== "object" ||
|
|
1099
|
+
Array.isArray(byBranch)
|
|
1100
|
+
) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`check-skipped-required-checks: \`required_contexts_by_branch\` must be an object mapping each branch name to the array of contexts its rules require, e.g. \`{ "staging": ["CodeRabbit"] }\`.`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
const union = new Set(declaration.required_contexts);
|
|
1106
|
+
for (const [branch, contexts] of Object.entries(byBranch)) {
|
|
1107
|
+
if (
|
|
1108
|
+
!Array.isArray(contexts) ||
|
|
1109
|
+
!contexts.every(context => typeof context === "string")
|
|
1110
|
+
) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`check-skipped-required-checks: \`required_contexts_by_branch.${branch}\` must be an array of context strings, transcribed byte for byte from \`rules/branches/${branch}\`.`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const strays = contexts.filter(context => !union.has(context));
|
|
1116
|
+
if (strays.length > 0) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`check-skipped-required-checks: \`required_contexts_by_branch.${branch}\` names ${strays.map(name => JSON.stringify(name)).join(", ")}, which \`required_contexts\` (the cross-branch union) does not carry. The two were written at different times and one of them is stale — re-run the host refresh so both come from the same read.`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1041
1124
|
/**
|
|
1042
1125
|
* Validates the optional enforcement switch independently of declaration data.
|
|
1043
1126
|
*
|
|
@@ -1688,9 +1771,19 @@ export function evaluateAbsentRequiredChecks(
|
|
|
1688
1771
|
checks,
|
|
1689
1772
|
options = {}
|
|
1690
1773
|
) {
|
|
1691
|
-
|
|
1692
|
-
if (!Array.isArray(required))
|
|
1774
|
+
if (!Array.isArray(declaration.required_contexts))
|
|
1693
1775
|
return { violations: [], checked: 0, absent: [] };
|
|
1776
|
+
// #4484: judged against the BASE branch's own set when the declaration has
|
|
1777
|
+
// one, and against the union — reported, never silently — when it does not.
|
|
1778
|
+
const requiredSet = selectRequiredContexts(
|
|
1779
|
+
declaration,
|
|
1780
|
+
options.base ?? { base: null, source: null }
|
|
1781
|
+
);
|
|
1782
|
+
const required = requiredSet.contexts;
|
|
1783
|
+
const judgedFor =
|
|
1784
|
+
requiredSet.source === "branch"
|
|
1785
|
+
? ` Judged against the contexts required on base \`${requiredSet.base}\`.`
|
|
1786
|
+
: "";
|
|
1694
1787
|
|
|
1695
1788
|
const at = citeHeadSha(options.headSha);
|
|
1696
1789
|
const parked = (options.parkedRuns ?? []).filter(
|
|
@@ -1718,15 +1811,186 @@ export function evaluateAbsentRequiredChecks(
|
|
|
1718
1811
|
return {
|
|
1719
1812
|
checked: required.length,
|
|
1720
1813
|
absent,
|
|
1814
|
+
requiredSet,
|
|
1721
1815
|
violations: absent.map(name => ({
|
|
1722
1816
|
kind: VIOLATIONS.absent,
|
|
1723
1817
|
token: name,
|
|
1724
1818
|
contexts: [name],
|
|
1725
|
-
message: `\`${name}\` is ruleset-required but did not report on this pull request at all.${at} It posted NO check-run — not pending, not failing, ABSENT. \`gh pr checks\` prints no line for it and the pull request page renders it exactly as one still in flight, so every surface says "wait" and none says "broken".${approval} (If the context was renamed, fix \`required_contexts\`; names are compared byte for byte.)`,
|
|
1819
|
+
message: `\`${name}\` is ruleset-required but did not report on this pull request at all.${at}${judgedFor} It posted NO check-run — not pending, not failing, ABSENT. \`gh pr checks\` prints no line for it and the pull request page renders it exactly as one still in flight, so every surface says "wait" and none says "broken".${approval} (If the context was renamed, fix \`required_contexts\`; names are compared byte for byte.)`,
|
|
1726
1820
|
})),
|
|
1727
1821
|
};
|
|
1728
1822
|
}
|
|
1729
1823
|
|
|
1824
|
+
/** The prefix a full ref carries and a declaration's branch keys do not. */
|
|
1825
|
+
const HEADS_PREFIX = "refs/heads/";
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Normalises a base ref to the bare branch name the declaration is keyed by.
|
|
1829
|
+
*
|
|
1830
|
+
* @param {unknown} ref - A ref as some source reported it
|
|
1831
|
+
* @returns {string|null} The branch name, or null when there is none
|
|
1832
|
+
*/
|
|
1833
|
+
function normaliseBaseRef(ref) {
|
|
1834
|
+
if (typeof ref !== "string") return null;
|
|
1835
|
+
const trimmed = ref.trim();
|
|
1836
|
+
const bare = trimmed.startsWith(HEADS_PREFIX)
|
|
1837
|
+
? trimmed.slice(HEADS_PREFIX.length)
|
|
1838
|
+
: trimmed;
|
|
1839
|
+
return bare === "" ? null : bare;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Resolves the branch the pull request under inspection merges INTO.
|
|
1844
|
+
*
|
|
1845
|
+
* Most-explicit-first, the same order {@link resolvePullRequestNumber} uses:
|
|
1846
|
+
* an operator's `--base`, then `GITHUB_BASE_REF` (set by Actions on every
|
|
1847
|
+
* `pull_request` and `pull_request_target` run), then the event payload's
|
|
1848
|
+
* `pull_request.base.ref`. Offline on purpose — every source is a fact this run
|
|
1849
|
+
* already holds, so selecting a set costs no API call, no token scope and no
|
|
1850
|
+
* rate limit, and works the same on a fork pull request. Returning null is a
|
|
1851
|
+
* real answer: the caller falls back to the union and SAYS so.
|
|
1852
|
+
*
|
|
1853
|
+
* The run's own base describes the run's own pull request, and nothing else.
|
|
1854
|
+
* When `options.pr` names the pull request being inspected, GITHUB_BASE_REF
|
|
1855
|
+
* and the payload are used only if the payload's `pull_request.number` IS that
|
|
1856
|
+
* pull request, in the same repository (`options.repo`, from `--repo`).
|
|
1857
|
+
* Otherwise a `--pr=8` inspected from PR #7's staging-base run, or another
|
|
1858
|
+
* repository's PR #7, would be judged against staging, and a dev-base PR could
|
|
1859
|
+
* pass while missing a dev-only context. The ignored base is returned as
|
|
1860
|
+
* `ignoredEventBase` so the fallback can say why it fell back; `--base` still
|
|
1861
|
+
* names it explicitly.
|
|
1862
|
+
*
|
|
1863
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
1864
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
|
|
1865
|
+
* @param {{pr?: string, repo?: string}} [options] - The pull request and repository being inspected, when known
|
|
1866
|
+
* @returns {{base: string|null, source: string|null, ignoredEventBase?: string}} The base and where it came from
|
|
1867
|
+
*/
|
|
1868
|
+
export function resolveBaseRef(argv, env = process.env, options = {}) {
|
|
1869
|
+
const explicit = normaliseBaseRef(readFlagValue(argv, "--base"));
|
|
1870
|
+
if (explicit !== null) return { base: explicit, source: "--base" };
|
|
1871
|
+
|
|
1872
|
+
const eventPath = env.GITHUB_EVENT_PATH;
|
|
1873
|
+
const payload =
|
|
1874
|
+
eventPath !== undefined && eventPath !== "" && existsSync(eventPath)
|
|
1875
|
+
? readJsonOrNull(eventPath)
|
|
1876
|
+
: null;
|
|
1877
|
+
const fromEnv = normaliseBaseRef(env.GITHUB_BASE_REF);
|
|
1878
|
+
const fromEvent = normaliseBaseRef(payload?.pull_request?.base?.ref);
|
|
1879
|
+
const found =
|
|
1880
|
+
fromEnv !== null
|
|
1881
|
+
? { base: fromEnv, source: "GITHUB_BASE_REF" }
|
|
1882
|
+
: fromEvent !== null
|
|
1883
|
+
? { base: fromEvent, source: "GITHUB_EVENT_PATH" }
|
|
1884
|
+
: null;
|
|
1885
|
+
if (found === null) return { base: null, source: null };
|
|
1886
|
+
if (options.pr === undefined) return found;
|
|
1887
|
+
|
|
1888
|
+
const eventPr = payload?.pull_request?.number ?? payload?.number;
|
|
1889
|
+
return String(eventPr) === String(options.pr) &&
|
|
1890
|
+
isEventRepository(options.repo, payload, env)
|
|
1891
|
+
? found
|
|
1892
|
+
: { base: null, source: null, ignoredEventBase: found.base };
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/**
|
|
1896
|
+
* True when the inspected repository is the one this run's event belongs to.
|
|
1897
|
+
*
|
|
1898
|
+
* PR numbers are per repository, so `--repo=other/name --pr=7` from
|
|
1899
|
+
* owner/name's PR #7 run is a different pull request with the same number. No
|
|
1900
|
+
* `--repo` means the run's own repository. An event repository that cannot be
|
|
1901
|
+
* read is not a match: the base would then be a guess.
|
|
1902
|
+
*
|
|
1903
|
+
* @param {string|undefined} repo - The inspected `owner/name`, if given
|
|
1904
|
+
* @param {object|null} payload - The parsed event payload
|
|
1905
|
+
* @param {NodeJS.ProcessEnv} env - Environment
|
|
1906
|
+
* @returns {boolean} True when the event's base describes the inspected repository
|
|
1907
|
+
*/
|
|
1908
|
+
function isEventRepository(repo, payload, env) {
|
|
1909
|
+
if (repo === undefined || repo === "") return true;
|
|
1910
|
+
const eventRepo = payload?.repository?.full_name ?? env.GITHUB_REPOSITORY;
|
|
1911
|
+
return (
|
|
1912
|
+
typeof eventRepo === "string" &&
|
|
1913
|
+
eventRepo.toLowerCase() === repo.toLowerCase()
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Picks the required set the absent arm judges a pull request against.
|
|
1919
|
+
*
|
|
1920
|
+
* The base branch's own set when `required_contexts_by_branch` carries one —
|
|
1921
|
+
* including an EMPTY one, which is a branch that requires nothing, not a branch
|
|
1922
|
+
* nobody recorded. Otherwise the union in `required_contexts`, with a
|
|
1923
|
+
* {@link REQUIRED_SET_FALLBACK} notice naming why. `line` is printed on every
|
|
1924
|
+
* run that reaches the absent arm, so a downstream run proves which path it
|
|
1925
|
+
* took from its own log.
|
|
1926
|
+
*
|
|
1927
|
+
* @param {object} declaration - The per-repo declaration
|
|
1928
|
+
* @param {{base: string|null, source: string|null}} base - The resolved base ref
|
|
1929
|
+
* @returns {{source: "branch"|"union", base: string|null, baseSource: string|null, contexts: string[], fallback: {kind: string, reason: string}|null, line: string}} The selection
|
|
1930
|
+
*/
|
|
1931
|
+
export function selectRequiredContexts(declaration, base) {
|
|
1932
|
+
const byBranch = declaration.required_contexts_by_branch;
|
|
1933
|
+
const branchSet =
|
|
1934
|
+
base.base !== null &&
|
|
1935
|
+
byBranch !== undefined &&
|
|
1936
|
+
Object.hasOwn(byBranch, base.base)
|
|
1937
|
+
? byBranch[base.base]
|
|
1938
|
+
: undefined;
|
|
1939
|
+
if (Array.isArray(branchSet)) {
|
|
1940
|
+
return {
|
|
1941
|
+
source: "branch",
|
|
1942
|
+
base: base.base,
|
|
1943
|
+
baseSource: base.source,
|
|
1944
|
+
contexts: [...branchSet],
|
|
1945
|
+
fallback: null,
|
|
1946
|
+
line: `required contexts: per-branch set for base '${base.base}' (${branchSet.length} contexts)`,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
return {
|
|
1950
|
+
source: "union",
|
|
1951
|
+
base: base.base,
|
|
1952
|
+
baseSource: base.source,
|
|
1953
|
+
contexts: [...(declaration.required_contexts ?? [])],
|
|
1954
|
+
fallback: {
|
|
1955
|
+
kind: REQUIRED_SET_FALLBACK,
|
|
1956
|
+
reason: unionFallbackReason(declaration, base),
|
|
1957
|
+
},
|
|
1958
|
+
line:
|
|
1959
|
+
base.base === null
|
|
1960
|
+
? "required contexts: union fallback (no base ref resolved)"
|
|
1961
|
+
: `required contexts: union fallback (no set for base '${base.base}')`,
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Says why a run judged against the union, and what would end the fallback.
|
|
1967
|
+
*
|
|
1968
|
+
* The remedy is one a consumer of Lisa's shipped seed can always carry out:
|
|
1969
|
+
* transcribe the branch's rules by hand. Lisa ships no refresh, so a host
|
|
1970
|
+
* refresh is named only as an option for a repository that has one.
|
|
1971
|
+
*
|
|
1972
|
+
* @param {object} declaration - The per-repo declaration
|
|
1973
|
+
* @param {{base: string|null, ignoredEventBase?: string}} resolved - The resolved base
|
|
1974
|
+
* @returns {string} The reason
|
|
1975
|
+
*/
|
|
1976
|
+
function unionFallbackReason(declaration, resolved) {
|
|
1977
|
+
const consequence =
|
|
1978
|
+
"so absent required checks were judged against `required_contexts`, the UNION across every protected branch. A context only some other branch requires is reported absent here even though this base may not require it (CodySwannGT/lisa#4484).";
|
|
1979
|
+
const base = resolved.base;
|
|
1980
|
+
if (base === null) {
|
|
1981
|
+
const ignored =
|
|
1982
|
+
resolved.ignoredEventBase === undefined
|
|
1983
|
+
? ""
|
|
1984
|
+
: ` This run's own base, \`${resolved.ignoredEventBase}\`, was NOT used: it belongs to the run's own pull request, and \`--pr\`/\`--repo\` named a different one.`;
|
|
1985
|
+
return `No base ref could be resolved for the inspected pull request (looked at \`--base\`, GITHUB_BASE_REF and the event payload's \`pull_request.base.ref\`), ${consequence}${ignored} Pass \`--base=<branch>\` to name it.`;
|
|
1986
|
+
}
|
|
1987
|
+
const remedy = `Record it by hand: transcribe \`gh api repos/OWNER/NAME/rules/branches/${base} --jq '.[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context'\` (read access only) into \`required_contexts_by_branch.${base}\`, byte for byte, keeping every context in \`required_contexts\` too. A host refresh that writes per-branch sets does the same, if this repository has one.`;
|
|
1988
|
+
if (declaration.required_contexts_by_branch === undefined) {
|
|
1989
|
+
return `The declaration has no \`required_contexts_by_branch\`, ${consequence} ${remedy}`;
|
|
1990
|
+
}
|
|
1991
|
+
return `\`required_contexts_by_branch\` has no set for base \`${base}\`, ${consequence} ${remedy}`;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1730
1994
|
/**
|
|
1731
1995
|
* Descriptions that WAIVE the review gate, matched whole and case-insensitively.
|
|
1732
1996
|
*
|
|
@@ -3984,6 +4248,9 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
3984
4248
|
const absent = evaluateAbsentRequiredChecks(declaration, read.checks, {
|
|
3985
4249
|
headSha: read.headSha,
|
|
3986
4250
|
parkedRuns: options.parkedRuns,
|
|
4251
|
+
// Tied to the pull request actually inspected, so a `--pr` naming another
|
|
4252
|
+
// pull request is never judged against this run's own base.
|
|
4253
|
+
base: resolveBaseRef(argv, options.env ?? process.env, { pr, repo }),
|
|
3987
4254
|
});
|
|
3988
4255
|
// The gate is evaluated on EVERY run, not only when a caller asked it to
|
|
3989
4256
|
// block. Its findings are what make a waiver visible, and a waiver that is
|
|
@@ -4057,6 +4324,7 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4057
4324
|
...(objection?.violations ?? []),
|
|
4058
4325
|
],
|
|
4059
4326
|
absent: absent.absent,
|
|
4327
|
+
requiredSet: absent.requiredSet,
|
|
4060
4328
|
gateStates: gate.states,
|
|
4061
4329
|
gateConditions: gate.conditions,
|
|
4062
4330
|
gateDescriptions: gate.descriptions,
|
|
@@ -4436,6 +4704,7 @@ export function runGuard(argv, options = {}) {
|
|
|
4436
4704
|
"require-review-evidence": { type: "boolean" },
|
|
4437
4705
|
outcomes: { type: "boolean" },
|
|
4438
4706
|
pr: { type: "string" },
|
|
4707
|
+
base: { type: "string" },
|
|
4439
4708
|
repo: { type: "string" },
|
|
4440
4709
|
"settle-timeout": { type: "string" },
|
|
4441
4710
|
"settle-interval": { type: "string" },
|
|
@@ -4676,6 +4945,32 @@ function appendVerdictReport(lines, result) {
|
|
|
4676
4945
|
);
|
|
4677
4946
|
}
|
|
4678
4947
|
|
|
4948
|
+
/**
|
|
4949
|
+
* Prints which required set the absent arm judged against, and annotates a
|
|
4950
|
+
* union fallback (CodySwannGT/lisa#4484).
|
|
4951
|
+
*
|
|
4952
|
+
* The line is printed on EVERY run that reached the absent arm, both paths, so
|
|
4953
|
+
* a downstream run proves from its own log whether the per-branch set was used.
|
|
4954
|
+
* A fallback is additionally a named `::warning` annotation — loud on the
|
|
4955
|
+
* checks page — while the verdict stays whatever the union's findings make it.
|
|
4956
|
+
*
|
|
4957
|
+
* @param {string[]} lines - Report lines
|
|
4958
|
+
* @param {{line: string, fallback: {kind: string, reason: string}|null}|undefined} requiredSet - The selection
|
|
4959
|
+
* @returns {void}
|
|
4960
|
+
*/
|
|
4961
|
+
function appendRequiredSetReport(lines, requiredSet) {
|
|
4962
|
+
if (requiredSet === undefined) return;
|
|
4963
|
+
lines.push(requiredSet.line, "");
|
|
4964
|
+
if (requiredSet.fallback === null) return;
|
|
4965
|
+
lines.push(
|
|
4966
|
+
`⚠️ **${requiredSet.fallback.kind}** — ${requiredSet.fallback.reason}`,
|
|
4967
|
+
""
|
|
4968
|
+
);
|
|
4969
|
+
process.stderr.write(
|
|
4970
|
+
`::warning title=${requiredSet.fallback.kind}::${requiredSet.fallback.reason}\n`
|
|
4971
|
+
);
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4679
4974
|
/**
|
|
4680
4975
|
* Appends and annotates an untrusted ruleset snapshot.
|
|
4681
4976
|
*
|
|
@@ -4956,6 +5251,7 @@ function appendUnexaminedContexts(lines, notExamined) {
|
|
|
4956
5251
|
function buildTextReport(result, policy, outcome) {
|
|
4957
5252
|
const lines = ["## 🔒 Required checks that prove nothing", ""];
|
|
4958
5253
|
appendVerdictReport(lines, result);
|
|
5254
|
+
appendRequiredSetReport(lines, result.vacuity?.requiredSet);
|
|
4959
5255
|
appendSnapshotRefusal(lines, result, policy);
|
|
4960
5256
|
appendInspectionRefusal(lines, outcome);
|
|
4961
5257
|
appendOutcomeReport(lines, result.outcomes);
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"It reads `gh pr checks --json name,state,bucket,description`, the only route that carries the description for a legacy commit status like CodeRabbit's, and falls back to the commit-status API when the token lacks `actions: read` (`gh pr checks` resolves the rollup through `checkSuite.workflowRun`, and without that scope it exits non-zero with EMPTY STDOUT — a failure shaped like a content problem that never says the word \"permission\").",
|
|
18
18
|
"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
19
|
"`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
|
-
"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`."
|
|
20
|
+
"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 then judges a pull request against its BASE branch's set (`--base`, else `GITHUB_BASE_REF`, else the event payload's `pull_request.base.ref`). 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."
|
|
21
22
|
],
|
|
22
23
|
"enforcement": "warn",
|
|
23
24
|
"ruleset": {
|