@codyswann/lisa 4.10.1 → 4.10.3
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/codex/scripts/setup-jira-cli.sh +87 -18
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +3 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +4 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-session-bootstrap.ts +11 -2
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/setup-jira-cli.sh +88 -22
- 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-copilot/hooks/setup-jira-cli.sh +88 -22
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/setup-jira-cli.sh +88 -22
- 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/plugins/src/base/hooks/setup-jira-cli.sh +88 -22
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +321 -10
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* halves implement is `docs/nightly-e2e-gate.md` in Lisa, whose §2 truth table
|
|
11
11
|
* is proven row-by-row by `tests/unit/scripts/nightly-e2e-health*.test.ts`
|
|
12
12
|
* (rows 1-16, `-api` rows 17-20, `-bypass` rows 21-25, `-completeness` row 26,
|
|
13
|
-
* `-issues` rows 27-31).
|
|
13
|
+
* `-issues` rows 27-31), with §10.9 proven by
|
|
14
|
+
* `tests/unit/scripts/nightly-e2e-health-bypass-label.test.ts`.
|
|
14
15
|
*
|
|
15
16
|
* Usage:
|
|
16
17
|
* node scripts/check-nightly-e2e-health.mjs # human report, exit 1 when blocked
|
|
@@ -79,6 +80,22 @@
|
|
|
79
80
|
* instead, which also means a label somebody REMOVED stops waiving. An
|
|
80
81
|
* unreadable pull request is a REJECTED bypass, never a granted one.
|
|
81
82
|
*
|
|
83
|
+
* ## The escape hatch has to EXIST (contract 1.7.0, §10.9)
|
|
84
|
+
*
|
|
85
|
+
* Row 40's other half. There the label existed and the gate could not see it;
|
|
86
|
+
* here there is no label to apply. Measured across four adopters on 2026-08-19,
|
|
87
|
+
* two had the gate required by an active ruleset and no bypass label at all — so
|
|
88
|
+
* §6's preferred path was unreachable and the unaudited admin merge was the only
|
|
89
|
+
* exit, reached by following the printed instructions exactly.
|
|
90
|
+
*
|
|
91
|
+
* `fetchBypassLabelState` measures it on the REPORTING half, nightly, whether or
|
|
92
|
+
* not anybody is blocked yet: discovering the escape hatch is absent while
|
|
93
|
+
* trying to use it is the failure mode, so the gate half — which only speaks
|
|
94
|
+
* once somebody is already blocked — is the wrong place to find out. An
|
|
95
|
+
* unreadable labels API is `unknown`, never `present`; nothing here creates the
|
|
96
|
+
* label, because manufacturing a bypass surface in a repository that never
|
|
97
|
+
* adopted §6 is a new hole rather than a closed one.
|
|
98
|
+
*
|
|
82
99
|
* ## Inherited from three implementations, with one path closed
|
|
83
100
|
*
|
|
84
101
|
* `DECISIVE_CONCLUSIONS` comes from acmeorgb's `check-nightly-e2e.mjs` and is
|
|
@@ -101,7 +118,7 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
|
101
118
|
* rather than running a contract neither half agrees on. See §8 of
|
|
102
119
|
* `docs/nightly-e2e-gate.md` for what counts as major / minor / patch.
|
|
103
120
|
*/
|
|
104
|
-
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.
|
|
121
|
+
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.7.0";
|
|
105
122
|
|
|
106
123
|
/**
|
|
107
124
|
* The conclusions that constitute a verdict about the code.
|
|
@@ -267,6 +284,35 @@ export const REQUIREDNESS = Object.freeze({
|
|
|
267
284
|
unknown: "unknown",
|
|
268
285
|
});
|
|
269
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Whether the audited bypass label EXISTS — measured, never assumed (§10.9).
|
|
289
|
+
*
|
|
290
|
+
* §6 names the `nightly-e2e-bypass` label the preferred way past a red gate.
|
|
291
|
+
* That path only exists if the label exists. Measured across four adopters on
|
|
292
|
+
* 2026-08-19: two had the gate required by an active ruleset and **no bypass
|
|
293
|
+
* label at all**, so the documented escape hatch was unreachable and the only
|
|
294
|
+
* remaining exit was the unaudited admin merge — precisely the outcome §6
|
|
295
|
+
* exists to prevent. A gate whose printed remedy cannot be followed is worse
|
|
296
|
+
* than one with no remedy, because an operator follows the instructions and
|
|
297
|
+
* they do not work.
|
|
298
|
+
*
|
|
299
|
+
* `unknown` is a first-class member for the same reason it is one in
|
|
300
|
+
* `REQUIREDNESS`: an unreadable labels API must never render as either answer.
|
|
301
|
+
* Rendering `present` on an unreadable read would reprint the instruction that
|
|
302
|
+
* does not work; rendering `absent` would file a defect against a repository
|
|
303
|
+
* that is fine. Note which way the asymmetry runs, though — the state this
|
|
304
|
+
* guard must never invent is `present`, because that is the one that claims an
|
|
305
|
+
* escape hatch exists. Nothing here CREATES the label: manufacturing a bypass
|
|
306
|
+
* surface in a repository that never adopted the contract is a different
|
|
307
|
+
* defect, not a fix for this one.
|
|
308
|
+
*/
|
|
309
|
+
export const BYPASS_LABEL_STATE = Object.freeze({
|
|
310
|
+
present: "present",
|
|
311
|
+
absent: "absent",
|
|
312
|
+
unknown: "unknown",
|
|
313
|
+
notMeasured: "not_measured",
|
|
314
|
+
});
|
|
315
|
+
|
|
270
316
|
/**
|
|
271
317
|
* The status-check context the gate publishes, as Lisa's own caller names it.
|
|
272
318
|
*
|
|
@@ -282,6 +328,18 @@ export const REQUIREDNESS = Object.freeze({
|
|
|
282
328
|
*/
|
|
283
329
|
export const DEFAULT_GATE_CONTEXT = "🌙 Nightly E2E Health / 🌙 Gate";
|
|
284
330
|
|
|
331
|
+
/**
|
|
332
|
+
* The audited bypass label's default name, as §6 documents it.
|
|
333
|
+
*
|
|
334
|
+
* One constant rather than three literals: this string is the identity of the
|
|
335
|
+
* escape hatch, and §10.9 now MEASURES whether a label by this name exists.
|
|
336
|
+
* Three copies is three chances for the renderer, the settings resolver and the
|
|
337
|
+
* measurement to disagree about which label they are talking about — and a
|
|
338
|
+
* disagreement there reads to an operator as "the label is missing" when it is
|
|
339
|
+
* merely spelled differently in one of them.
|
|
340
|
+
*/
|
|
341
|
+
export const DEFAULT_BYPASS_LABEL = "nightly-e2e-bypass";
|
|
342
|
+
|
|
285
343
|
// ---------------------------------------------------------------------------
|
|
286
344
|
// SECURITY LIMITS — source constants, never env-readable (portfolio doctrine)
|
|
287
345
|
// ---------------------------------------------------------------------------
|
|
@@ -1895,6 +1953,7 @@ function bypassParagraph(claim, context) {
|
|
|
1895
1953
|
"",
|
|
1896
1954
|
];
|
|
1897
1955
|
}
|
|
1956
|
+
const defect = bypassLabelLines(context);
|
|
1898
1957
|
const recipe = [
|
|
1899
1958
|
`1. Apply the \`${label}\` label to the pull request.`,
|
|
1900
1959
|
"2. Put a line in the **pull request body** naming a ticket and a reason:",
|
|
@@ -1906,16 +1965,73 @@ function bypassParagraph(claim, context) {
|
|
|
1906
1965
|
`3. The waiver is time-boxed and only an \`admin\` or \`maintain\` collaborator can grant one. It is recorded on the run, and the label is removed when the PR closes. This tracking issue stays open either way — a bypass waives ONE pull request, it does not make the nightly green.`,
|
|
1907
1966
|
];
|
|
1908
1967
|
if (claim.state === REQUIREDNESS.required) {
|
|
1909
|
-
return [
|
|
1968
|
+
return [
|
|
1969
|
+
"**If you must merge before this is fixed.**",
|
|
1970
|
+
"",
|
|
1971
|
+
...defect,
|
|
1972
|
+
...recipe,
|
|
1973
|
+
"",
|
|
1974
|
+
];
|
|
1910
1975
|
}
|
|
1911
1976
|
return [
|
|
1912
1977
|
`**If you must merge before this is fixed.** ⚠️ Confirm first whether the gate is actually required on \`${context.branch}\` — that could not be read tonight. If it is not, the \`${label}\` label waives nothing and should not be applied. If it is:`,
|
|
1913
1978
|
"",
|
|
1979
|
+
...defect,
|
|
1914
1980
|
...recipe,
|
|
1915
1981
|
"",
|
|
1916
1982
|
];
|
|
1917
1983
|
}
|
|
1918
1984
|
|
|
1985
|
+
/**
|
|
1986
|
+
* The §10.9 defect block: the escape hatch this gate documents does not exist.
|
|
1987
|
+
*
|
|
1988
|
+
* Rendered ABOVE the waiver recipe rather than instead of it, deliberately. The
|
|
1989
|
+
* recipe is not wrong, it is unreachable — and it becomes reachable the moment
|
|
1990
|
+
* somebody runs the one command printed here. Deleting it would leave a reader
|
|
1991
|
+
* who fixed the label with no instructions for the thing they just enabled.
|
|
1992
|
+
*
|
|
1993
|
+
* Empty when the label is `present`, so a repository that installed the whole
|
|
1994
|
+
* contract reads EXACTLY what it read before this shipped — byte for byte.
|
|
1995
|
+
* That is the control: a defect renderer that also perturbs the healthy case
|
|
1996
|
+
* cannot be told apart from a renderer that fires indiscriminately.
|
|
1997
|
+
*
|
|
1998
|
+
* @param {{bypassLabel: string, bypassLabelState?: {state: string, detail: string|null},
|
|
1999
|
+
* requiredness?: {rulesets?: ReadonlyArray<object>}}} context - Reporting context
|
|
2000
|
+
* @returns {ReadonlyArray<string>} Markdown lines, possibly none
|
|
2001
|
+
*/
|
|
2002
|
+
function bypassLabelLines(context) {
|
|
2003
|
+
const label = context.bypassLabel;
|
|
2004
|
+
const measured = context.bypassLabelState;
|
|
2005
|
+
const state = measured?.state ?? BYPASS_LABEL_STATE.unknown;
|
|
2006
|
+
if (state === BYPASS_LABEL_STATE.absent) {
|
|
2007
|
+
return [
|
|
2008
|
+
"> [!CAUTION]",
|
|
2009
|
+
`> **This gate is armed with no escape hatch.** The \`${label}\` label does not exist in this repository, so the audited waiver below **cannot be applied**. The gate is required by ${describeRulesets(context.requiredness?.rulesets)}, which means a red suite holds every pull request into \`${context.branch}\` and the only remaining route past it is an unaudited admin merge — the one route that records nothing about who waived what. Anyone with \`admin\` or \`maintain\` can fix it with one command:`,
|
|
2010
|
+
">",
|
|
2011
|
+
"> ```",
|
|
2012
|
+
`> gh label create ${label} --description "Audited waiver for the nightly e2e merge gate — docs/nightly-e2e-gate.md section 6" --color B60205`,
|
|
2013
|
+
"> ```",
|
|
2014
|
+
">",
|
|
2015
|
+
"> This report does not create the label itself. Conjuring a bypass surface into a repository whose owners never chose to have one is a different defect, not a fix for this one — so the mismatch is named and the decision is left where it belongs.",
|
|
2016
|
+
"",
|
|
2017
|
+
];
|
|
2018
|
+
}
|
|
2019
|
+
// `not_measured` and `present` both render NOTHING, and they are kept apart
|
|
2020
|
+
// anyway: `not_measured` is "nobody asked", `unknown` is "we asked and the
|
|
2021
|
+
// API would not say". Collapsing the first into the second would print a
|
|
2022
|
+
// hedge in every issue filed by a caller that predates this measurement, and
|
|
2023
|
+
// collapsing the second into the first would silently swallow the case the
|
|
2024
|
+
// hedge exists for.
|
|
2025
|
+
if (state === BYPASS_LABEL_STATE.unknown) {
|
|
2026
|
+
return [
|
|
2027
|
+
"> [!WARNING]",
|
|
2028
|
+
`> Whether the \`${label}\` label exists in this repository could not be read${measured?.detail ? ` (${measured.detail})` : ""}. Confirm it exists before relying on the route below — "we could not check" is not "it is there", and following a recipe for a label that is not there ends at the unaudited admin merge.`,
|
|
2029
|
+
"",
|
|
2030
|
+
];
|
|
2031
|
+
}
|
|
2032
|
+
return [];
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1919
2035
|
/**
|
|
1920
2036
|
* The issue body — written for whoever is standing at the gate.
|
|
1921
2037
|
*
|
|
@@ -2002,8 +2118,15 @@ export function planIssueActions(findings, openIssues, context) {
|
|
|
2002
2118
|
const resolved = {
|
|
2003
2119
|
...context,
|
|
2004
2120
|
requiredness: context.requiredness ?? { state: REQUIREDNESS.unknown },
|
|
2005
|
-
bypassLabel: context.bypassLabel ??
|
|
2121
|
+
bypassLabel: context.bypassLabel ?? DEFAULT_BYPASS_LABEL,
|
|
2006
2122
|
gateContext: context.gateContext ?? DEFAULT_GATE_CONTEXT,
|
|
2123
|
+
// `not_measured`, never `unknown`: a caller that predates §10.9 did not ask
|
|
2124
|
+
// about the label, and printing "we could not read it" for a question
|
|
2125
|
+
// nobody put is a false report about this repository.
|
|
2126
|
+
bypassLabelState: context.bypassLabelState ?? {
|
|
2127
|
+
state: BYPASS_LABEL_STATE.notMeasured,
|
|
2128
|
+
detail: null,
|
|
2129
|
+
},
|
|
2007
2130
|
};
|
|
2008
2131
|
return Object.freeze(
|
|
2009
2132
|
findings.map(finding => {
|
|
@@ -2119,12 +2242,47 @@ export function planIssueActions(findings, openIssues, context) {
|
|
|
2119
2242
|
);
|
|
2120
2243
|
}
|
|
2121
2244
|
|
|
2245
|
+
/**
|
|
2246
|
+
* The §10.9 line in the nightly job summary — the surface that runs whether or
|
|
2247
|
+
* not anybody needs the escape hatch yet.
|
|
2248
|
+
*
|
|
2249
|
+
* This is the whole point of putting the measurement on the REPORTING half. The
|
|
2250
|
+
* gate half only speaks when somebody is already blocked, which is the moment
|
|
2251
|
+
* it is too late to discover that the documented way out was never installed.
|
|
2252
|
+
* This job runs every night, green or red, and writes here every time.
|
|
2253
|
+
*
|
|
2254
|
+
* Silent on `present` and on `not_measured`, so a healthy repository's summary
|
|
2255
|
+
* is byte-identical to the one it produced before this shipped.
|
|
2256
|
+
*
|
|
2257
|
+
* @param {{branch: string, bypassLabel?: string, requiredness?: object,
|
|
2258
|
+
* bypassLabelState?: {state: string, detail: string|null}}} context - Reporting context
|
|
2259
|
+
* @returns {ReadonlyArray<string>} Markdown lines, possibly none
|
|
2260
|
+
*/
|
|
2261
|
+
function bypassLabelSummary(context) {
|
|
2262
|
+
const label = context.bypassLabel ?? DEFAULT_BYPASS_LABEL;
|
|
2263
|
+
const measured = context.bypassLabelState;
|
|
2264
|
+
const state = measured?.state ?? BYPASS_LABEL_STATE.notMeasured;
|
|
2265
|
+
if (state === BYPASS_LABEL_STATE.absent) {
|
|
2266
|
+
return [
|
|
2267
|
+
`⛔ **Defect — this gate is armed with no escape hatch.** \`${label}\` does not exist in this repository, while the gate is required by ${describeRulesets(context.requiredness?.rulesets)}. The audited waiver §6 documents cannot be applied, so the only route past a red suite is an unaudited admin merge. Create the label once: \`gh label create ${label}\`.`,
|
|
2268
|
+
"",
|
|
2269
|
+
];
|
|
2270
|
+
}
|
|
2271
|
+
if (state === BYPASS_LABEL_STATE.unknown) {
|
|
2272
|
+
return [
|
|
2273
|
+
`⚠️ Whether the \`${label}\` waiver label exists could not be read${measured?.detail ? ` (${measured.detail})` : ""}. Unreadable is not confirmation that it is there.`,
|
|
2274
|
+
"",
|
|
2275
|
+
];
|
|
2276
|
+
}
|
|
2277
|
+
return [];
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2122
2280
|
/**
|
|
2123
2281
|
* Renders the reporting outcome for the job log and summary.
|
|
2124
2282
|
*
|
|
2125
2283
|
* @param {ReadonlyArray<object>} results - Output of `applyIssuePlan`
|
|
2126
|
-
* @param {{branch: string, requiredness?: object, gateContext?: string
|
|
2127
|
-
* Reporting context
|
|
2284
|
+
* @param {{branch: string, requiredness?: object, gateContext?: string,
|
|
2285
|
+
* bypassLabel?: string, bypassLabelState?: object}} context - Reporting context
|
|
2128
2286
|
* @returns {string} Markdown
|
|
2129
2287
|
*/
|
|
2130
2288
|
export function formatIssueReport(results, context) {
|
|
@@ -2150,6 +2308,7 @@ export function formatIssueReport(results, context) {
|
|
|
2150
2308
|
"",
|
|
2151
2309
|
requirednessLine,
|
|
2152
2310
|
"",
|
|
2311
|
+
...bypassLabelSummary(context),
|
|
2153
2312
|
];
|
|
2154
2313
|
for (const result of results) {
|
|
2155
2314
|
const where = result.issues.length
|
|
@@ -2387,7 +2546,12 @@ export async function fetchAllJobs(api, runId, wait) {
|
|
|
2387
2546
|
*/
|
|
2388
2547
|
export async function fetchRequiredness(api, branch, gateContext, wait) {
|
|
2389
2548
|
const unknown = detail =>
|
|
2390
|
-
Object.freeze({
|
|
2549
|
+
Object.freeze({
|
|
2550
|
+
state: REQUIREDNESS.unknown,
|
|
2551
|
+
detail,
|
|
2552
|
+
contexts: [],
|
|
2553
|
+
rulesets: [],
|
|
2554
|
+
});
|
|
2391
2555
|
// No context, no measurement. `resolveSettings` can no longer produce an
|
|
2392
2556
|
// empty one, but this is the place where an empty one does its damage, and a
|
|
2393
2557
|
// guard belongs where the harm lands as well as where it originates: matching
|
|
@@ -2423,9 +2587,11 @@ export async function fetchRequiredness(api, branch, gateContext, wait) {
|
|
|
2423
2587
|
"the branch-rules API returned something that is not a list of rules"
|
|
2424
2588
|
);
|
|
2425
2589
|
}
|
|
2590
|
+
const rules = result.body.filter(
|
|
2591
|
+
rule => rule?.type === "required_status_checks"
|
|
2592
|
+
);
|
|
2426
2593
|
const contexts = Object.freeze(
|
|
2427
|
-
|
|
2428
|
-
.filter(rule => rule?.type === "required_status_checks")
|
|
2594
|
+
rules
|
|
2429
2595
|
.flatMap(rule => rule?.parameters?.required_status_checks ?? [])
|
|
2430
2596
|
.map(check => check?.context)
|
|
2431
2597
|
.filter(context => typeof context === "string")
|
|
@@ -2433,6 +2599,33 @@ export async function fetchRequiredness(api, branch, gateContext, wait) {
|
|
|
2433
2599
|
const matched = contexts.filter(context =>
|
|
2434
2600
|
contextMatchesGate(context, gateContext)
|
|
2435
2601
|
);
|
|
2602
|
+
// WHICH ruleset requires it, not merely that something does. A defect report
|
|
2603
|
+
// that names only the label leaves the reader hunting for the rule to read;
|
|
2604
|
+
// §10.9 has to name both halves of the mismatch, and this endpoint already
|
|
2605
|
+
// carries the source on every rule it returns. An org-level ruleset renders
|
|
2606
|
+
// as `Organization` here, which is the case where "check your repository
|
|
2607
|
+
// settings" would send someone to the wrong page entirely.
|
|
2608
|
+
const rulesets = Object.freeze(
|
|
2609
|
+
rules
|
|
2610
|
+
.filter(rule =>
|
|
2611
|
+
(rule?.parameters?.required_status_checks ?? []).some(check =>
|
|
2612
|
+
contextMatchesGate(check?.context, gateContext)
|
|
2613
|
+
)
|
|
2614
|
+
)
|
|
2615
|
+
.map(rule =>
|
|
2616
|
+
Object.freeze({
|
|
2617
|
+
sourceType:
|
|
2618
|
+
typeof rule?.ruleset_source_type === "string"
|
|
2619
|
+
? rule.ruleset_source_type
|
|
2620
|
+
: null,
|
|
2621
|
+
source:
|
|
2622
|
+
typeof rule?.ruleset_source === "string"
|
|
2623
|
+
? rule.ruleset_source
|
|
2624
|
+
: null,
|
|
2625
|
+
id: typeof rule?.ruleset_id === "number" ? rule.ruleset_id : null,
|
|
2626
|
+
})
|
|
2627
|
+
)
|
|
2628
|
+
);
|
|
2436
2629
|
return Object.freeze({
|
|
2437
2630
|
state:
|
|
2438
2631
|
matched.length > 0 ? REQUIREDNESS.required : REQUIREDNESS.notRequired,
|
|
@@ -2441,9 +2634,95 @@ export async function fetchRequiredness(api, branch, gateContext, wait) {
|
|
|
2441
2634
|
? `required as ${matched.map(context => `\`${context}\``).join(", ")}`
|
|
2442
2635
|
: null,
|
|
2443
2636
|
contexts,
|
|
2637
|
+
rulesets,
|
|
2444
2638
|
});
|
|
2445
2639
|
}
|
|
2446
2640
|
|
|
2641
|
+
/**
|
|
2642
|
+
* Renders the ruleset(s) that require this gate, for a defect report.
|
|
2643
|
+
*
|
|
2644
|
+
* @param {ReadonlyArray<{sourceType: string|null, source: string|null, id: number|null}>} rulesets -
|
|
2645
|
+
* Matching rule sources from `fetchRequiredness`
|
|
2646
|
+
* @returns {string} A prose fragment, never empty
|
|
2647
|
+
*/
|
|
2648
|
+
export function describeRulesets(rulesets) {
|
|
2649
|
+
const named = (rulesets ?? [])
|
|
2650
|
+
.map(entry => {
|
|
2651
|
+
const scope = entry?.sourceType
|
|
2652
|
+
? `${entry.sourceType} ruleset`
|
|
2653
|
+
: "ruleset";
|
|
2654
|
+
const name = entry?.source ? ` \`${entry.source}\`` : "";
|
|
2655
|
+
const id = typeof entry?.id === "number" ? ` (id ${entry.id})` : "";
|
|
2656
|
+
return `${scope}${name}${id}`;
|
|
2657
|
+
})
|
|
2658
|
+
.filter(Boolean);
|
|
2659
|
+
// Not "unknown ruleset": the requiredness measurement already proved a rule
|
|
2660
|
+
// is in effect, and the caller only reaches this on `required`. What is
|
|
2661
|
+
// missing is the rule's SOURCE, which older API responses omit.
|
|
2662
|
+
if (named.length === 0) return "an active ruleset on this branch";
|
|
2663
|
+
return named.join(" and ");
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/**
|
|
2667
|
+
* Measures whether the audited bypass label EXISTS in this repository (§10.9).
|
|
2668
|
+
*
|
|
2669
|
+
* REPORTING ONLY, and it cannot fail its caller — same contract as
|
|
2670
|
+
* `fetchRequiredness`, for the same §10.4 reason: a reporter that aborted on an
|
|
2671
|
+
* unreadable labels API would stop filing the issues that tell people the suite
|
|
2672
|
+
* is down.
|
|
2673
|
+
*
|
|
2674
|
+
* ## Why a 404 is `absent` here, when a 404 is `unknown` for the branch rules
|
|
2675
|
+
*
|
|
2676
|
+
* It looks like the same ambiguity and it is not. `apiGet` maps 404 to `null`,
|
|
2677
|
+
* and `/repos/{o}/{r}/rules/branches/{b}` 404s for a repository the token
|
|
2678
|
+
* cannot see — so there, 404 conflates "no rules" with "not allowed to look".
|
|
2679
|
+
* `/repos/{o}/{r}/labels/{name}` 404s for a label that does not exist in a
|
|
2680
|
+
* repository that IS readable, and the only caller acts on this measurement
|
|
2681
|
+
* after `fetchRequiredness` returned `required`, which required a successful
|
|
2682
|
+
* `200` from the same repository. Repository visibility is therefore already
|
|
2683
|
+
* PROVEN by the time `absent` can be rendered; the remaining meaning of a 404
|
|
2684
|
+
* is the label.
|
|
2685
|
+
*
|
|
2686
|
+
* Everything else — a throw out of `apiGet`, a non-2xx that outlived its
|
|
2687
|
+
* retries, a network failure — is `unknown`. Never `present`: the state this
|
|
2688
|
+
* function must not invent is the one that claims an escape hatch exists. It
|
|
2689
|
+
* never returns `not_measured` either — that token belongs to callers who did
|
|
2690
|
+
* not ask, and this function is the asking.
|
|
2691
|
+
*
|
|
2692
|
+
* @param {object} api - API coordinates
|
|
2693
|
+
* @param {string} label - The configured bypass label
|
|
2694
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
2695
|
+
* @returns {Promise<{state: string, detail: string|null}>} The measurement, never a throw
|
|
2696
|
+
*/
|
|
2697
|
+
export async function fetchBypassLabelState(api, label, wait) {
|
|
2698
|
+
const unknown = detail =>
|
|
2699
|
+
Object.freeze({ state: BYPASS_LABEL_STATE.unknown, detail });
|
|
2700
|
+
if (typeof label !== "string" || label.trim().length === 0) {
|
|
2701
|
+
return unknown(
|
|
2702
|
+
"no bypass label is configured, so there is nothing to look for — an unnamed label is not evidence that a waiver route exists"
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
let result;
|
|
2706
|
+
try {
|
|
2707
|
+
result = await apiGet(
|
|
2708
|
+
api,
|
|
2709
|
+
`/repos/${api.repo}/labels/${encodeURIComponent(label)}`,
|
|
2710
|
+
wait
|
|
2711
|
+
);
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
return unknown(
|
|
2714
|
+
`the labels API was unreadable: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
if (result === null) {
|
|
2718
|
+
return Object.freeze({
|
|
2719
|
+
state: BYPASS_LABEL_STATE.absent,
|
|
2720
|
+
detail: `\`GET /repos/${api.repo}/labels/${label}\` returned 404 — no label by that name exists in this repository`,
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
return Object.freeze({ state: BYPASS_LABEL_STATE.present, detail: null });
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2447
2726
|
/**
|
|
2448
2727
|
* The artifact NAMES a run published, paginated to exhaustion.
|
|
2449
2728
|
*
|
|
@@ -3098,7 +3377,7 @@ export function resolveSettings(env) {
|
|
|
3098
3377
|
freshnessHours: limits.freshnessHours,
|
|
3099
3378
|
bootstrapUntil: env.NIGHTLY_BOOTSTRAP_UNTIL || "",
|
|
3100
3379
|
bootstrapMaxDays: limits.bootstrapMaxDays,
|
|
3101
|
-
bypassLabel: env.NIGHTLY_BYPASS_LABEL ||
|
|
3380
|
+
bypassLabel: env.NIGHTLY_BYPASS_LABEL || DEFAULT_BYPASS_LABEL,
|
|
3102
3381
|
bypassMaxHours: limits.bypassMaxHours,
|
|
3103
3382
|
// An ADDITIONAL project rule, never a replacement — see `evaluateBypass`.
|
|
3104
3383
|
extraBypassReasonPattern: env.NIGHTLY_BYPASS_REASON_PATTERN || "",
|
|
@@ -3262,6 +3541,21 @@ export async function runReport(env, wait) {
|
|
|
3262
3541
|
settings.gateContext,
|
|
3263
3542
|
wait
|
|
3264
3543
|
);
|
|
3544
|
+
// Measured only where a waiver recipe is actually printed. On a branch this
|
|
3545
|
+
// gate does not guard, the issue already says "you do not need a waiver" and
|
|
3546
|
+
// never names the label — so a missing label there gates nothing, and
|
|
3547
|
+
// reporting it would be a defect against a repository that has none. Skipping
|
|
3548
|
+
// the CALL rather than suppressing the RENDER is deliberate: it makes
|
|
3549
|
+
// "not reported when the gate is unrequired" a property of the wire, which a
|
|
3550
|
+
// renderer edit cannot quietly undo.
|
|
3551
|
+
const bypassLabelState =
|
|
3552
|
+
requiredness.state === REQUIREDNESS.notRequired
|
|
3553
|
+
? Object.freeze({
|
|
3554
|
+
state: BYPASS_LABEL_STATE.notMeasured,
|
|
3555
|
+
detail:
|
|
3556
|
+
"not measured — this gate is not required on this branch, so no waiver recipe is printed and a missing label waives nothing",
|
|
3557
|
+
})
|
|
3558
|
+
: await fetchBypassLabelState(settings.api, settings.bypassLabel, wait);
|
|
3265
3559
|
const context = {
|
|
3266
3560
|
branch: settings.branch,
|
|
3267
3561
|
label: settings.issueLabel,
|
|
@@ -3269,6 +3563,7 @@ export async function runReport(env, wait) {
|
|
|
3269
3563
|
requiredness,
|
|
3270
3564
|
gateContext: settings.gateContext,
|
|
3271
3565
|
bypassLabel: settings.bypassLabel,
|
|
3566
|
+
bypassLabelState,
|
|
3272
3567
|
pinIssues: settings.pinIssues,
|
|
3273
3568
|
};
|
|
3274
3569
|
const plan = planIssueActions(
|
|
@@ -3279,6 +3574,7 @@ export async function runReport(env, wait) {
|
|
|
3279
3574
|
return {
|
|
3280
3575
|
findings,
|
|
3281
3576
|
requiredness,
|
|
3577
|
+
bypassLabelState,
|
|
3282
3578
|
plan,
|
|
3283
3579
|
results: await applyIssuePlan(settings.api, plan, wait),
|
|
3284
3580
|
settings,
|
|
@@ -3317,6 +3613,8 @@ async function reportIssues(asJson) {
|
|
|
3317
3613
|
branch: settings.branch,
|
|
3318
3614
|
requiredness: machine.requiredness,
|
|
3319
3615
|
gateContext: settings.gateContext,
|
|
3616
|
+
bypassLabel: settings.bypassLabel,
|
|
3617
|
+
bypassLabelState: machine.bypassLabelState,
|
|
3320
3618
|
});
|
|
3321
3619
|
process.stdout.write(report);
|
|
3322
3620
|
await appendSummary(report);
|
|
@@ -3329,6 +3627,19 @@ async function reportIssues(asJson) {
|
|
|
3329
3627
|
`::warning title=Nightly E2E requiredness unknown::Could not read \`${settings.branch}\`'s branch rules, so the tracking issues claim neither that merges are blocked nor that they are not. ${machine.requiredness.detail ?? ""}\n`
|
|
3330
3628
|
);
|
|
3331
3629
|
}
|
|
3630
|
+
// §10.9. An annotation rather than a failure, and that is not timidity: this
|
|
3631
|
+
// job's status answers "did REPORTING work" (§10.4), and reddening it for a
|
|
3632
|
+
// repository-configuration defect would teach operators to ignore the one job
|
|
3633
|
+
// that tells them a suite is down. It is loud where loudness is free.
|
|
3634
|
+
if (machine.bypassLabelState?.state === BYPASS_LABEL_STATE.absent) {
|
|
3635
|
+
process.stderr.write(
|
|
3636
|
+
`::error title=Nightly E2E bypass label missing::The gate is required on \`${settings.branch}\` but the \`${settings.bypassLabel}\` label does not exist in this repository, so the audited waiver cannot be applied and an unaudited admin merge is the only exit. Create it once: gh label create ${settings.bypassLabel}\n`
|
|
3637
|
+
);
|
|
3638
|
+
} else if (machine.bypassLabelState?.state === BYPASS_LABEL_STATE.unknown) {
|
|
3639
|
+
process.stderr.write(
|
|
3640
|
+
`::warning title=Nightly E2E bypass label unreadable::Could not read whether \`${settings.bypassLabel}\` exists in this repository. ${machine.bypassLabelState.detail ?? ""}\n`
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3332
3643
|
for (const result of machine.results) {
|
|
3333
3644
|
for (const warning of result.warnings ?? []) {
|
|
3334
3645
|
process.stderr.write(
|