@codyswann/lisa 4.29.12 → 4.31.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/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -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/nightly-e2e-guard-behavior-certificate.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -21
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/required-checks.json +1 -1
- package/nestjs/create-only/.github/required-checks.json +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-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/scripts/two-channel-couplings.json +2 -2
- package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +0 -14
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +86 -143
- package/typescript/create-only/.github/required-checks.json +1 -1
- package/typescript/create-only/.github/workflows/review-evidence.yml +1 -1
- package/typescript/deletions.json +5 -1
- package/typescript/package-lisa/package.lisa.json +2 -2
- package/ui/index.html +0 -18
- package/typescript/create-only/.github/workflows/auto-update-pr-branches-dispatch.yml +0 -27
- package/typescript/create-only/.github/workflows/auto-update-pr-branches.yml +0 -31
- package/typescript/create-only/.github/workflows/required-checks-drift.yml +0 -91
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* yours to edit).
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
|
-
* node scripts/check-skipped-required-checks.mjs [rootDir] [--
|
|
15
|
+
* node scripts/check-skipped-required-checks.mjs [rootDir] [--json]
|
|
16
16
|
* node scripts/check-skipped-required-checks.mjs --vacuity [--fail-on-vacuous]
|
|
17
17
|
* node scripts/check-skipped-required-checks.mjs --pr=1234 [--repo=OWNER/NAME]
|
|
18
18
|
*
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*
|
|
32
32
|
* Two of the three live here:
|
|
33
33
|
*
|
|
34
|
-
* - **Skipped** (
|
|
34
|
+
* - **Skipped** (the offline arm, below): GitHub counts a `skipped`
|
|
35
35
|
* required check as SATISFIED, so a `skip_jobs` token makes the gate
|
|
36
36
|
* decorative. Static, offline, BLOCKING.
|
|
37
37
|
* - **Vacuous** (`--pr` arm): the check really ran and really reported
|
|
@@ -40,11 +40,15 @@
|
|
|
40
40
|
*
|
|
41
41
|
* ## Where this runs
|
|
42
42
|
*
|
|
43
|
-
* Lisa's `quality.yml` runs
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* Lisa's `quality.yml` runs this on every pull request. EVERY arm is offline:
|
|
44
|
+
* nothing here reads a live ruleset, needs a token, or touches the network for
|
|
45
|
+
* the required-context rules. That is a deliberate reduction (#3599) — the
|
|
46
|
+
* scheduled arm that used to re-read the ruleset was removed along with the
|
|
47
|
+
* `administration:read` credential it required, which sat as a standing repo
|
|
48
|
+
* secret in every consumer to detect a rare event. If the committed snapshot
|
|
49
|
+
* and the live ruleset drift apart, that is now discovered by consequence
|
|
50
|
+
* rather than by check, and that is the accepted trade. Do not reintroduce a
|
|
51
|
+
* cheaper detector for it.
|
|
48
52
|
*
|
|
49
53
|
* ## `enforcement`
|
|
50
54
|
*
|
|
@@ -73,13 +77,26 @@
|
|
|
73
77
|
* assert.
|
|
74
78
|
*
|
|
75
79
|
* So `required_contexts` is treated as a cache of a live fetch, not as
|
|
76
|
-
* testimony.
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* `required_contexts` is skipped, no ✅ is
|
|
80
|
-
* CHECKED and why. The rules that do NOT read
|
|
81
|
-
* undeclared token, a token written with whitespace —
|
|
82
|
-
* are true regardless of what the ruleset requires.
|
|
80
|
+
* testimony. Trust is a function of PRESENCE: it is believed while
|
|
81
|
+
* `ruleset.baseline_fetched_at` carries a parseable timestamp, and refused when
|
|
82
|
+
* that stamp is empty or unreadable. Untranscribed, the guard REFUSES to
|
|
83
|
+
* answer: every rule that depends on `required_contexts` is skipped, no ✅ is
|
|
84
|
+
* printed, and the report says NOT CHECKED and why. The rules that do NOT read
|
|
85
|
+
* `required_contexts` — an undeclared token, a token written with whitespace —
|
|
86
|
+
* still run, because they are true regardless of what the ruleset requires.
|
|
87
|
+
*
|
|
88
|
+
* NOTHING EXPIRES, and that is a decided constraint (#3599), not an oversight.
|
|
89
|
+
* A ninety-day ceiling used to sit here, and it was coherent only while a
|
|
90
|
+
* scheduled arm existed that could re-read the ruleset and re-stamp the file.
|
|
91
|
+
* With that arm removed, a deadline is an obligation with no way to discharge
|
|
92
|
+
* it: somebody would have to re-transcribe every consumer's snapshot every
|
|
93
|
+
* quarter forever, or the guard silently goes NOT CHECKED. A guard that
|
|
94
|
+
* reliably expires into "not checked" is worse than one answering from a stamp
|
|
95
|
+
* somebody wrote once, because the first looks healthy right up until it has
|
|
96
|
+
* been quietly inert for a quarter. A softer form — believe it, but warn past N
|
|
97
|
+
* days — is the same substitution and is refused for the same reason: it
|
|
98
|
+
* recreates the recurring manual obligation and adds a signal nobody can act
|
|
99
|
+
* on. Non-empty believed. Empty refused. Nothing expiring.
|
|
83
100
|
*
|
|
84
101
|
* Refusal is exit 1, so an explicit `npm run check:skipped-required-checks`
|
|
85
102
|
* cannot be mistaken for a pass. Under `"enforcement": "warn"` — which is what
|
|
@@ -114,16 +131,18 @@
|
|
|
114
131
|
* authoritative alone; the coherence rules below are what stop either rotting
|
|
115
132
|
* into decoration.
|
|
116
133
|
*
|
|
117
|
-
* That mutual policing has one blind spot, and
|
|
118
|
-
* snapshots in one repo can only catch each other drifting from the CODE.
|
|
134
|
+
* That mutual policing has one blind spot, and it is now UNCOVERED ON PURPOSE:
|
|
135
|
+
* two snapshots in one repo can only catch each other drifting from the CODE.
|
|
119
136
|
* Neither can see the ruleset itself change in the admin console — which is
|
|
120
|
-
* exactly how
|
|
121
|
-
* test still green, because the "independent" transcription was made
|
|
122
|
-
* same reading at the same moment.
|
|
137
|
+
* exactly how one repository's list silently went from ten contexts to eleven,
|
|
138
|
+
* with every test still green, because the "independent" transcription was made
|
|
139
|
+
* from the same reading at the same moment.
|
|
123
140
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
141
|
+
* A scheduled arm used to close that gap by reading the live ruleset, and it
|
|
142
|
+
* was removed (#3599) because closing it cost a permanent `administration:read`
|
|
143
|
+
* token in every consumer. The blind spot is the price of not holding that
|
|
144
|
+
* credential. It is stated here rather than hidden, and it is not an invitation
|
|
145
|
+
* to build a lighter-weight version of the same thing.
|
|
127
146
|
*
|
|
128
147
|
* ## `--pr` — the VACUOUS arm, and why it only ever reports
|
|
129
148
|
*
|
|
@@ -148,8 +167,8 @@
|
|
|
148
167
|
* ## `--vacuity` — why a flag nobody passes is the same defect
|
|
149
168
|
*
|
|
150
169
|
* MEASURED (CodySwannGT/lisa#2928): for the whole life of this arm, NOTHING
|
|
151
|
-
* INVOKED IT. `quality.yml` ran the offline arm with no `--pr`, the shipped
|
|
152
|
-
*
|
|
170
|
+
* INVOKED IT. `quality.yml` ran the offline arm with no `--pr`, the then-shipped
|
|
171
|
+
* scheduled drift workflow ran its own arm, and the package script named
|
|
153
172
|
* `check:vacuous-required-checks` was a bare invocation — so the command named
|
|
154
173
|
* for the vacuous check reported on SKIPS and said nothing about vacuity unless
|
|
155
174
|
* a caller happened to remember `-- --pr=1234`. A rule that runs only when
|
|
@@ -270,18 +289,6 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
|
270
289
|
/** Repo-relative path of the per-repo declaration file. */
|
|
271
290
|
export const DECLARATION_PATH = ".github/required-checks.json";
|
|
272
291
|
|
|
273
|
-
/**
|
|
274
|
-
* How long a transcribed `required_contexts` snapshot stays trustworthy.
|
|
275
|
-
*
|
|
276
|
-
* A SOURCE CONSTANT, not an input: a ceiling somebody can widen from a config
|
|
277
|
-
* file fails open on exactly the runs nobody tests. Rulesets are edited in an
|
|
278
|
-
* admin console with no signal in the repository, so an old transcription is
|
|
279
|
-
* not evidence — it is a memory of evidence. Ninety days is long enough that
|
|
280
|
-
* the weekly `--remote` run refreshes it many times over, and short enough that
|
|
281
|
-
* an abandoned repository stops being told its skips are fine.
|
|
282
|
-
*/
|
|
283
|
-
export const SNAPSHOT_MAX_AGE_DAYS = 90;
|
|
284
|
-
|
|
285
292
|
/**
|
|
286
293
|
* Matches a `skip_jobs:` key and captures everything after the colon.
|
|
287
294
|
*
|
|
@@ -331,7 +338,6 @@ export const VIOLATIONS = Object.freeze({
|
|
|
331
338
|
stale: "declaration_overstates_requirement",
|
|
332
339
|
orphaned: "orphaned_exemption",
|
|
333
340
|
badExemption: "exemption_without_valid_ticket",
|
|
334
|
-
remoteDrift: "ruleset_snapshot_drift",
|
|
335
341
|
whitespace: "whitespace_in_skip_token",
|
|
336
342
|
vacuous: "vacuous_required_check",
|
|
337
343
|
unproven: "unproven_required_check",
|
|
@@ -399,15 +405,8 @@ const ENFORCEMENT_MODES = Object.freeze(["error", "warn"]);
|
|
|
399
405
|
* both that a context is ruleset-required AND that a token it actually skips
|
|
400
406
|
* silences it; that is a reviewed state, and shipping past it is the exact
|
|
401
407
|
* defect this file exists to refuse.
|
|
402
|
-
*
|
|
403
|
-
* `ruleset_snapshot_drift` blocks for the same reason: it only fires under an
|
|
404
|
-
* explicit `--remote` on a repository that filled in its ruleset ids, and it is
|
|
405
|
-
* measured against live truth rather than inferred from a seed.
|
|
406
408
|
*/
|
|
407
|
-
const ALWAYS_BLOCKING = Object.freeze([
|
|
408
|
-
VIOLATIONS.suppressesRequired,
|
|
409
|
-
VIOLATIONS.remoteDrift,
|
|
410
|
-
]);
|
|
409
|
+
const ALWAYS_BLOCKING = Object.freeze([VIOLATIONS.suppressesRequired]);
|
|
411
410
|
|
|
412
411
|
/**
|
|
413
412
|
* Violation kinds that NEVER fail the build, in any enforcement mode.
|
|
@@ -488,6 +487,27 @@ export const SETTLE_INTERVAL_SECONDS = 15;
|
|
|
488
487
|
/** Enables the blocking review-evidence policy. */
|
|
489
488
|
const REQUIRE_REVIEW_EVIDENCE_FLAG = "--require-review-evidence";
|
|
490
489
|
|
|
490
|
+
/**
|
|
491
|
+
* The flag that used to select the live-ruleset drift mode, retired with it.
|
|
492
|
+
*
|
|
493
|
+
* Kept as an explicit rejection rather than left to the argument parser. Every
|
|
494
|
+
* unrecognised `--*` argument is discarded before the positional read, so a
|
|
495
|
+
* caller that still selects the retired mode would otherwise get the ordinary
|
|
496
|
+
* offline run and an exit code of zero — a caller asking for a live comparison
|
|
497
|
+
* and being told "pass" by something that never looked. Retiring a mode
|
|
498
|
+
* silently is how a removed control keeps reporting success.
|
|
499
|
+
*/
|
|
500
|
+
const RETIRED_REMOTE_FLAG = "--remote";
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* What a caller that still selects the retired mode is told.
|
|
504
|
+
*
|
|
505
|
+
* Names the flag only to say it is gone: the caller has to be able to find the
|
|
506
|
+
* thing it must delete. It is deliberately not a suggestion to run anything —
|
|
507
|
+
* there is no live-ruleset mode left to point at.
|
|
508
|
+
*/
|
|
509
|
+
const RETIRED_REMOTE_MESSAGE = `check-skipped-required-checks: \`${RETIRED_REMOTE_FLAG}\` was retired together with the live-ruleset drift arm and the standing \`administration:read\` token it required (CodySwannGT/lisa#3599). There is no live comparison left to run, so this invocation would otherwise have reported success without performing the check that was asked for. Drop the flag, and drop the \`check:skipped-required-checks:remote\` npm script that passes it.`;
|
|
510
|
+
|
|
491
511
|
/** Matches the `GITHUB_REF` a `pull_request` event run carries. */
|
|
492
512
|
const REF_PULL = /^refs\/pull\/(\d+)\/(?:merge|head)$/u;
|
|
493
513
|
|
|
@@ -829,11 +849,19 @@ export function collectSkipJobTokens(rootDir, workflows) {
|
|
|
829
849
|
* the state Lisa's seeds ship in, and the seeds are a GUESS — a guess that was
|
|
830
850
|
* measured wrong (#2476). No stamp, no answer.
|
|
831
851
|
*
|
|
852
|
+
* Trust is PRESENCE, not freshness (#3599). There is no age ceiling here and
|
|
853
|
+
* there must not be one: the scheduled arm that could re-read a live ruleset
|
|
854
|
+
* and re-stamp this file is gone, so an expiry would be a deadline nobody can
|
|
855
|
+
* meet, and the guard would go quietly inert rather than loudly wrong. A
|
|
856
|
+
* warning past N days is the same mistake wearing a softer hat — it recreates
|
|
857
|
+
* the obligation and emits a signal no operator can discharge. The protection
|
|
858
|
+
* that matters is the REFUSAL on an untranscribed snapshot, and that is what
|
|
859
|
+
* the two arms below preserve.
|
|
860
|
+
*
|
|
832
861
|
* @param {object} declaration - The per-repo declaration
|
|
833
|
-
* @param {number} [now] - Current epoch milliseconds, injectable for tests
|
|
834
862
|
* @returns {{trusted: boolean, reason: string}} Whether to believe the snapshot, and why not
|
|
835
863
|
*/
|
|
836
|
-
export function snapshotTrust(declaration
|
|
864
|
+
export function snapshotTrust(declaration) {
|
|
837
865
|
const stamp = declaration.ruleset?.baseline_fetched_at;
|
|
838
866
|
if (typeof stamp !== "string" || stamp.trim() === "") {
|
|
839
867
|
return {
|
|
@@ -841,20 +869,12 @@ export function snapshotTrust(declaration, now = Date.now()) {
|
|
|
841
869
|
reason: `\`ruleset.baseline_fetched_at\` is empty, so \`required_contexts\` has never been transcribed from a live ruleset. Lisa's seed ships a GUESS, and the guess was measured WRONG once in this fleet (#2476): in that repository it named a context no ruleset required and omitted six that were. It names no context here on purpose — what is required is a per-repository fact. Transcribe the real list, stamp the date, and this guard starts answering.`,
|
|
842
870
|
};
|
|
843
871
|
}
|
|
844
|
-
|
|
845
|
-
if (Number.isNaN(fetchedAt)) {
|
|
872
|
+
if (Number.isNaN(Date.parse(stamp))) {
|
|
846
873
|
return {
|
|
847
874
|
trusted: false,
|
|
848
875
|
reason: `\`ruleset.baseline_fetched_at\` is ${JSON.stringify(stamp)}, which is not a date this can read. Use an ISO-8601 timestamp, e.g. "2026-08-13".`,
|
|
849
876
|
};
|
|
850
877
|
}
|
|
851
|
-
const ageDays = (now - fetchedAt) / 86_400_000;
|
|
852
|
-
if (ageDays > SNAPSHOT_MAX_AGE_DAYS) {
|
|
853
|
-
return {
|
|
854
|
-
trusted: false,
|
|
855
|
-
reason: `\`required_contexts\` was last transcribed ${Math.floor(ageDays)} days ago, past the ${SNAPSHOT_MAX_AGE_DAYS}-day ceiling. Rulesets are edited in an admin console with no signal in this repository, so a stale transcription is a memory of evidence rather than evidence.`,
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
878
|
return { trusted: true, reason: "" };
|
|
859
879
|
}
|
|
860
880
|
|
|
@@ -1897,67 +1917,6 @@ export function vacuityRefusal(input) {
|
|
|
1897
1917
|
return null;
|
|
1898
1918
|
}
|
|
1899
1919
|
|
|
1900
|
-
/**
|
|
1901
|
-
* Fetches the live required contexts for every declared ruleset.
|
|
1902
|
-
*
|
|
1903
|
-
* @param {object} ruleset - `{ repo, ids }` from the declaration
|
|
1904
|
-
* @returns {string[]} Live contexts across all declared rulesets
|
|
1905
|
-
* @throws {Error} When `gh` is unavailable or the API cannot be read
|
|
1906
|
-
*/
|
|
1907
|
-
export function fetchLiveRequiredContexts(ruleset) {
|
|
1908
|
-
if (
|
|
1909
|
-
!ruleset?.repo ||
|
|
1910
|
-
!Array.isArray(ruleset.ids) ||
|
|
1911
|
-
ruleset.ids.length === 0
|
|
1912
|
-
) {
|
|
1913
|
-
throw new Error(
|
|
1914
|
-
`check-skipped-required-checks: --remote needs \`ruleset.repo\` and \`ruleset.ids\` in ${DECLARATION_PATH}.`
|
|
1915
|
-
);
|
|
1916
|
-
}
|
|
1917
|
-
const contexts = [];
|
|
1918
|
-
for (const id of ruleset.ids) {
|
|
1919
|
-
const raw = boundedExecFileSync(
|
|
1920
|
-
"gh",
|
|
1921
|
-
[
|
|
1922
|
-
"api",
|
|
1923
|
-
`repos/${ruleset.repo}/rulesets/${id}`,
|
|
1924
|
-
"--jq",
|
|
1925
|
-
'.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context',
|
|
1926
|
-
],
|
|
1927
|
-
{ encoding: "utf8" }
|
|
1928
|
-
);
|
|
1929
|
-
for (const line of raw.split("\n").map(value => value.trim())) {
|
|
1930
|
-
if (line !== "" && !contexts.includes(line)) contexts.push(line);
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
return contexts;
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
/**
|
|
1937
|
-
* Diffs the committed snapshot against the live ruleset, in BOTH directions.
|
|
1938
|
-
*
|
|
1939
|
-
* Both directions matter. A context added in the admin console makes the
|
|
1940
|
-
* snapshot UNDER-detect (acmeorgd's ten-to-eleven drift, unnoticed for a day with
|
|
1941
|
-
* every test green). A context removed there makes it OVER-detect, and the
|
|
1942
|
-
* obvious fix for a false alarm is to weaken the guard.
|
|
1943
|
-
*
|
|
1944
|
-
* @param {ReadonlyArray<string>} snapshot - Committed contexts
|
|
1945
|
-
* @param {ReadonlyArray<string>} live - Contexts from the API
|
|
1946
|
-
* @returns {object[]} Drift violations
|
|
1947
|
-
*/
|
|
1948
|
-
export function compareRulesetBaseline(snapshot, live) {
|
|
1949
|
-
const added = live.filter(context => !snapshot.includes(context));
|
|
1950
|
-
const removed = snapshot.filter(context => !live.includes(context));
|
|
1951
|
-
if (added.length === 0 && removed.length === 0) return [];
|
|
1952
|
-
return [
|
|
1953
|
-
{
|
|
1954
|
-
kind: VIOLATIONS.remoteDrift,
|
|
1955
|
-
token: null,
|
|
1956
|
-
message: `\`required_contexts\` has drifted from the live ruleset.${added.length ? `\n Live but not committed (the snapshot UNDER-detects): ${added.map(name => `"${name}"`).join(", ")}` : ""}${removed.length ? `\n Committed but not live (the snapshot OVER-detects): ${removed.map(name => `"${name}"`).join(", ")}` : ""}\n Update the snapshot and re-read what it now implies about the skip declarations.`,
|
|
1957
|
-
},
|
|
1958
|
-
];
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
1920
|
/**
|
|
1962
1921
|
* Reads a `--name=<seconds>` flag, falling back when absent or unreadable.
|
|
1963
1922
|
*
|
|
@@ -2071,17 +2030,18 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
2071
2030
|
/**
|
|
2072
2031
|
* Runs the guard.
|
|
2073
2032
|
*
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2076
|
-
*
|
|
2077
|
-
* disagreement surfaces as `ruleset_snapshot_drift` rather than as a verdict
|
|
2078
|
-
* about skips.
|
|
2033
|
+
* Every arm is OFFLINE. There is no network read and no token anywhere in this
|
|
2034
|
+
* path: the required-context rules answer from the committed snapshot when
|
|
2035
|
+
* {@link snapshotTrust} believes it, and refuse when it does not.
|
|
2079
2036
|
*
|
|
2080
2037
|
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
2081
2038
|
* @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}
|
|
2082
2039
|
* @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
|
|
2083
2040
|
*/
|
|
2084
2041
|
export function runGuard(argv, options = {}) {
|
|
2042
|
+
if (argv.includes(RETIRED_REMOTE_FLAG)) {
|
|
2043
|
+
throw new Error(RETIRED_REMOTE_MESSAGE);
|
|
2044
|
+
}
|
|
2085
2045
|
if (
|
|
2086
2046
|
argv.includes(REQUIRE_REVIEW_EVIDENCE_FLAG) &&
|
|
2087
2047
|
!argv.includes("--vacuity") &&
|
|
@@ -2095,26 +2055,11 @@ export function runGuard(argv, options = {}) {
|
|
|
2095
2055
|
const rootDir = positional[0] ?? process.cwd();
|
|
2096
2056
|
const declaration = loadDeclaration(rootDir);
|
|
2097
2057
|
const collected = collectSkipJobTokens(rootDir, declaration.workflows);
|
|
2098
|
-
const
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
const trust = remote
|
|
2103
|
-
? { trusted: true, reason: "" }
|
|
2104
|
-
: snapshotTrust(declaration);
|
|
2105
|
-
const result = evaluateSkippedRequiredChecks(
|
|
2106
|
-
live === undefined
|
|
2107
|
-
? declaration
|
|
2108
|
-
: { ...declaration, required_contexts: live },
|
|
2109
|
-
collected.tokens,
|
|
2110
|
-
{ trustRequiredContexts: trust.trusted }
|
|
2111
|
-
);
|
|
2058
|
+
const trust = snapshotTrust(declaration);
|
|
2059
|
+
const result = evaluateSkippedRequiredChecks(declaration, collected.tokens, {
|
|
2060
|
+
trustRequiredContexts: trust.trusted,
|
|
2061
|
+
});
|
|
2112
2062
|
const violations = [...collected.violations, ...result.violations];
|
|
2113
|
-
if (live !== undefined) {
|
|
2114
|
-
violations.push(
|
|
2115
|
-
...compareRulesetBaseline(declaration.required_contexts, live)
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2118
2063
|
|
|
2119
2064
|
// The vacuity arm is layered ON TOP of the offline run rather than replacing
|
|
2120
2065
|
// it: it is a third variant of one family, so it belongs in one report. Its
|
|
@@ -2237,8 +2182,6 @@ function main(argv) {
|
|
|
2237
2182
|
"```",
|
|
2238
2183
|
result.recipe,
|
|
2239
2184
|
"```",
|
|
2240
|
-
"",
|
|
2241
|
-
`Meanwhile \`--remote\` answers WITHOUT the cache, because it reads the ruleset live: \`npm run check:skipped-required-checks:remote\`.`,
|
|
2242
2185
|
""
|
|
2243
2186
|
);
|
|
2244
2187
|
process.stderr.write(
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"To arm it: gh api repos/OWNER/NAME/rulesets --jq '.[] | \"\\(.id) \\(.name)\"' to find the ids, then gh api repos/OWNER/NAME/rulesets/ID --jq '.rules[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context' for the contexts. Paste them into `required_contexts` BYTE FOR BYTE (emoji and the ' / ' separator included), fill in `ruleset.repo` / `ruleset.ids`, and set `baseline_fetched_at` to today.",
|
|
8
8
|
"The guard compares with exact string equality on purpose: a repo routinely carries confusable pairs (an external app's required `GitGuardian Security Checks` beside a skippable, not-required `🔐 Credential Leakage` proving the same property; `🧹 Lint` beside `🐢 Slow Lint Rules`, whose skip tokens `lint` and `lint_slow` are a strict prefix pair), and a fuzzy match would raise a false alarm whose obvious fix is to delete the guard.",
|
|
9
9
|
"The worst confusable pair Lisa ever shipped was a NOT-required `🧪 Run Tests` beside the required `🧪 Run Unit Tests` — it merged red on two PRs because 'Run Tests failed' reads like the required test gate failing. It was DELETED in #2485, not renamed, because it only re-ran what the two coverage-carrying required contexts already run. If you find a near-duplicate advisory check in your own roster, prefer deleting it over trusting a reader to tell three similar names apart.",
|
|
10
|
-
"A transcription
|
|
10
|
+
"A transcription NEVER EXPIRES. Trust is presence-based: a non-empty, parseable `baseline_fetched_at` means somebody read the list off a live ruleset, and it is believed; an empty or unreadable one means it was never transcribed, and the guard refuses to answer. A ninety-day ceiling used to sit here alongside a scheduled workflow that re-read the live ruleset and re-stamped this file. Both were removed (#3599) because that workflow needed a standing `administration:read` token in every repository. A deadline with no refresh mechanism is an obligation nobody can discharge, and a guard that reliably expires into NOT CHECKED looks healthy right up until it has been quietly inert for a quarter. The consequence is stated plainly: if this list and your live ruleset drift apart, you find out by consequence rather than by check. Re-transcribe when you change your ruleset.",
|
|
11
11
|
"Lisa's quality.yml runs the offline arm on every pull request. This seed ships `\"enforcement\": \"warn\"`, which downgrades findings AND the NOT-CHECKED refusal to reports so a fresh install does not go red on arrival. Delete the key once you have transcribed the list — then it blocks.",
|
|
12
12
|
"`skip_jobs` is matched as an exact comma-delimited token and GitHub Actions expression syntax has no string-replace, so write the list with no spaces: `skip_jobs: 'a,b'`, never `skip_jobs: 'a, b'`. A spaced token matches nothing and the job runs — reported as `whitespace_in_skip_token`.",
|
|
13
13
|
"`_example_required_contexts` below is a STARTING POINT FOR TYPING, never read by the guard. Verify every line against your own ruleset before promoting any of it.",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# WHY THIS WORKFLOW EXISTS. The rule that detects it shipped long before
|
|
13
13
|
# anything invoked it: `quality.yml` runs the guard's OFFLINE arm with no `--pr`,
|
|
14
|
-
#
|
|
14
|
+
# a scheduled drift workflow ran its own arm, and the package
|
|
15
15
|
# script named for the vacuous check was a bare invocation. So the detection was
|
|
16
16
|
# `declared-but-uncallable` — the very family the guard exists to describe,
|
|
17
17
|
# one level up inside its own shipping. Measured on the repository that owns it:
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
".github/workflows/create-jira-issue-on-failure.yml",
|
|
27
27
|
".github/workflows/create-sentry-issue-on-failure.yml",
|
|
28
28
|
".github/workflows/reusable-auto-update-pr-branches.yml",
|
|
29
|
+
".github/workflows/reusable-auto-update-pr-branches-dispatch.yml",
|
|
30
|
+
".github/workflows/auto-update-pr-branches.yml",
|
|
31
|
+
".github/workflows/auto-update-pr-branches-dispatch.yml",
|
|
29
32
|
".github/workflows/reusable-claude.yml",
|
|
30
33
|
".github/workflows/reusable-claude-ci-auto-fix.yml",
|
|
31
34
|
".github/workflows/reusable-claude-code-review-response.yml",
|
|
@@ -46,7 +49,8 @@
|
|
|
46
49
|
".github/workflows/reusable-claude-nightly-code-complexity-rails.yml",
|
|
47
50
|
".github/workflows/reusable-claude-nightly-jira-triage.yml",
|
|
48
51
|
".github/workflows/reusable-claude-nightly-test-coverage-rails.yml",
|
|
49
|
-
".github/workflows/reusable-claude-nightly-test-improvement-rails.yml"
|
|
52
|
+
".github/workflows/reusable-claude-nightly-test-improvement-rails.yml",
|
|
53
|
+
".github/workflows/required-checks-drift.yml"
|
|
50
54
|
],
|
|
51
55
|
"keep": []
|
|
52
56
|
}
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"prepare": "node -e \"if (process.env.INIT_CWD?.includes('.serverless')) process.exit(0); process.exit(1);\" || husky install || true",
|
|
23
23
|
"nightly:health": "node scripts/check-nightly-e2e-health.mjs",
|
|
24
24
|
"check:skipped-required-checks": "node scripts/check-skipped-required-checks.mjs",
|
|
25
|
-
"check:skipped-required-checks:remote": "node scripts/check-skipped-required-checks.mjs --remote",
|
|
26
25
|
"check:vacuous-required-checks": "node scripts/check-skipped-required-checks.mjs --vacuity",
|
|
27
26
|
"test:node": "LISA_TEST_SCRATCH_PREFIXES='[\"node-\"]' lisa-test-run --profile typescript --adapter direct -- node scripts/lisa-test-node.mjs",
|
|
28
27
|
"check:work-item": "node scripts/lisa-work-item.mjs validate-pr",
|
|
@@ -110,7 +109,8 @@
|
|
|
110
109
|
],
|
|
111
110
|
"scripts": [
|
|
112
111
|
"knip",
|
|
113
|
-
"knip:lisa"
|
|
112
|
+
"knip:lisa",
|
|
113
|
+
"check:skipped-required-checks:remote"
|
|
114
114
|
]
|
|
115
115
|
},
|
|
116
116
|
"adopt": {
|
package/ui/index.html
CHANGED
|
@@ -8755,24 +8755,6 @@
|
|
|
8755
8755
|
{ t: "mono", v: "pull_request" },
|
|
8756
8756
|
{ t: "text", v: "Deploy & environments" },
|
|
8757
8757
|
],
|
|
8758
|
-
[
|
|
8759
|
-
{ t: "mono", v: "auto-update-pr-branches.yml" },
|
|
8760
|
-
{
|
|
8761
|
-
t: "text",
|
|
8762
|
-
v: "Upkeep — keeps open PRs rebased on their base",
|
|
8763
|
-
},
|
|
8764
|
-
{ t: "mono", v: "push · pull_request" },
|
|
8765
|
-
{ t: "text", v: "—" },
|
|
8766
|
-
],
|
|
8767
|
-
[
|
|
8768
|
-
{ t: "mono", v: "auto-update-pr-branches-dispatch.yml" },
|
|
8769
|
-
{
|
|
8770
|
-
t: "text",
|
|
8771
|
-
v: "Upkeep — on-demand handler for the same rebase, via repository dispatch",
|
|
8772
|
-
},
|
|
8773
|
-
{ t: "mono", v: "repository_dispatch" },
|
|
8774
|
-
{ t: "text", v: "—" },
|
|
8775
|
-
],
|
|
8776
8758
|
[
|
|
8777
8759
|
{ t: "mono", v: "zap-baseline.yml" },
|
|
8778
8760
|
{
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Seeded by Lisa on first setup — this file is YOURS.
|
|
2
|
-
# Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
|
|
3
|
-
# You can customize this file — Lisa will not overwrite it.
|
|
4
|
-
|
|
5
|
-
name: Auto-update PR branches (Dispatch Handler)
|
|
6
|
-
|
|
7
|
-
on:
|
|
8
|
-
repository_dispatch:
|
|
9
|
-
types:
|
|
10
|
-
- auto-update-pr-branches
|
|
11
|
-
- auto-update-pr-branch
|
|
12
|
-
|
|
13
|
-
permissions:
|
|
14
|
-
contents: write
|
|
15
|
-
pull-requests: write
|
|
16
|
-
id-token: write
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
dispatch:
|
|
20
|
-
uses: CodySwannGT/lisa/.github/workflows/reusable-auto-update-pr-branches-dispatch.yml@main
|
|
21
|
-
with:
|
|
22
|
-
action: ${{ github.event.action }}
|
|
23
|
-
ref_name: ${{ github.event.client_payload.ref_name || '' }}
|
|
24
|
-
repo_full_name: ${{ github.event.client_payload.repo_full_name }}
|
|
25
|
-
pr_number: ${{ github.event.client_payload.pr_number || 0 }}
|
|
26
|
-
secrets:
|
|
27
|
-
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Seeded by Lisa on first setup — this file is YOURS.
|
|
2
|
-
# Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
|
|
3
|
-
# You can customize this file — Lisa will not overwrite it.
|
|
4
|
-
|
|
5
|
-
name: Auto-update PR branches
|
|
6
|
-
|
|
7
|
-
on:
|
|
8
|
-
push:
|
|
9
|
-
branches:
|
|
10
|
-
- main
|
|
11
|
-
- staging
|
|
12
|
-
- dev
|
|
13
|
-
pull_request:
|
|
14
|
-
types: [opened, reopened, ready_for_review]
|
|
15
|
-
|
|
16
|
-
permissions:
|
|
17
|
-
contents: write
|
|
18
|
-
pull-requests: write
|
|
19
|
-
id-token: write
|
|
20
|
-
|
|
21
|
-
jobs:
|
|
22
|
-
autoupdate:
|
|
23
|
-
uses: CodySwannGT/lisa/.github/workflows/reusable-auto-update-pr-branches.yml@main
|
|
24
|
-
with:
|
|
25
|
-
event_name: ${{ github.event_name }}
|
|
26
|
-
ref_name: ${{ github.ref_name }}
|
|
27
|
-
pr_base_ref: ${{ github.event.pull_request.base.ref || '' }}
|
|
28
|
-
pr_number: ${{ github.event.pull_request.number || 0 }}
|
|
29
|
-
repo_full_name: ${{ github.repository }}
|
|
30
|
-
secrets:
|
|
31
|
-
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Seeded by Lisa on first setup — this file is YOURS.
|
|
2
|
-
# Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
|
|
3
|
-
# -----------------------------------------------------------------------------
|
|
4
|
-
# Required-checks snapshot drift — the REMOTE arm (#2426)
|
|
5
|
-
# -----------------------------------------------------------------------------
|
|
6
|
-
# `.github/required-checks.json` holds two reviewed snapshots that police each
|
|
7
|
-
# other: the contexts your ruleset requires, and what each `skip_jobs` token
|
|
8
|
-
# silences. Two snapshots inside one repository can only catch each other
|
|
9
|
-
# drifting from the CODE. Neither can see the RULESET change, because rulesets
|
|
10
|
-
# are edited in an admin console — which is exactly how one repo's list went
|
|
11
|
-
# from ten required contexts to eleven with every test still green for a day.
|
|
12
|
-
#
|
|
13
|
-
# So this runs the `--remote` arm on a schedule, and the pull-request path stays
|
|
14
|
-
# offline. That split is the whole design: an enforced gate that needed network
|
|
15
|
-
# and `gh` auth on every run would flake, and a flaky gate gets skipped — which
|
|
16
|
-
# reintroduces the false-green class the guard exists to refuse. This workflow
|
|
17
|
-
# never runs on `pull_request`, so it cannot wedge a merge.
|
|
18
|
-
#
|
|
19
|
-
# `--remote` is also the arm that STRUCTURALLY CANNOT LIE (#2476). It evaluates
|
|
20
|
-
# the skip rules against the contexts it just read from the live ruleset, not
|
|
21
|
-
# against the committed cache, so it answers correctly even in a repository
|
|
22
|
-
# whose `required_contexts` has never been transcribed — which is exactly the
|
|
23
|
-
# state the offline arm refuses to answer in. Arming this workflow is therefore
|
|
24
|
-
# the fastest way to get a real answer.
|
|
25
|
-
#
|
|
26
|
-
# Reading a repository ruleset needs a token with administration:read. The
|
|
27
|
-
# default GITHUB_TOKEN generally does not have it, so provide a PAT as the
|
|
28
|
-
# repository secret RULESET_READ_TOKEN. Without it this job goes red rather
|
|
29
|
-
# than reporting a clean bill of health it never checked.
|
|
30
|
-
#
|
|
31
|
-
# Until `ruleset.repo` / `ruleset.ids` are filled in, this reports UNCONFIGURED
|
|
32
|
-
# and passes. Find the ids with:
|
|
33
|
-
# gh api repos/OWNER/NAME/rulesets --jq '.[] | "\(.id) \(.name)"'
|
|
34
|
-
|
|
35
|
-
name: 🔒 Required-Checks Drift
|
|
36
|
-
|
|
37
|
-
on:
|
|
38
|
-
schedule:
|
|
39
|
-
# Weekly. A ruleset can be edited in the admin console on a day nobody
|
|
40
|
-
# touched the repository, so change-triggered checking alone would miss it.
|
|
41
|
-
- cron: '0 8 * * 1'
|
|
42
|
-
workflow_dispatch:
|
|
43
|
-
|
|
44
|
-
permissions:
|
|
45
|
-
contents: read
|
|
46
|
-
|
|
47
|
-
jobs:
|
|
48
|
-
drift:
|
|
49
|
-
name: 🔒 Ruleset snapshot vs live ruleset
|
|
50
|
-
runs-on: ubuntu-latest
|
|
51
|
-
timeout-minutes: 10
|
|
52
|
-
|
|
53
|
-
steps:
|
|
54
|
-
- name: 📥 Checkout repository
|
|
55
|
-
uses: actions/checkout@v6
|
|
56
|
-
with:
|
|
57
|
-
persist-credentials: false
|
|
58
|
-
|
|
59
|
-
- name: 🔧 Setup Node.js
|
|
60
|
-
uses: actions/setup-node@v6
|
|
61
|
-
with:
|
|
62
|
-
package-manager-cache: false
|
|
63
|
-
node-version: '22.21.1'
|
|
64
|
-
|
|
65
|
-
- name: 🔒 Compare the committed snapshot against the live ruleset
|
|
66
|
-
env:
|
|
67
|
-
GH_TOKEN: ${{ secrets.RULESET_READ_TOKEN || github.token }}
|
|
68
|
-
run: |
|
|
69
|
-
set -euo pipefail
|
|
70
|
-
# An absent input is a FAILURE, never a skip — the same flip #2933
|
|
71
|
-
# made to the `🔒 Skipped Required Checks` job in quality.yml, for the
|
|
72
|
-
# same reason: this workflow is installed by the same `lisa apply`
|
|
73
|
-
# that installs the prover and the seed, so neither can be absent
|
|
74
|
-
# without the install being broken, and a weekly run that reports
|
|
75
|
-
# success having compared nothing is the drift it exists to catch.
|
|
76
|
-
if [ ! -f scripts/check-skipped-required-checks.mjs ]; then
|
|
77
|
-
echo "::error title=Skipped-required-check prover missing::scripts/check-skipped-required-checks.mjs does not exist, so the committed snapshot was compared against nothing. Run \`lisa apply\` to install the Lisa-managed script."
|
|
78
|
-
exit 1
|
|
79
|
-
fi
|
|
80
|
-
if [ ! -f .github/required-checks.json ]; then
|
|
81
|
-
echo "::error title=Required-checks declaration missing::.github/required-checks.json does not exist, so there is no committed snapshot to compare against the live ruleset. It is create-only and Lisa ships a seed — run \`lisa apply\` to get it."
|
|
82
|
-
exit 1
|
|
83
|
-
fi
|
|
84
|
-
configured="$(node -e "const d=require('./.github/required-checks.json');process.stdout.write(String(Boolean(d.ruleset&&d.ruleset.repo&&Array.isArray(d.ruleset.ids)&&d.ruleset.ids.length)))")"
|
|
85
|
-
if [ "$configured" != "true" ]; then
|
|
86
|
-
echo "UNCONFIGURED: .github/required-checks.json has no \`ruleset.repo\` / \`ruleset.ids\`, so drift cannot be checked."
|
|
87
|
-
echo "Fill them in to arm this: gh api repos/OWNER/NAME/rulesets --jq '.[] | \"\\(.id) \\(.name)\"'"
|
|
88
|
-
echo "::warning title=Required-checks drift UNCHECKED::Fill in ruleset.repo and ruleset.ids in .github/required-checks.json to arm the remote drift check."
|
|
89
|
-
exit 0
|
|
90
|
-
fi
|
|
91
|
-
node scripts/check-skipped-required-checks.mjs --remote
|