@codyswann/lisa 4.6.7 → 4.6.8
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 +5 -1
- package/dist/cli/doctor-skip-jobs-migration.d.ts.map +1 -1
- package/dist/cli/doctor-skip-jobs-migration.js +120 -74
- package/dist/cli/doctor-skip-jobs-migration.js.map +1 -1
- package/dist/cli/workflow-moments.d.ts +67 -0
- package/dist/cli/workflow-moments.d.ts.map +1 -0
- package/dist/cli/workflow-moments.js +225 -0
- package/dist/cli/workflow-moments.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +2 -0
- 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
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { DoctorCheck } from "./doctor.js";
|
|
2
|
+
import type { MomentContext } from "./workflow-moments.js";
|
|
3
|
+
export { qualityCalls } from "./workflow-moments.js";
|
|
4
|
+
export type { MomentContext, QualityCall } from "./workflow-moments.js";
|
|
2
5
|
/** One token's migration, exactly as the shipped registry resolves it. */
|
|
3
6
|
export interface SkipJobToken {
|
|
4
7
|
/** The token as the caller spells it, including any damaging whitespace. */
|
|
@@ -32,9 +35,10 @@ export declare function skipJobCallers(targetPath: string): Promise<SkipJobCalle
|
|
|
32
35
|
/**
|
|
33
36
|
* The operator-readable line for one token.
|
|
34
37
|
* @param entry - The token's resolved migration
|
|
38
|
+
* @param context - Every job resolving this caller's moment, when known
|
|
35
39
|
* @returns One line naming the edit, or naming why there is none
|
|
36
40
|
*/
|
|
37
|
-
export declare function describeToken(entry: SkipJobToken): string;
|
|
41
|
+
export declare function describeToken(entry: SkipJobToken, context?: MomentContext): string;
|
|
38
42
|
/**
|
|
39
43
|
* Report the gate declarations that replace a caller's `skip_jobs` tokens.
|
|
40
44
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-skip-jobs-migration.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-skip-jobs-migration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor-skip-jobs-migration.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-skip-jobs-migration.ts"],"names":[],"mappings":"AA+DA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAU3D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAQxE,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAgFD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,EAAE,CAAC,CAQ1B;AA8DD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CAiDR;AAqKD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CA8BtB"}
|
|
@@ -37,6 +37,20 @@
|
|
|
37
37
|
* names the exact edit; the edit is made by something that can read the
|
|
38
38
|
* surrounding code and verify afterwards that the same checks still run.
|
|
39
39
|
*
|
|
40
|
+
* ## Why the advice depends on who else resolves the moment
|
|
41
|
+
*
|
|
42
|
+
* A gate declaration is keyed by gate and moment in `.lisa.config.json`. It is
|
|
43
|
+
* not keyed by workflow, so it governs EVERY job resolving that moment. A
|
|
44
|
+
* repository whose `ci.yml` calls `quality.yml` directly and whose `deploy.yml`
|
|
45
|
+
* reaches it through `release.yml` resolves `pull-request` from both — one
|
|
46
|
+
* declaration, two callers. Telling the operator to declare the gate `off` to
|
|
47
|
+
* neutralise a token that exists on only one of them switches the check off on
|
|
48
|
+
* the other too, and the other is the caller that was running it for real. That
|
|
49
|
+
* is a strict downgrade reached by following the advice literally
|
|
50
|
+
* (CodySwannGT/lisa#3100), so this check enumerates every job reaching
|
|
51
|
+
* `quality.yml` — not only the ones passing `skip_jobs` — and withholds the
|
|
52
|
+
* declaration when more than one resolves the token's moment.
|
|
53
|
+
*
|
|
40
54
|
* ## Why it warns and never fails
|
|
41
55
|
*
|
|
42
56
|
* The input still works, and it must keep working until every consumer has
|
|
@@ -44,17 +58,15 @@
|
|
|
44
58
|
* has not yet done a migration Lisa only just made possible.
|
|
45
59
|
* @module cli/doctor-skip-jobs-migration
|
|
46
60
|
*/
|
|
47
|
-
import {
|
|
61
|
+
import { readFile } from "node:fs/promises";
|
|
48
62
|
import * as path from "node:path";
|
|
49
63
|
import { importGateRegistry } from "./gate-registry-source.js";
|
|
64
|
+
import { momentContext, momentFor, qualityCalls, scalar, unpinnedClause, workflowFiles, } from "./workflow-moments.js";
|
|
65
|
+
export { qualityCalls } from "./workflow-moments.js";
|
|
50
66
|
/** Name rendered in the doctor report. */
|
|
51
67
|
const CHECK_NAME = "skip_jobs migrated to gates?";
|
|
52
|
-
/** The moment `quality.yml` resolves gates at when a caller declares none. */
|
|
53
|
-
const DEFAULT_MOMENT = "pull-request";
|
|
54
68
|
/** A caller's `skip_jobs` input, whatever quoting style it uses. */
|
|
55
69
|
const SKIP_JOBS_INPUT = /^([ \t]{0,64})skip_jobs:[ \t]{0,64}(.{0,4096})$/;
|
|
56
|
-
/** A caller's `moment` input, whatever quoting style it uses. */
|
|
57
|
-
const MOMENT_INPUT = /^([ \t]{0,64})moment:[ \t]{0,64}(.{0,4096})$/;
|
|
58
70
|
/**
|
|
59
71
|
* Import the shipped registry.
|
|
60
72
|
*
|
|
@@ -66,60 +78,6 @@ const MOMENT_INPUT = /^([ \t]{0,64})moment:[ \t]{0,64}(.{0,4096})$/;
|
|
|
66
78
|
async function loadRegistry() {
|
|
67
79
|
return importGateRegistry();
|
|
68
80
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Strip YAML quoting and a trailing comment from an inline scalar.
|
|
71
|
-
* @param raw - The text after `key:`
|
|
72
|
-
* @returns The scalar's value
|
|
73
|
-
*/
|
|
74
|
-
function scalar(raw) {
|
|
75
|
-
const withoutComment = /^(['"])(.*?)\1/.exec(raw.trim());
|
|
76
|
-
if (withoutComment)
|
|
77
|
-
return withoutComment[2] ?? "";
|
|
78
|
-
return raw.split("#")[0]?.trim() ?? "";
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* The `moment` declared in the same `with:` block as a `skip_jobs` line.
|
|
82
|
-
*
|
|
83
|
-
* Scoped by indentation rather than taken from anywhere in the file: a
|
|
84
|
-
* workflow may call `quality.yml` twice with different moments, and attributing
|
|
85
|
-
* one job's moment to the other's tokens would produce a declaration that is
|
|
86
|
-
* legal for neither.
|
|
87
|
-
* @param lines - The workflow file's lines
|
|
88
|
-
* @param at - Index of the `skip_jobs` line
|
|
89
|
-
* @param indent - That line's leading whitespace
|
|
90
|
-
* @returns The declared moment, or the workflow's default
|
|
91
|
-
*/
|
|
92
|
-
function momentFor(lines, at, indent) {
|
|
93
|
-
const sameBlock = (index) => {
|
|
94
|
-
const line = lines[index];
|
|
95
|
-
if (line === undefined)
|
|
96
|
-
return false;
|
|
97
|
-
if (line.trim() === "")
|
|
98
|
-
return true;
|
|
99
|
-
return line.startsWith(indent) && !/^[ \t]/.test(line.slice(indent.length));
|
|
100
|
-
};
|
|
101
|
-
const declaredAt = (index) => {
|
|
102
|
-
const match = MOMENT_INPUT.exec(lines[index] ?? "");
|
|
103
|
-
return match && match[1] === indent ? scalar(match[2] ?? "") : null;
|
|
104
|
-
};
|
|
105
|
-
const scan = (index, step) => sameBlock(index) ? (declaredAt(index) ?? scan(index + step, step)) : null;
|
|
106
|
-
return scan(at - 1, -1) ?? scan(at + 1, 1) ?? DEFAULT_MOMENT;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Every workflow file in a project's `.github/workflows` directory.
|
|
110
|
-
* @param targetPath - Project root
|
|
111
|
-
* @returns Absolute paths, empty when the directory is absent
|
|
112
|
-
*/
|
|
113
|
-
async function workflowFiles(targetPath) {
|
|
114
|
-
const dir = path.join(targetPath, ".github", "workflows");
|
|
115
|
-
const entries = await readdir(dir).catch(() => undefined);
|
|
116
|
-
if (entries === undefined)
|
|
117
|
-
return [];
|
|
118
|
-
return entries
|
|
119
|
-
.filter(name => /\.ya?ml$/.test(name))
|
|
120
|
-
.sort((left, right) => left.localeCompare(right))
|
|
121
|
-
.map(name => path.join(dir, name));
|
|
122
|
-
}
|
|
123
81
|
/**
|
|
124
82
|
* Read one workflow's `skip_jobs` callers.
|
|
125
83
|
*
|
|
@@ -181,12 +139,70 @@ export async function skipJobCallers(targetPath) {
|
|
|
181
139
|
const perFile = await Promise.all(files.map(file => callersIn(registry, targetPath, file)));
|
|
182
140
|
return perFile.flat();
|
|
183
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The `moment` a shared caller should be moved to instead of declaring `off`.
|
|
144
|
+
*
|
|
145
|
+
* A placeholder rather than a concrete environment because doctor cannot know
|
|
146
|
+
* which environment this caller deploys. `release.yml`'s own `moment` input
|
|
147
|
+
* documents this route as the one that retires `skip_jobs`.
|
|
148
|
+
*/
|
|
149
|
+
const OWN_MOMENT_HINT = "moment: pre-deploy:<environment>";
|
|
150
|
+
/**
|
|
151
|
+
* Whether declaring this token's gate would reach a caller it is not about.
|
|
152
|
+
* @param entry - The token's resolved migration
|
|
153
|
+
* @param context - Every job resolving this caller's moment, when known
|
|
154
|
+
* @returns True when the declaration would govern more than this caller
|
|
155
|
+
*/
|
|
156
|
+
function isHeld(entry, context) {
|
|
157
|
+
// Only a declaration can do this damage: every other branch tells the
|
|
158
|
+
// operator to keep or delete the token, neither of which changes what any
|
|
159
|
+
// other caller runs.
|
|
160
|
+
return entry.declaration !== null && (context?.callers.length ?? 0) > 1;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The advice for a token whose moment more than one job resolves.
|
|
164
|
+
*
|
|
165
|
+
* A gate declaration is keyed by gate and moment, so it governs every job
|
|
166
|
+
* resolving that moment. Declaring the gate `off` to neutralise a token that
|
|
167
|
+
* exists on one caller switches the check off on the other caller too — and the
|
|
168
|
+
* other caller is usually the one running it for real. Doctor cannot see
|
|
169
|
+
* whether that run does real work, so it says exactly that and withholds the
|
|
170
|
+
* declaration rather than recommending it (CodySwannGT/lisa#3100).
|
|
171
|
+
* @param entry - The token's resolved migration
|
|
172
|
+
* @param context - The moment and every job resolving it
|
|
173
|
+
* @returns One line naming the risk and the edit that avoids it
|
|
174
|
+
*/
|
|
175
|
+
function describeShared(entry, context) {
|
|
176
|
+
const gate = entry.gate ?? "the gate";
|
|
177
|
+
const files = [...new Set(context.callers)].join(", ");
|
|
178
|
+
const alsoRunning = entry.ungated.length === 0
|
|
179
|
+
? ""
|
|
180
|
+
: ` Separately, ${entry.ungated.join(", ")} have no gate yet and keep ` +
|
|
181
|
+
"running whatever you declare.";
|
|
182
|
+
return (`${entry.token} → HOLD, do not declare this gate off yet: ` +
|
|
183
|
+
`${context.callers.length} jobs resolve the ${context.moment} moment ` +
|
|
184
|
+
`(${files})${unpinnedClause(context)}, and a gate setting applies to the ` +
|
|
185
|
+
`moment, not to one workflow. Writing ${entry.declaration ?? ""} switches ` +
|
|
186
|
+
`${gate} off for all of them — including a workflow that is not passing ` +
|
|
187
|
+
"this token and may be running the check for real. Doctor cannot tell " +
|
|
188
|
+
"from here whether that run does real work, so it will not tell you to " +
|
|
189
|
+
"switch it off. Give this workflow a moment of its own instead — pass " +
|
|
190
|
+
`\`${OWN_MOMENT_HINT}\` on this call, which is what release.yml's moment ` +
|
|
191
|
+
"input is for — then check what your project already declares at that new " +
|
|
192
|
+
"moment, because a moment with nothing declared runs no checks at all, " +
|
|
193
|
+
`and only then declare ${gate} off at the new moment and delete the ` +
|
|
194
|
+
`token.${alsoRunning}`);
|
|
195
|
+
}
|
|
184
196
|
/**
|
|
185
197
|
* The operator-readable line for one token.
|
|
186
198
|
* @param entry - The token's resolved migration
|
|
199
|
+
* @param context - Every job resolving this caller's moment, when known
|
|
187
200
|
* @returns One line naming the edit, or naming why there is none
|
|
188
201
|
*/
|
|
189
|
-
export function describeToken(entry) {
|
|
202
|
+
export function describeToken(entry, context) {
|
|
203
|
+
if (context !== undefined && isHeld(entry, context)) {
|
|
204
|
+
return describeShared(entry, context);
|
|
205
|
+
}
|
|
190
206
|
switch (entry.status) {
|
|
191
207
|
case "replaceable":
|
|
192
208
|
return `${entry.token} → declare ${entry.declaration} and delete the token`;
|
|
@@ -223,18 +239,24 @@ export function describeToken(entry) {
|
|
|
223
239
|
}
|
|
224
240
|
/**
|
|
225
241
|
* Count the token classes the summary has to speak about separately.
|
|
226
|
-
*
|
|
242
|
+
*
|
|
243
|
+
* `held` is split out of `declarable` rather than folded into it: both have a
|
|
244
|
+
* declaration, but writing a held one switches the check off for a caller the
|
|
245
|
+
* token was never about, so the two need opposite instructions.
|
|
246
|
+
* @param reported - Every caller reported, with its moment's other callers
|
|
227
247
|
* @returns The per-class counts
|
|
228
248
|
*/
|
|
229
|
-
function countTokens(
|
|
230
|
-
const tokens =
|
|
231
|
-
const suppressing = tokens.filter(entry => entry.jobs.length > 0);
|
|
232
|
-
const
|
|
249
|
+
function countTokens(reported) {
|
|
250
|
+
const tokens = reported.flatMap(entry => entry.caller.tokens.map(token => ({ token, context: entry.context })));
|
|
251
|
+
const suppressing = tokens.filter(entry => entry.token.jobs.length > 0);
|
|
252
|
+
const withDeclaration = suppressing.filter(entry => entry.token.declaration !== null);
|
|
253
|
+
const held = withDeclaration.filter(entry => isHeld(entry.token, entry.context)).length;
|
|
233
254
|
return {
|
|
234
255
|
total: tokens.length,
|
|
235
256
|
suppressing: suppressing.length,
|
|
236
|
-
declarable,
|
|
237
|
-
|
|
257
|
+
declarable: withDeclaration.length - held,
|
|
258
|
+
held,
|
|
259
|
+
keepers: suppressing.length - withDeclaration.length,
|
|
238
260
|
hollow: tokens.length - suppressing.length,
|
|
239
261
|
};
|
|
240
262
|
}
|
|
@@ -263,6 +285,24 @@ function migrateThese(count) {
|
|
|
263
285
|
? `1 of those has a gate to migrate to: add the${tail}`
|
|
264
286
|
: `${count} of those have a gate to migrate to: add each${tail}`;
|
|
265
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* The tokens whose declaration would reach a caller they were never about.
|
|
290
|
+
*
|
|
291
|
+
* Kept out of `migrateThese` because the two instructions are opposites: one
|
|
292
|
+
* says write the declaration, this one says do not write it yet. Merging them
|
|
293
|
+
* is how the harmful advice got issued in the first place — the summary spoke
|
|
294
|
+
* for every token at once (CodySwannGT/lisa#3100).
|
|
295
|
+
* @param count - How many
|
|
296
|
+
* @returns The sentence
|
|
297
|
+
*/
|
|
298
|
+
function holdThese(count) {
|
|
299
|
+
const why = " marked HOLD below because more than one job resolves that token's " +
|
|
300
|
+
"moment, and a gate setting applies to the moment rather than to one " +
|
|
301
|
+
"workflow: declaring the gate off there would also stop the check " +
|
|
302
|
+
"running for a workflow that is not skipping it. Move that caller to a " +
|
|
303
|
+
"moment of its own before declaring anything.";
|
|
304
|
+
return count === 1 ? `1 is${why}` : `${count} are${why}`;
|
|
305
|
+
}
|
|
266
306
|
/**
|
|
267
307
|
* The tokens that suppress a job no declaration can replace.
|
|
268
308
|
*
|
|
@@ -303,15 +343,16 @@ function deleteThese(count) {
|
|
|
303
343
|
* declare. An operator who followed that text on such a token wrote a
|
|
304
344
|
* declaration for a gate that does not exist. The aggregate and the per-token
|
|
305
345
|
* lines have to agree, so both are now derived from the same classification.
|
|
306
|
-
* @param
|
|
346
|
+
* @param reported - Every caller reported, with its moment's other callers
|
|
307
347
|
* @returns The summary sentences, space-joined
|
|
308
348
|
*/
|
|
309
|
-
function summarize(
|
|
310
|
-
const counts = countTokens(
|
|
349
|
+
function summarize(reported) {
|
|
350
|
+
const counts = countTokens(reported);
|
|
311
351
|
return [
|
|
312
352
|
`${counts.total} skip_jobs token(s) still passed.`,
|
|
313
353
|
...(counts.suppressing > 0 ? [falselyGreen(counts.suppressing)] : []),
|
|
314
354
|
...(counts.declarable > 0 ? [migrateThese(counts.declarable)] : []),
|
|
355
|
+
...(counts.held > 0 ? [holdThese(counts.held)] : []),
|
|
315
356
|
...(counts.keepers > 0 ? [keepThese(counts.keepers)] : []),
|
|
316
357
|
...(counts.hollow > 0 ? [deleteThese(counts.hollow)] : []),
|
|
317
358
|
"doctor does not edit the workflow, so confirm the same checks still run " +
|
|
@@ -337,15 +378,20 @@ export async function checkSkipJobsMigration(targetPath) {
|
|
|
337
378
|
detail: "No caller workflow passes skip_jobs",
|
|
338
379
|
};
|
|
339
380
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
381
|
+
const calls = await qualityCalls(targetPath);
|
|
382
|
+
const reported = callers.map(caller => ({
|
|
383
|
+
caller,
|
|
384
|
+
context: momentContext(calls, caller.moment),
|
|
385
|
+
}));
|
|
386
|
+
const detail = reported
|
|
387
|
+
.map(({ caller, context }) => `${caller.file} (moment: ${caller.moment}) — ${caller.tokens
|
|
388
|
+
.map(token => describeToken(token, context))
|
|
343
389
|
.join("; ")}`)
|
|
344
390
|
.join(" | ");
|
|
345
391
|
return {
|
|
346
392
|
name: CHECK_NAME,
|
|
347
393
|
status: "warn",
|
|
348
|
-
detail: `${summarize(
|
|
394
|
+
detail: `${summarize(reported)} ${detail}`,
|
|
349
395
|
};
|
|
350
396
|
}
|
|
351
397
|
//# sourceMappingURL=doctor-skip-jobs-migration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-skip-jobs-migration.js","sourceRoot":"","sources":["../../src/cli/doctor-skip-jobs-migration.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"doctor-skip-jobs-migration.js","sourceRoot":"","sources":["../../src/cli/doctor-skip-jobs-migration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** The moment `quality.yml` resolves gates at when a caller declares none. */
|
|
2
|
+
export declare const DEFAULT_MOMENT = "pull-request";
|
|
3
|
+
/**
|
|
4
|
+
* Strip YAML quoting and a trailing comment from an inline scalar.
|
|
5
|
+
* @param raw - The text after `key:`
|
|
6
|
+
* @returns The scalar's value
|
|
7
|
+
*/
|
|
8
|
+
export declare function scalar(raw: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The `moment` declared among the keys sitting at one indentation.
|
|
11
|
+
*
|
|
12
|
+
* Scoped by indentation rather than taken from anywhere in the file: a
|
|
13
|
+
* workflow may call `quality.yml` twice with different moments, and attributing
|
|
14
|
+
* one job's moment to the other's tokens would produce a declaration that is
|
|
15
|
+
* legal for neither.
|
|
16
|
+
* @param lines - The workflow file's lines
|
|
17
|
+
* @param at - Index to scan outwards from
|
|
18
|
+
* @param indent - The indentation of the keys to scan
|
|
19
|
+
* @returns The declared moment, or the workflow's default
|
|
20
|
+
*/
|
|
21
|
+
export declare function momentFor(lines: string[], at: number, indent: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Every workflow file in a project's `.github/workflows` directory.
|
|
24
|
+
* @param targetPath - Project root
|
|
25
|
+
* @returns Absolute paths, empty when the directory is absent
|
|
26
|
+
*/
|
|
27
|
+
export declare function workflowFiles(targetPath: string): Promise<string[]>;
|
|
28
|
+
/** One job that reaches `quality.yml`, whether or not it passes `skip_jobs`. */
|
|
29
|
+
export interface QualityCall {
|
|
30
|
+
/** Workflow file, relative to the project root. */
|
|
31
|
+
readonly file: string;
|
|
32
|
+
/** The moment it resolves, or null when an expression decides it. */
|
|
33
|
+
readonly moment: string | null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every job in a project that reaches `quality.yml`.
|
|
37
|
+
* @param targetPath - Project root
|
|
38
|
+
* @returns One entry per job, in workflow-file order
|
|
39
|
+
*/
|
|
40
|
+
export declare function qualityCalls(targetPath: string): Promise<QualityCall[]>;
|
|
41
|
+
/** What else in the project resolves the moment a token's caller resolves. */
|
|
42
|
+
export interface MomentContext {
|
|
43
|
+
/** The moment the caller resolves. */
|
|
44
|
+
readonly moment: string;
|
|
45
|
+
/** Every job resolving it, this caller included, named by workflow file. */
|
|
46
|
+
readonly callers: readonly string[];
|
|
47
|
+
/** Jobs whose moment an expression sets, so doctor cannot pin which it is. */
|
|
48
|
+
readonly unpinned: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The clause naming the jobs whose moment could not be pinned, if any.
|
|
52
|
+
* @param context - The moment and every job resolving it
|
|
53
|
+
* @returns A parenthetical clause, or the empty string
|
|
54
|
+
*/
|
|
55
|
+
export declare function unpinnedClause(context: MomentContext): string;
|
|
56
|
+
/**
|
|
57
|
+
* Which jobs resolve one moment.
|
|
58
|
+
*
|
|
59
|
+
* A job whose moment an expression decides counts against every moment: it may
|
|
60
|
+
* be this one, and leaving it out would under-report the callers a declaration
|
|
61
|
+
* governs — the direction that produces the harmful advice.
|
|
62
|
+
* @param calls - Every job in the project reaching `quality.yml`
|
|
63
|
+
* @param moment - The moment to resolve callers for
|
|
64
|
+
* @returns The moment and the jobs resolving it
|
|
65
|
+
*/
|
|
66
|
+
export declare function momentContext(calls: readonly QualityCall[], moment: string): MomentContext;
|
|
67
|
+
//# sourceMappingURL=workflow-moments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-moments.d.ts","sourceRoot":"","sources":["../../src/cli/workflow-moments.ts"],"names":[],"mappings":"AAoBA,8EAA8E;AAC9E,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAuB7C;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1C;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAS7E;AAgDD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQzE;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAqCD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAM7E;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAO7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,MAAM,EAAE,MAAM,GACb,aAAa,CAYf"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which moment each job in a project's workflows resolves `quality.yml` gates
|
|
3
|
+
* at, and which jobs reach `quality.yml` at all.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `doctor-skip-jobs-migration` because the two questions have
|
|
6
|
+
* different populations. That check reports the jobs passing `skip_jobs`; the
|
|
7
|
+
* jobs that decide whether its advice is SAFE are mostly the ones passing no
|
|
8
|
+
* `skip_jobs` at all, because a gate declaration is keyed by gate and moment in
|
|
9
|
+
* `.lisa.config.json` and therefore governs every job resolving that moment
|
|
10
|
+
* (CodySwannGT/lisa#3100).
|
|
11
|
+
*
|
|
12
|
+
* Matched as text rather than through a YAML parse, for the same reason
|
|
13
|
+
* `doctor-reusable-workflow-refs` does: a caller whose YAML is malformed still
|
|
14
|
+
* passes the input, and a parse failure would silently drop the file from the
|
|
15
|
+
* audit — reporting a migrated project because one file could not be read.
|
|
16
|
+
* @module cli/workflow-moments
|
|
17
|
+
*/
|
|
18
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
/** The moment `quality.yml` resolves gates at when a caller declares none. */
|
|
21
|
+
export const DEFAULT_MOMENT = "pull-request";
|
|
22
|
+
/** A caller's `moment` input, whatever quoting style it uses. */
|
|
23
|
+
const MOMENT_INPUT = /^([ \t]{0,64})moment:[ \t]{0,64}(.{0,4096})$/;
|
|
24
|
+
/** Any `uses:` line, with its indentation and its unparsed value. */
|
|
25
|
+
const USES_INPUT = /^([ \t]{0,64})uses:[ \t]{0,64}(.{0,4096})$/;
|
|
26
|
+
/**
|
|
27
|
+
* A `uses:` value reaching `quality.yml`, directly or through `release.yml`.
|
|
28
|
+
*
|
|
29
|
+
* `release.yml` counts because it forwards its own `moment` input straight on
|
|
30
|
+
* to `quality.yml`, so a job calling it resolves a `quality.yml` gate set just
|
|
31
|
+
* as directly as a job naming `quality.yml` itself.
|
|
32
|
+
*/
|
|
33
|
+
const QUALITY_WORKFLOW = /workflows\/(?:quality|release)\.ya?ml(?:@|$)/;
|
|
34
|
+
/** The `with:` key — the sibling of `uses:` whose children carry `moment`. */
|
|
35
|
+
const WITH_BLOCK = /^with:[ \t]{0,64}(?:#.{0,512})?$/;
|
|
36
|
+
/** A `workflow_call` trigger, in either the block or the inline-list form. */
|
|
37
|
+
const WORKFLOW_CALL = /^(?:[ \t]{1,64}workflow_call:|on:.{0,256}workflow_call)/;
|
|
38
|
+
/**
|
|
39
|
+
* Strip YAML quoting and a trailing comment from an inline scalar.
|
|
40
|
+
* @param raw - The text after `key:`
|
|
41
|
+
* @returns The scalar's value
|
|
42
|
+
*/
|
|
43
|
+
export function scalar(raw) {
|
|
44
|
+
const withoutComment = /^(['"])(.*?)\1/.exec(raw.trim());
|
|
45
|
+
if (withoutComment)
|
|
46
|
+
return withoutComment[2] ?? "";
|
|
47
|
+
return raw.split("#")[0]?.trim() ?? "";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A predicate for "this line is still a key of the block sitting at `indent`".
|
|
51
|
+
*
|
|
52
|
+
* Blank lines count as inside so a block separated by one is not cut in half.
|
|
53
|
+
* @param lines - The workflow file's lines
|
|
54
|
+
* @param indent - The leading whitespace of the block's keys
|
|
55
|
+
* @returns The predicate
|
|
56
|
+
*/
|
|
57
|
+
function blockAt(lines, indent) {
|
|
58
|
+
return (index) => {
|
|
59
|
+
const line = lines[index];
|
|
60
|
+
if (line === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
if (line.trim() === "")
|
|
63
|
+
return true;
|
|
64
|
+
return line.startsWith(indent) && !/^[ \t]/.test(line.slice(indent.length));
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `moment` declared among the keys sitting at one indentation.
|
|
69
|
+
*
|
|
70
|
+
* Scoped by indentation rather than taken from anywhere in the file: a
|
|
71
|
+
* workflow may call `quality.yml` twice with different moments, and attributing
|
|
72
|
+
* one job's moment to the other's tokens would produce a declaration that is
|
|
73
|
+
* legal for neither.
|
|
74
|
+
* @param lines - The workflow file's lines
|
|
75
|
+
* @param at - Index to scan outwards from
|
|
76
|
+
* @param indent - The indentation of the keys to scan
|
|
77
|
+
* @returns The declared moment, or the workflow's default
|
|
78
|
+
*/
|
|
79
|
+
export function momentFor(lines, at, indent) {
|
|
80
|
+
const inBlock = blockAt(lines, indent);
|
|
81
|
+
const declaredAt = (index) => {
|
|
82
|
+
const match = MOMENT_INPUT.exec(lines[index] ?? "");
|
|
83
|
+
return match && match[1] === indent ? scalar(match[2] ?? "") : null;
|
|
84
|
+
};
|
|
85
|
+
const scan = (index, step) => inBlock(index) ? (declaredAt(index) ?? scan(index + step, step)) : null;
|
|
86
|
+
return scan(at - 1, -1) ?? scan(at + 1, 1) ?? DEFAULT_MOMENT;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The indentation of a block's first child line.
|
|
90
|
+
* @param lines - The workflow file's lines
|
|
91
|
+
* @param from - Index to start looking at
|
|
92
|
+
* @param outer - The parent key's leading whitespace
|
|
93
|
+
* @returns The child indentation, or null when the block has no children
|
|
94
|
+
*/
|
|
95
|
+
function firstChildIndent(lines, from, outer) {
|
|
96
|
+
const line = lines[from];
|
|
97
|
+
if (line === undefined)
|
|
98
|
+
return null;
|
|
99
|
+
if (line.trim() === "")
|
|
100
|
+
return firstChildIndent(lines, from + 1, outer);
|
|
101
|
+
const lead = /^[ \t]{0,128}/.exec(line)?.[0] ?? "";
|
|
102
|
+
return lead.length > outer.length && lead.startsWith(outer) ? lead : null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The moment one `uses:` call resolves gates at.
|
|
106
|
+
*
|
|
107
|
+
* `with:` is a sibling key of `uses:` inside the same job and `moment:` is one
|
|
108
|
+
* of its children, so this locates the sibling `with:` and then reuses the
|
|
109
|
+
* indentation-scoped scan `momentFor` performs around a `skip_jobs` line.
|
|
110
|
+
* @param lines - The workflow file's lines
|
|
111
|
+
* @param at - Index of the `uses:` line
|
|
112
|
+
* @param indent - That line's leading whitespace
|
|
113
|
+
* @returns The declared moment, or the workflow's default
|
|
114
|
+
*/
|
|
115
|
+
function momentForCall(lines, at, indent) {
|
|
116
|
+
const inBlock = blockAt(lines, indent);
|
|
117
|
+
const findWith = (index, step) => {
|
|
118
|
+
if (!inBlock(index))
|
|
119
|
+
return null;
|
|
120
|
+
if (WITH_BLOCK.test((lines[index] ?? "").slice(indent.length)))
|
|
121
|
+
return index;
|
|
122
|
+
return findWith(index + step, step);
|
|
123
|
+
};
|
|
124
|
+
const withAt = findWith(at - 1, -1) ?? findWith(at + 1, 1);
|
|
125
|
+
if (withAt === null)
|
|
126
|
+
return DEFAULT_MOMENT;
|
|
127
|
+
const children = firstChildIndent(lines, withAt + 1, indent);
|
|
128
|
+
return children === null
|
|
129
|
+
? DEFAULT_MOMENT
|
|
130
|
+
: momentFor(lines, withAt, children);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Every workflow file in a project's `.github/workflows` directory.
|
|
134
|
+
* @param targetPath - Project root
|
|
135
|
+
* @returns Absolute paths, empty when the directory is absent
|
|
136
|
+
*/
|
|
137
|
+
export async function workflowFiles(targetPath) {
|
|
138
|
+
const dir = path.join(targetPath, ".github", "workflows");
|
|
139
|
+
const entries = await readdir(dir).catch(() => undefined);
|
|
140
|
+
if (entries === undefined)
|
|
141
|
+
return [];
|
|
142
|
+
return entries
|
|
143
|
+
.filter(name => /\.ya?ml$/.test(name))
|
|
144
|
+
.sort((left, right) => left.localeCompare(right))
|
|
145
|
+
.map(name => path.join(dir, name));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Every job in one workflow that reaches `quality.yml`.
|
|
149
|
+
*
|
|
150
|
+
* A workflow that is itself called by others (`on: workflow_call`) is skipped.
|
|
151
|
+
* `release.yml` is one, and it forwards whatever moment ITS caller passed — so
|
|
152
|
+
* counting it would invent a caller no repository ever triggers, and would name
|
|
153
|
+
* a file the operator cannot edit.
|
|
154
|
+
* @param targetPath - Project root
|
|
155
|
+
* @param file - Absolute path to the workflow
|
|
156
|
+
* @returns One entry per job reaching `quality.yml`
|
|
157
|
+
*/
|
|
158
|
+
async function qualityCallsIn(targetPath, file) {
|
|
159
|
+
const lines = (await readFile(file, "utf8").catch(() => "")).split("\n");
|
|
160
|
+
if (lines.some(line => WORKFLOW_CALL.test(line)))
|
|
161
|
+
return [];
|
|
162
|
+
return lines.flatMap((line, index) => {
|
|
163
|
+
const match = USES_INPUT.exec(line);
|
|
164
|
+
if (match === null)
|
|
165
|
+
return [];
|
|
166
|
+
if (!QUALITY_WORKFLOW.test(scalar(match[2] ?? "")))
|
|
167
|
+
return [];
|
|
168
|
+
const moment = momentForCall(lines, index, match[1] ?? "");
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
file: path.relative(targetPath, file),
|
|
172
|
+
// An expression is not a moment. Reported as null so the advice counts
|
|
173
|
+
// this job against EVERY moment and says it could not be pinned —
|
|
174
|
+
// guessing the default instead would shrink the set of callers a
|
|
175
|
+
// declaration is known to govern, which is the direction that harms.
|
|
176
|
+
moment: moment.includes("${{") ? null : moment,
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Every job in a project that reaches `quality.yml`.
|
|
183
|
+
* @param targetPath - Project root
|
|
184
|
+
* @returns One entry per job, in workflow-file order
|
|
185
|
+
*/
|
|
186
|
+
export async function qualityCalls(targetPath) {
|
|
187
|
+
const files = await workflowFiles(targetPath);
|
|
188
|
+
const perFile = await Promise.all(files.map(file => qualityCallsIn(targetPath, file)));
|
|
189
|
+
return perFile.flat();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The clause naming the jobs whose moment could not be pinned, if any.
|
|
193
|
+
* @param context - The moment and every job resolving it
|
|
194
|
+
* @returns A parenthetical clause, or the empty string
|
|
195
|
+
*/
|
|
196
|
+
export function unpinnedClause(context) {
|
|
197
|
+
if (context.unpinned.length === 0)
|
|
198
|
+
return "";
|
|
199
|
+
return (` (${context.unpinned.join(", ")} builds its moment from an expression, ` +
|
|
200
|
+
"so doctor cannot tell which moment it resolves and counts it here rather " +
|
|
201
|
+
"than assuming it is a different one)");
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Which jobs resolve one moment.
|
|
205
|
+
*
|
|
206
|
+
* A job whose moment an expression decides counts against every moment: it may
|
|
207
|
+
* be this one, and leaving it out would under-report the callers a declaration
|
|
208
|
+
* governs — the direction that produces the harmful advice.
|
|
209
|
+
* @param calls - Every job in the project reaching `quality.yml`
|
|
210
|
+
* @param moment - The moment to resolve callers for
|
|
211
|
+
* @returns The moment and the jobs resolving it
|
|
212
|
+
*/
|
|
213
|
+
export function momentContext(calls, moment) {
|
|
214
|
+
const byName = (left, right) => left.localeCompare(right);
|
|
215
|
+
const unpinned = calls.filter(call => call.moment === null);
|
|
216
|
+
return {
|
|
217
|
+
moment,
|
|
218
|
+
callers: calls
|
|
219
|
+
.filter(call => call.moment === null || call.moment === moment)
|
|
220
|
+
.map(call => call.file)
|
|
221
|
+
.sort(byName),
|
|
222
|
+
unpinned: [...new Set(unpinned.map(call => call.file))].sort(byName),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=workflow-moments.js.map
|