@codyswann/lisa 4.26.1 → 4.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +64 -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.d.ts +5 -0
- package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +8 -1
- 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 +6 -5
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +64 -1
- package/expo/create-only/.github/workflows/nightly-e2e-report.yml +21 -6
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +64 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +64 -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-copilot/hooks/parity-safety-net.sh +64 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +64 -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/plugins/src/base/hooks/parity-safety-net.sh +64 -1
- package/scripts/generate-nightly-e2e-guard-certificate.mjs +17 -1
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +238 -44
|
@@ -28,9 +28,24 @@
|
|
|
28
28
|
* it stays red, closed when a complete green run lands.
|
|
29
29
|
*
|
|
30
30
|
* They are separate because filing is reporting, not verdict. Issue writes live
|
|
31
|
-
* behind `apiWrite`, reachable only from `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* behind `apiWrite`, reachable only from `applyIssuePlan`, which only
|
|
32
|
+
* `--report-issues` reaches; the gate path cannot reach them, and the gate's
|
|
33
|
+
* reusable workflow requests no `issues:` scope. An Issues API that is down must
|
|
34
|
+
* never be able to redden a required check.
|
|
35
|
+
*
|
|
36
|
+
* ## `--report-issues` publishes BEST EFFORT, and the summary is the report
|
|
37
|
+
*
|
|
38
|
+
* The tracking issue is a convenience; the verdict is the product. So the
|
|
39
|
+
* reporting half resolves the verdict, writes it to `$GITHUB_STEP_SUMMARY`
|
|
40
|
+
* FIRST, and only then attempts to publish — with the publish's outcome
|
|
41
|
+
* absorbed into a `::warning::` that cannot change the job's result. A report
|
|
42
|
+
* channel must not die because its optional publishing destination is
|
|
43
|
+
* unavailable, which is precisely what a repository that switched GitHub Issues
|
|
44
|
+
* off measured: three attempts at `POST /repos/{o}/{r}/issues`, HTTP 410, exit 1
|
|
45
|
+
* — a dead publisher reported as a dead reporter (§10.4).
|
|
46
|
+
*
|
|
47
|
+
* Its exit code therefore answers "is the suite green", and nothing else. See
|
|
48
|
+
* §10.4 for the full three-outcome contract.
|
|
34
49
|
*
|
|
35
50
|
* Zero dependencies and no install step, on purpose: a gate that sits on every
|
|
36
51
|
* pull request has to be cheap enough to stay uncontroversial, and a gate that
|
|
@@ -154,7 +169,7 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
|
154
169
|
* rather than running a contract neither half agrees on. See §8 of
|
|
155
170
|
* `docs/nightly-e2e-gate.md` for what counts as major / minor / patch.
|
|
156
171
|
*/
|
|
157
|
-
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.
|
|
172
|
+
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.9.0";
|
|
158
173
|
|
|
159
174
|
/**
|
|
160
175
|
* The conclusions that constitute a verdict about the code.
|
|
@@ -2476,11 +2491,42 @@ function bypassLabelSummary(context) {
|
|
|
2476
2491
|
}
|
|
2477
2492
|
|
|
2478
2493
|
/**
|
|
2479
|
-
* Renders the
|
|
2494
|
+
* Renders the VERDICT — which suites are green and which are not.
|
|
2495
|
+
*
|
|
2496
|
+
* Written to the job summary before any publishing is attempted (§10.4), which
|
|
2497
|
+
* is the whole reason it is a separate renderer from `formatIssueReport`: this
|
|
2498
|
+
* one describes the suites and needs nothing from the Issues API, so it survives
|
|
2499
|
+
* an Issues API that is switched off, throttled or forbidden.
|
|
2500
|
+
*
|
|
2501
|
+
* @param {ReadonlyArray<object>} findings - Output of `assessSuite`, per suite
|
|
2502
|
+
* @param {{branch: string}} context - Reporting context
|
|
2503
|
+
* @returns {string} Markdown
|
|
2504
|
+
*/
|
|
2505
|
+
export function formatVerdictReport(findings, context) {
|
|
2506
|
+
const red = findings.filter(finding => finding.state === SUITE_STATES.fail);
|
|
2507
|
+
const lines = [
|
|
2508
|
+
red.length > 0
|
|
2509
|
+
? `## 🔴 Nightly E2E verdict — ${red.length} suite(s) not green`
|
|
2510
|
+
: "## 🌙 Nightly E2E verdict",
|
|
2511
|
+
"",
|
|
2512
|
+
`Branch: \`${context.branch}\``,
|
|
2513
|
+
"",
|
|
2514
|
+
...findings.map(finding => `- ${formatFinding(finding)}`),
|
|
2515
|
+
"",
|
|
2516
|
+
// Said here, on the surface the verdict lands on, rather than only in the
|
|
2517
|
+
// contract: the next reader of a run whose publish warned needs to know
|
|
2518
|
+
// that they are already looking at the report.
|
|
2519
|
+
"**This section is the report.** Publishing it to a GitHub tracking issue is a best-effort side-effect recorded below; a publish that fails leaves this verdict untouched and does not change this job's result.",
|
|
2520
|
+
"",
|
|
2521
|
+
];
|
|
2522
|
+
return `${lines.join("\n")}\n`;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/**
|
|
2526
|
+
* The publishing half of the job summary — what happened to the issues.
|
|
2480
2527
|
*
|
|
2481
2528
|
* @param {ReadonlyArray<object>} results - Output of `applyIssuePlan`
|
|
2482
|
-
* @param {{branch
|
|
2483
|
-
* bypassLabel?: string, bypassLabelState?: object}} context - Reporting context
|
|
2529
|
+
* @param {object} context - `{branch, requiredness, gateContext, bypassLabel, bypassLabelState, publishFailure}`
|
|
2484
2530
|
* @returns {string} Markdown
|
|
2485
2531
|
*/
|
|
2486
2532
|
export function formatIssueReport(results, context) {
|
|
@@ -2508,6 +2554,17 @@ export function formatIssueReport(results, context) {
|
|
|
2508
2554
|
"",
|
|
2509
2555
|
...bypassLabelSummary(context),
|
|
2510
2556
|
];
|
|
2557
|
+
// The publish died outright rather than per-suite. Said on the summary as
|
|
2558
|
+
// well as in the annotation, because the annotation scrolls off a busy run
|
|
2559
|
+
// page and this section is where a reader goes looking for it.
|
|
2560
|
+
if (context.publishFailure) {
|
|
2561
|
+
lines.push(
|
|
2562
|
+
`- ⚠️ **Nothing was published** — ${context.publishFailure}`,
|
|
2563
|
+
"",
|
|
2564
|
+
"The verdict above is unaffected: it was written before publishing was attempted, and this job's result reflects the SUITE, not the publish.",
|
|
2565
|
+
""
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2511
2568
|
for (const result of results) {
|
|
2512
2569
|
const where = result.issues.length
|
|
2513
2570
|
? ` (#${result.issues.join(", #")})`
|
|
@@ -2526,7 +2583,7 @@ export function formatIssueReport(results, context) {
|
|
|
2526
2583
|
}
|
|
2527
2584
|
lines.push(
|
|
2528
2585
|
"",
|
|
2529
|
-
"This job REPORTS; it does not gate. Nothing here can block a pull request — the merge gate is a separate workflow that never writes.",
|
|
2586
|
+
"This job REPORTS; it does not gate. Nothing here can block a pull request — the merge gate is a separate workflow that never writes. This job's own result answers whether the SUITE is green, never whether publishing worked: a tracking issue that could not be written is a ⚠️ above, not a failure.",
|
|
2530
2587
|
""
|
|
2531
2588
|
);
|
|
2532
2589
|
return `${lines.join("\n")}\n`;
|
|
@@ -3794,7 +3851,7 @@ export async function runGate(env, wait) {
|
|
|
3794
3851
|
}
|
|
3795
3852
|
|
|
3796
3853
|
/**
|
|
3797
|
-
*
|
|
3854
|
+
* The READ half of reporting: everything up to, and not including, the writes.
|
|
3798
3855
|
*
|
|
3799
3856
|
* Reads exactly what the gate reads and assesses it with exactly the same
|
|
3800
3857
|
* classifier, so the issue and the merge gate can never disagree about whether a
|
|
@@ -3807,11 +3864,15 @@ export async function runGate(env, wait) {
|
|
|
3807
3864
|
* ONE pull request, it does not make the nightly green, and the tracking issue
|
|
3808
3865
|
* stays open until a green run lands.
|
|
3809
3866
|
*
|
|
3867
|
+
* Split from the writes so the CLI can surface the verdict BEFORE publishing it
|
|
3868
|
+
* (§10.4). Everything here is a `GET`; the first write in the reporting path is
|
|
3869
|
+
* `applyIssuePlan`.
|
|
3870
|
+
*
|
|
3810
3871
|
* @param {NodeJS.ProcessEnv} env - The environment
|
|
3811
3872
|
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
3812
|
-
* @returns {Promise<object>} Findings,
|
|
3873
|
+
* @returns {Promise<object>} Findings, requiredness, label state, plan, settings
|
|
3813
3874
|
*/
|
|
3814
|
-
export async function
|
|
3875
|
+
export async function planReport(env, wait) {
|
|
3815
3876
|
const settings = resolveSettings(env);
|
|
3816
3877
|
const now = new Date();
|
|
3817
3878
|
const observations = await observe(
|
|
@@ -3867,36 +3928,99 @@ export async function runReport(env, wait) {
|
|
|
3867
3928
|
bypassLabelState,
|
|
3868
3929
|
pinIssues: settings.pinIssues,
|
|
3869
3930
|
};
|
|
3931
|
+
return { findings, requiredness, bypassLabelState, context, settings };
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/**
|
|
3935
|
+
* The PUBLISH half: read the open tracking issues, plan, and write.
|
|
3936
|
+
*
|
|
3937
|
+
* The read of the existing issues lives on THIS side of the split, not with the
|
|
3938
|
+
* verdict, and that placement is the point. Listing open issues is the Issues
|
|
3939
|
+
* API — the same API that can be switched off, throttled or forbidden — so a
|
|
3940
|
+
* verdict that depended on it would still die with its publisher, which is the
|
|
3941
|
+
* whole defect (§10.4). Everything the verdict needs comes from Actions run
|
|
3942
|
+
* history instead.
|
|
3943
|
+
*
|
|
3944
|
+
* @param {object} planned - Output of `planReport`
|
|
3945
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
3946
|
+
* @returns {Promise<{plan: ReadonlyArray<object>, results: ReadonlyArray<object>}>} Plan and outcomes
|
|
3947
|
+
*/
|
|
3948
|
+
export async function publishReport(planned, wait) {
|
|
3949
|
+
const { settings, findings, context } = planned;
|
|
3870
3950
|
const plan = planIssueActions(
|
|
3871
3951
|
findings,
|
|
3872
3952
|
await fetchTrackingIssues(settings.api, settings.issueLabel, wait),
|
|
3873
3953
|
context
|
|
3874
3954
|
);
|
|
3875
|
-
return {
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3955
|
+
return { plan, results: await applyIssuePlan(settings.api, plan, wait) };
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
/**
|
|
3959
|
+
* Plan, then publish. The whole report in one call.
|
|
3960
|
+
*
|
|
3961
|
+
* Kept as the single-call entry point it has always been — every existing
|
|
3962
|
+
* caller and every §10 test reads the same five fields back. `reportIssues`
|
|
3963
|
+
* deliberately does NOT use it: the CLI has to get the verdict onto the job
|
|
3964
|
+
* summary between the two halves, and has to survive the second half failing,
|
|
3965
|
+
* neither of which a combined call can express (§10.4).
|
|
3966
|
+
*
|
|
3967
|
+
* @param {Record<string, string|undefined>} env - Process environment
|
|
3968
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
3969
|
+
* @returns {Promise<object>} `{findings, requiredness, bypassLabelState, plan, results, settings}`
|
|
3970
|
+
*/
|
|
3971
|
+
export async function runReport(env, wait) {
|
|
3972
|
+
const planned = await planReport(env, wait);
|
|
3973
|
+
const { context: _context, ...verdict } = planned;
|
|
3974
|
+
return { ...verdict, ...(await publishReport(planned, wait)) };
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* Whether a suite is red in the sense §10.4's exit code needs.
|
|
3979
|
+
*
|
|
3980
|
+
* `fail` AND complete evidence — the same pair `planIssueActions` requires
|
|
3981
|
+
* before it will file anything, asked here of the FINDING so the exit code needs
|
|
3982
|
+
* nothing from the Issues API. A `fail` that row 30 routed to
|
|
3983
|
+
* `evidence_incomplete` decided nothing there and decides nothing here either,
|
|
3984
|
+
* or the job would go red about a run whose own report says it proved nothing.
|
|
3985
|
+
*
|
|
3986
|
+
* @param {object} finding - A finding
|
|
3987
|
+
* @returns {boolean} True when the suite genuinely failed
|
|
3988
|
+
*/
|
|
3989
|
+
function isGenuinelyRed(finding) {
|
|
3990
|
+
return finding.state === SUITE_STATES.fail && isCompleteEvidence(finding);
|
|
3883
3991
|
}
|
|
3884
3992
|
|
|
3885
3993
|
/**
|
|
3886
3994
|
* The reporting entry point, as the scheduled workflow invokes it.
|
|
3887
3995
|
*
|
|
3888
|
-
*
|
|
3889
|
-
*
|
|
3890
|
-
*
|
|
3891
|
-
*
|
|
3996
|
+
* Three outcomes, three deliberately different exit codes (§10.4):
|
|
3997
|
+
*
|
|
3998
|
+
* 1. **No verdict could be resolved** — configuration, a schema refusal, an
|
|
3999
|
+
* unreadable run history → EXIT 1. Nothing was reported, and "we could not
|
|
4000
|
+
* check" must never render as "it is fine".
|
|
4001
|
+
* 2. **Verdict resolved, publishing failed** → EXIT 0 + `::warning::`. The
|
|
4002
|
+
* tracking issue is a convenience; the verdict is the product, and it is
|
|
4003
|
+
* already on the job summary by the time publishing is attempted. A report
|
|
4004
|
+
* channel must not die because its optional publishing destination is
|
|
4005
|
+
* unavailable — measured as HTTP 410 on `POST /repos/{o}/{r}/issues` for a
|
|
4006
|
+
* repository that switched Issues off.
|
|
4007
|
+
* 3. **The SUITE itself is red** → EXIT 1 + `::error::` per red suite.
|
|
4008
|
+
*
|
|
4009
|
+
* Case 3 is why case 2 is safe to absorb. Without it this entry point would have
|
|
4010
|
+
* no failing path left at all once publishing stopped reddening it, and a job
|
|
4011
|
+
* that cannot go red is a job whose green means nothing.
|
|
3892
4012
|
*
|
|
3893
4013
|
* @param {boolean} asJson - Emit the machine record instead of prose
|
|
3894
4014
|
* @returns {Promise<void>} Resolves once the report is written
|
|
3895
4015
|
*/
|
|
3896
|
-
async function reportIssues(asJson) {
|
|
3897
|
-
|
|
4016
|
+
export async function reportIssues(asJson) {
|
|
4017
|
+
// ---------------------------------------------------------------------
|
|
4018
|
+
// 1. Resolve the verdict. Read-only, and the one path that still exits 1
|
|
4019
|
+
// for a reporting failure — because it produced no report at all.
|
|
4020
|
+
// ---------------------------------------------------------------------
|
|
4021
|
+
let planned;
|
|
3898
4022
|
try {
|
|
3899
|
-
|
|
4023
|
+
planned = await planReport(process.env);
|
|
3900
4024
|
} catch (error) {
|
|
3901
4025
|
const message = error instanceof Error ? error.message : String(error);
|
|
3902
4026
|
process.stderr.write(
|
|
@@ -3906,16 +4030,51 @@ async function reportIssues(asJson) {
|
|
|
3906
4030
|
process.exitCode = 1;
|
|
3907
4031
|
return;
|
|
3908
4032
|
}
|
|
3909
|
-
const { settings, ...
|
|
4033
|
+
const { settings, context: _context, ...verdict } = planned;
|
|
4034
|
+
|
|
4035
|
+
// ---------------------------------------------------------------------
|
|
4036
|
+
// 2. Surface the verdict, BEFORE anything can go wrong publishing it.
|
|
4037
|
+
// Making publishing non-fatal on its own would trade a false red for a
|
|
4038
|
+
// silent gap — the same defect pointing the other way. The step summary
|
|
4039
|
+
// is a channel that exists whether or not GitHub Issues do.
|
|
4040
|
+
// ---------------------------------------------------------------------
|
|
4041
|
+
if (!asJson) {
|
|
4042
|
+
const verdictReport = formatVerdictReport(verdict.findings, {
|
|
4043
|
+
branch: settings.branch,
|
|
4044
|
+
});
|
|
4045
|
+
process.stdout.write(verdictReport);
|
|
4046
|
+
await appendSummary(verdictReport);
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
// ---------------------------------------------------------------------
|
|
4050
|
+
// 3. Publish — best effort, and nothing it does reaches the exit code.
|
|
4051
|
+
// ---------------------------------------------------------------------
|
|
4052
|
+
// `applyIssuePlan` already records a per-suite write failure rather than
|
|
4053
|
+
// throwing (row 31). This catch is for everything around it — most concretely
|
|
4054
|
+
// the LIST of open issues, which is the same Issues API the writes use and can
|
|
4055
|
+
// therefore be off, throttled or forbidden in exactly the same way.
|
|
4056
|
+
let plan = [];
|
|
4057
|
+
let results = [];
|
|
4058
|
+
let publishFailure = null;
|
|
4059
|
+
try {
|
|
4060
|
+
({ plan, results } = await publishReport(planned));
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
publishFailure = error instanceof Error ? error.message : String(error);
|
|
4063
|
+
process.stderr.write(
|
|
4064
|
+
`::warning title=Nightly E2E tracking issues not published::${publishFailure.split("\n")[0]} — the verdict for this run is in the job summary, and this job's result reflects the SUITE, not the publish.\n`
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
const machine = { ...verdict, plan, results, publishFailure };
|
|
3910
4068
|
if (asJson) {
|
|
3911
4069
|
process.stdout.write(`${JSON.stringify(machine, null, 2)}\n`);
|
|
3912
4070
|
} else {
|
|
3913
|
-
const report = formatIssueReport(
|
|
4071
|
+
const report = formatIssueReport(results, {
|
|
3914
4072
|
branch: settings.branch,
|
|
3915
|
-
requiredness:
|
|
4073
|
+
requiredness: verdict.requiredness,
|
|
3916
4074
|
gateContext: settings.gateContext,
|
|
3917
4075
|
bypassLabel: settings.bypassLabel,
|
|
3918
|
-
bypassLabelState:
|
|
4076
|
+
bypassLabelState: verdict.bypassLabelState,
|
|
4077
|
+
publishFailure,
|
|
3919
4078
|
});
|
|
3920
4079
|
process.stdout.write(report);
|
|
3921
4080
|
await appendSummary(report);
|
|
@@ -3923,38 +4082,56 @@ async function reportIssues(asJson) {
|
|
|
3923
4082
|
// An `unknown` requiredness is annotated on the run, because it means every
|
|
3924
4083
|
// issue this report touched is deliberately silent about merge consequences —
|
|
3925
4084
|
// and the reason (a scope, a rename, an outage) is fixable.
|
|
3926
|
-
if (
|
|
4085
|
+
if (verdict.requiredness?.state === REQUIREDNESS.unknown) {
|
|
3927
4086
|
process.stderr.write(
|
|
3928
|
-
`::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. ${
|
|
4087
|
+
`::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. ${verdict.requiredness.detail ?? ""}\n`
|
|
3929
4088
|
);
|
|
3930
4089
|
}
|
|
3931
4090
|
// §10.9. An annotation rather than a failure, and that is not timidity: this
|
|
3932
|
-
// job's
|
|
4091
|
+
// job's exit code answers "is the suite green" (§10.4), and reddening it for a
|
|
3933
4092
|
// repository-configuration defect would teach operators to ignore the one job
|
|
3934
4093
|
// that tells them a suite is down. It is loud where loudness is free.
|
|
3935
|
-
if (
|
|
4094
|
+
if (verdict.bypassLabelState?.state === BYPASS_LABEL_STATE.absent) {
|
|
3936
4095
|
process.stderr.write(
|
|
3937
4096
|
`::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`
|
|
3938
4097
|
);
|
|
3939
|
-
} else if (
|
|
4098
|
+
} else if (verdict.bypassLabelState?.state === BYPASS_LABEL_STATE.unknown) {
|
|
3940
4099
|
process.stderr.write(
|
|
3941
|
-
`::warning title=Nightly E2E bypass label unreadable::Could not read whether \`${settings.bypassLabel}\` exists in this repository. ${
|
|
4100
|
+
`::warning title=Nightly E2E bypass label unreadable::Could not read whether \`${settings.bypassLabel}\` exists in this repository. ${verdict.bypassLabelState.detail ?? ""}\n`
|
|
3942
4101
|
);
|
|
3943
4102
|
}
|
|
3944
|
-
for (const result of
|
|
4103
|
+
for (const result of results) {
|
|
3945
4104
|
for (const warning of result.warnings ?? []) {
|
|
3946
4105
|
process.stderr.write(
|
|
3947
4106
|
`::warning title=Nightly E2E tracking issue not pinned::${result.label} — ${warning}\n`
|
|
3948
4107
|
);
|
|
3949
4108
|
}
|
|
3950
4109
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
4110
|
+
// A tracking issue that did not land is a `::warning::`, NOT an `::error::`
|
|
4111
|
+
// and not a failure. It names the suite and the cause, which is what an
|
|
4112
|
+
// operator needs to fix the publisher; the verdict it could not publish is
|
|
4113
|
+
// three paragraphs up the same summary.
|
|
4114
|
+
for (const result of results.filter(entry => !entry.ok)) {
|
|
4115
|
+
process.stderr.write(
|
|
4116
|
+
`::warning title=Nightly E2E tracking issue not updated::${result.label} — ${result.error} (the verdict for this run is in the job summary; this job's result reflects the SUITE, not the publish)\n`
|
|
4117
|
+
);
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
// ---------------------------------------------------------------------
|
|
4121
|
+
// 4. Exit on the SUITE, never on the publish.
|
|
4122
|
+
// ---------------------------------------------------------------------
|
|
4123
|
+
// Read off the FINDINGS, never off the plan or the results: the plan needs the
|
|
4124
|
+
// Issues API to build, and a verdict that could not be reached because the
|
|
4125
|
+
// publisher was unreachable is the defect this whole section deletes.
|
|
4126
|
+
// `::error::` annotates but does not fail a step, so the explicit non-zero
|
|
4127
|
+
// exit code is what actually carries the red.
|
|
4128
|
+
const red = verdict.findings.filter(isGenuinelyRed);
|
|
4129
|
+
for (const finding of red) {
|
|
3953
4130
|
process.stderr.write(
|
|
3954
|
-
`::error title=Nightly E2E
|
|
4131
|
+
`::error title=Nightly E2E is not green::${finding.label} — ${finding.state.toUpperCase()}${finding.conclusion ? ` (${finding.conclusion})` : ""} [${finding.reason}]${finding.url ? ` — ${finding.url}` : ""}\n`
|
|
3955
4132
|
);
|
|
3956
4133
|
}
|
|
3957
|
-
if (
|
|
4134
|
+
if (red.length > 0) process.exitCode = 1;
|
|
3958
4135
|
}
|
|
3959
4136
|
|
|
3960
4137
|
/**
|
|
@@ -4039,13 +4216,30 @@ async function main(argv) {
|
|
|
4039
4216
|
/**
|
|
4040
4217
|
* Appends a report to the job summary when one exists.
|
|
4041
4218
|
*
|
|
4219
|
+
* NEVER throws. The same report has already gone to stdout, and on the gate path
|
|
4220
|
+
* the verdict is additionally in the step outputs and the exit code, so an
|
|
4221
|
+
* unwritable `$GITHUB_STEP_SUMMARY` is a failure of one RENDERING SURFACE and
|
|
4222
|
+
* nothing more. Left to propagate it would reach top-level `main`, where on the
|
|
4223
|
+
* reporting path it would fail the job AND skip publishing — a summary file the
|
|
4224
|
+
* runner could not open taking down both channels at once, which is §10.4's
|
|
4225
|
+
* defect wearing different clothes. It is absorbed into a `::warning::` rather
|
|
4226
|
+
* than swallowed, because a summary that silently never appears is how an
|
|
4227
|
+
* operator learns to stop looking at it.
|
|
4228
|
+
*
|
|
4042
4229
|
* @param {string} report - Markdown
|
|
4043
|
-
* @returns {Promise<void>} Resolves when written
|
|
4230
|
+
* @returns {Promise<void>} Resolves when written, or when it could not be
|
|
4044
4231
|
*/
|
|
4045
4232
|
async function appendSummary(report) {
|
|
4046
4233
|
if (!process.env.GITHUB_STEP_SUMMARY) return;
|
|
4047
|
-
|
|
4048
|
-
|
|
4234
|
+
try {
|
|
4235
|
+
const { appendFileSync } = await import("node:fs");
|
|
4236
|
+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, report);
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4239
|
+
process.stderr.write(
|
|
4240
|
+
`::warning title=Nightly E2E job summary unwritable::Could not append to \`$GITHUB_STEP_SUMMARY\` (${message.split("\n")[0]}). The same report is in this job's log.\n`
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4049
4243
|
}
|
|
4050
4244
|
|
|
4051
4245
|
/**
|