@codyswann/lisa 4.6.4 → 4.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/doctor-skip-jobs-migration.d.ts.map +1 -1
- package/dist/cli/doctor-skip-jobs-migration.js +119 -13
- package/dist/cli/doctor-skip-jobs-migration.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +1 -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 +2 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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-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/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +167 -27
|
@@ -67,6 +67,18 @@
|
|
|
67
67
|
* expiry timestamp on screen. Bootstrap forgives absence of evidence, never
|
|
68
68
|
* evidence of failure — a red run is red inside the window too.
|
|
69
69
|
*
|
|
70
|
+
* ## The bypass reads the pull request LIVE (contract 1.6.0, row 40)
|
|
71
|
+
*
|
|
72
|
+
* `github.event.pull_request` is a SNAPSHOT taken when the run was triggered,
|
|
73
|
+
* and a re-run replays it verbatim. Reading the bypass label and the
|
|
74
|
+
* `Nightly-E2E-Bypass:` trailer out of it made the gate's own printed remedy
|
|
75
|
+
* impossible to follow: applying the label fired no run, and re-running — the
|
|
76
|
+
* obvious next move — replayed a payload from before the label existed. The
|
|
77
|
+
* waiver was real, correctly recorded, and invisible to the gate that asked for
|
|
78
|
+
* it. `fetchPullRequestState` reads both halves from the API at gate time
|
|
79
|
+
* instead, which also means a label somebody REMOVED stops waiving. An
|
|
80
|
+
* unreadable pull request is a REJECTED bypass, never a granted one.
|
|
81
|
+
*
|
|
70
82
|
* ## Inherited from three implementations, with one path closed
|
|
71
83
|
*
|
|
72
84
|
* `DECISIVE_CONCLUSIONS` comes from acmeorgb's `check-nightly-e2e.mjs` and is
|
|
@@ -89,7 +101,7 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
|
89
101
|
* rather than running a contract neither half agrees on. See §8 of
|
|
90
102
|
* `docs/nightly-e2e-gate.md` for what counts as major / minor / patch.
|
|
91
103
|
*/
|
|
92
|
-
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.
|
|
104
|
+
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.6.0";
|
|
93
105
|
|
|
94
106
|
/**
|
|
95
107
|
* The conclusions that constitute a verdict about the code.
|
|
@@ -1381,6 +1393,8 @@ const BYPASS_REJECTIONS = Object.freeze({
|
|
|
1381
1393
|
"it was applied longer ago than `bypass_max_hours` allows. Bypasses auto-expire so a label nobody removes cannot become a permanent hole.",
|
|
1382
1394
|
no_reason_or_ticket:
|
|
1383
1395
|
"the PR body carries no `Nightly-E2E-Bypass: <TICKET> <reason>` line. A bypass without a reason and a ticket is not auditable.",
|
|
1396
|
+
pr_state_unreadable:
|
|
1397
|
+
"this pull request's live labels and body could not be read, so whether a waiver was requested is UNKNOWN. The gate stays closed: a bypass that fires when it could not read the request is worse than no bypass. On a private repository the caller job needs `pull-requests: read`; otherwise this is a transient API failure and a re-run resolves it.",
|
|
1384
1398
|
});
|
|
1385
1399
|
|
|
1386
1400
|
// ---------------------------------------------------------------------------
|
|
@@ -1591,8 +1605,17 @@ export function formatReport(verdict, context) {
|
|
|
1591
1605
|
);
|
|
1592
1606
|
} else if (verdict.blocked) {
|
|
1593
1607
|
if (verdict.bypass && !verdict.bypass.valid) {
|
|
1608
|
+
// `pr_state_unreadable` gets its own lead because the usual one asserts
|
|
1609
|
+
// something this branch cannot know. When the live read failed, whether a
|
|
1610
|
+
// label is present is precisely the unanswered question — saying "a label
|
|
1611
|
+
// is present but was REJECTED" would send the reader to remove a label
|
|
1612
|
+
// that may not exist.
|
|
1613
|
+
const lead =
|
|
1614
|
+
verdict.bypass.reason === "pr_state_unreadable"
|
|
1615
|
+
? "⛔ **The bypass could not be evaluated**"
|
|
1616
|
+
: `⛔ **A \`${context.bypassLabel}\` label is present but was REJECTED**`;
|
|
1594
1617
|
lines.push(
|
|
1595
|
-
|
|
1618
|
+
`${lead} — ${BYPASS_REJECTIONS[verdict.bypass.reason] ?? verdict.bypass.reason}`,
|
|
1596
1619
|
""
|
|
1597
1620
|
);
|
|
1598
1621
|
}
|
|
@@ -2829,6 +2852,94 @@ export async function applyIssuePlan(api, plan, wait) {
|
|
|
2829
2852
|
return Object.freeze(results);
|
|
2830
2853
|
}
|
|
2831
2854
|
|
|
2855
|
+
/**
|
|
2856
|
+
* Reads the pull request's CURRENT labels, body and author, LIVE from the API.
|
|
2857
|
+
*
|
|
2858
|
+
* THIS IS THE BYPASS'S ONLY SOURCE OF TRUTH, and the reason it exists is a
|
|
2859
|
+
* measured defect. The gate used to read both halves of the bypass request from
|
|
2860
|
+
* `github.event.pull_request` — `toJSON(...labels.*.name)` and `...body`. That
|
|
2861
|
+
* object is the event payload CAPTURED WHEN THE RUN WAS TRIGGERED. It is a
|
|
2862
|
+
* snapshot, not a live read, and a re-run REPLAYS the original payload. So the
|
|
2863
|
+
* gate's own printed remedy — "add the trailer, then apply the label" — could
|
|
2864
|
+
* not work: applying a label fires no run under the default activity types, and
|
|
2865
|
+
* re-running (the obvious next move, and the one the failure message invites)
|
|
2866
|
+
* replays a payload from before the label existed. Measured on two consumer
|
|
2867
|
+
* repositories: the label sat on the pull request the whole time while the job
|
|
2868
|
+
* logged `NIGHTLY_PR_LABELS: []`. The only thing that worked was an empty
|
|
2869
|
+
* commit, to manufacture a `synchronize` whose payload happened to carry the
|
|
2870
|
+
* label — a workaround nobody should have to discover.
|
|
2871
|
+
*
|
|
2872
|
+
* A live read is immune to both halves of that: it is correct on a re-run, and
|
|
2873
|
+
* correct whether or not the caller subscribed to the `labeled` activity type.
|
|
2874
|
+
*
|
|
2875
|
+
* It also fixes the mirror-image hole, which matters more: a payload that still
|
|
2876
|
+
* carries a label somebody has since REMOVED would waive a gate whose waiver was
|
|
2877
|
+
* withdrawn. Reading live is the only way "the label is gone" can reach the
|
|
2878
|
+
* gate at all.
|
|
2879
|
+
*
|
|
2880
|
+
* FAILS CLOSED, and never falls back to the payload. `null` means the pull
|
|
2881
|
+
* request could not be read — 404, an unreadable API, a body with no `labels`
|
|
2882
|
+
* array — and the caller turns that into a REJECTED bypass, never into a
|
|
2883
|
+
* bypass. Falling back to the payload here would reintroduce exactly the
|
|
2884
|
+
* stale-label waiver above, on the one path where nobody is watching. Note the
|
|
2885
|
+
* distinction the caller depends on: `null` is "unreadable", while a readable
|
|
2886
|
+
* pull request carrying no labels is `labels: []` — both stay gated, for
|
|
2887
|
+
* reasons the report states differently.
|
|
2888
|
+
*
|
|
2889
|
+
* @param {object} api - API coordinates
|
|
2890
|
+
* @param {number} prNumber - Pull request number
|
|
2891
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
2892
|
+
* @returns {Promise<{labels: ReadonlyArray<string>, body: string, author: string|null}|null>} Live PR state, or `null` when unreadable
|
|
2893
|
+
*/
|
|
2894
|
+
export async function fetchPullRequestState(api, prNumber, wait) {
|
|
2895
|
+
const result = await apiGet(
|
|
2896
|
+
api,
|
|
2897
|
+
`/repos/${api.repo}/pulls/${prNumber}`,
|
|
2898
|
+
wait
|
|
2899
|
+
).catch(() => null);
|
|
2900
|
+
const pr = result?.body;
|
|
2901
|
+
// A missing `labels` array is UNREADABLE, not "no labels". The two must not
|
|
2902
|
+
// collapse: one is a broken read and the other is a fact about the PR.
|
|
2903
|
+
if (!pr || !Array.isArray(pr.labels)) return null;
|
|
2904
|
+
const labels = pr.labels
|
|
2905
|
+
.map(entry => (typeof entry === "string" ? entry : entry?.name))
|
|
2906
|
+
.filter(name => typeof name === "string" && name.length > 0);
|
|
2907
|
+
return Object.freeze({
|
|
2908
|
+
labels: Object.freeze(labels),
|
|
2909
|
+
body: typeof pr.body === "string" ? pr.body : "",
|
|
2910
|
+
author: typeof pr.user?.login === "string" ? pr.user.login : null,
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
/**
|
|
2915
|
+
* The emitted decision for "the pull request could not be read".
|
|
2916
|
+
*
|
|
2917
|
+
* Same keys as every `evaluateBypass` outcome, so `audit_json` has one shape
|
|
2918
|
+
* whatever happened — §6's emitted record is asserted key for key.
|
|
2919
|
+
*
|
|
2920
|
+
* @param {{prNumber: number|null, label: string|null, prAuthor: string|null}} subject - What was being evaluated
|
|
2921
|
+
* @returns {{valid: boolean, reason: string}} A rejected bypass
|
|
2922
|
+
*/
|
|
2923
|
+
export function unreadablePullRequestBypass({
|
|
2924
|
+
prNumber = null,
|
|
2925
|
+
label = null,
|
|
2926
|
+
prAuthor = null,
|
|
2927
|
+
}) {
|
|
2928
|
+
return Object.freeze({
|
|
2929
|
+
label,
|
|
2930
|
+
prAuthor,
|
|
2931
|
+
prNumber,
|
|
2932
|
+
actorPermission: null,
|
|
2933
|
+
valid: false,
|
|
2934
|
+
reason: "pr_state_unreadable",
|
|
2935
|
+
actor: null,
|
|
2936
|
+
appliedAt: null,
|
|
2937
|
+
expiresAt: null,
|
|
2938
|
+
ticket: null,
|
|
2939
|
+
detail: null,
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2832
2943
|
/**
|
|
2833
2944
|
* Reads who most recently applied the bypass label, from the PR's issue events.
|
|
2834
2945
|
*
|
|
@@ -2992,18 +3103,26 @@ export function resolveSettings(env) {
|
|
|
2992
3103
|
// An ADDITIONAL project rule, never a replacement — see `evaluateBypass`.
|
|
2993
3104
|
extraBypassReasonPattern: env.NIGHTLY_BYPASS_REASON_PATTERN || "",
|
|
2994
3105
|
clamped,
|
|
3106
|
+
// The ONLY thing taken from the event payload is the pull request NUMBER,
|
|
3107
|
+
// and only because a PR's number is immutable — it is an address, not a
|
|
3108
|
+
// fact about the PR. Everything the bypass decides on (labels, body, and
|
|
3109
|
+
// the author carried onto the audit) is read LIVE by
|
|
3110
|
+
// `fetchPullRequestState`, because `github.event` is a snapshot from
|
|
3111
|
+
// trigger time that a re-run replays verbatim.
|
|
3112
|
+
//
|
|
3113
|
+
// `NIGHTLY_PR_LABELS` and `NIGHTLY_PR_BODY` are DELIBERATELY NOT READ from
|
|
3114
|
+
// here as of contract 1.6.0. The reusable workflow still sets them, on
|
|
3115
|
+
// purpose: a consumer that has taken the newer workflow ref but not yet run
|
|
3116
|
+
// `lisa apply` is still running a pre-1.6.0 guard, and that guard needs
|
|
3117
|
+
// them. Reading them here would defeat the fix — the payload is exactly
|
|
3118
|
+
// what was wrong.
|
|
3119
|
+
//
|
|
3120
|
+
// `payloadAuthor` is a fallback for the audit record only. It never gates
|
|
3121
|
+
// anything (see `evaluateBypass`: `prAuthor` is recorded, never used to
|
|
3122
|
+
// reject), so a stale value cannot change a verdict.
|
|
2995
3123
|
pr: {
|
|
2996
3124
|
number: Number(env.NIGHTLY_PR_NUMBER) || null,
|
|
2997
|
-
|
|
2998
|
-
body: env.NIGHTLY_PR_BODY || "",
|
|
2999
|
-
labels: (() => {
|
|
3000
|
-
try {
|
|
3001
|
-
const parsed = JSON.parse(env.NIGHTLY_PR_LABELS || "[]");
|
|
3002
|
-
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
3003
|
-
} catch {
|
|
3004
|
-
return [];
|
|
3005
|
-
}
|
|
3006
|
-
})(),
|
|
3125
|
+
payloadAuthor: env.NIGHTLY_PR_AUTHOR || null,
|
|
3007
3126
|
},
|
|
3008
3127
|
};
|
|
3009
3128
|
}
|
|
@@ -3044,28 +3163,49 @@ export async function runGate(env, wait) {
|
|
|
3044
3163
|
return grace.firstSeen === null ? finding : { ...finding, grace };
|
|
3045
3164
|
});
|
|
3046
3165
|
|
|
3166
|
+
// The bypass reads the pull request LIVE and never consults `github.event`.
|
|
3167
|
+
// Three outcomes, and the middle one is the vacuity guard: unreadable is a
|
|
3168
|
+
// REJECTED bypass, never an absent one and never a granted one.
|
|
3047
3169
|
let bypass = null;
|
|
3048
|
-
if (settings.pr.number
|
|
3049
|
-
const
|
|
3170
|
+
if (settings.pr.number) {
|
|
3171
|
+
const live = await fetchPullRequestState(
|
|
3050
3172
|
settings.api,
|
|
3051
3173
|
settings.pr.number,
|
|
3052
|
-
settings.bypassLabel,
|
|
3053
3174
|
wait
|
|
3054
3175
|
);
|
|
3055
|
-
const
|
|
3056
|
-
? await fetchActorPermission(settings.api, labelEvent.actor, wait)
|
|
3057
|
-
: null;
|
|
3058
|
-
bypass = evaluateBypass({
|
|
3059
|
-
labelEvent,
|
|
3060
|
-
prAuthor: settings.pr.author,
|
|
3061
|
-
prBody: settings.pr.body,
|
|
3062
|
-
actorPermission,
|
|
3176
|
+
const subject = {
|
|
3063
3177
|
prNumber: settings.pr.number,
|
|
3064
3178
|
label: settings.bypassLabel,
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3179
|
+
prAuthor: settings.pr.payloadAuthor,
|
|
3180
|
+
};
|
|
3181
|
+
if (live === null) {
|
|
3182
|
+
bypass = unreadablePullRequestBypass(subject);
|
|
3183
|
+
} else if (live.labels.includes(settings.bypassLabel)) {
|
|
3184
|
+
const labelEvent = await fetchLabelEvent(
|
|
3185
|
+
settings.api,
|
|
3186
|
+
settings.pr.number,
|
|
3187
|
+
settings.bypassLabel,
|
|
3188
|
+
wait
|
|
3189
|
+
);
|
|
3190
|
+
const actorPermission = labelEvent
|
|
3191
|
+
? await fetchActorPermission(settings.api, labelEvent.actor, wait)
|
|
3192
|
+
: null;
|
|
3193
|
+
bypass = evaluateBypass({
|
|
3194
|
+
labelEvent,
|
|
3195
|
+
prAuthor: live.author ?? settings.pr.payloadAuthor,
|
|
3196
|
+
prBody: live.body,
|
|
3197
|
+
actorPermission,
|
|
3198
|
+
prNumber: settings.pr.number,
|
|
3199
|
+
label: settings.bypassLabel,
|
|
3200
|
+
maxHours: settings.bypassMaxHours,
|
|
3201
|
+
extraReasonPattern: settings.extraBypassReasonPattern,
|
|
3202
|
+
now,
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
// The remaining case — read successfully, label not present — leaves
|
|
3206
|
+
// `bypass` null. Nobody asked for a waiver, so there is nothing to report,
|
|
3207
|
+
// and a label REMOVED since the run was triggered lands here rather than
|
|
3208
|
+
// waiving anything.
|
|
3069
3209
|
}
|
|
3070
3210
|
|
|
3071
3211
|
return {
|