@codyswann/lisa 4.29.11 → 4.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/cdk/create-only/.github/workflows/ci.yml +1 -1
  2. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  3. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +16 -20
  6. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  7. package/expo/create-only/.github/workflows/deploy.yml +4 -4
  8. package/harper-fabric/create-only/.github/workflows/deploy.yml +1 -1
  9. package/harper-fabric/create-only/.github/workflows/zap-baseline.yml +1 -1
  10. package/nestjs/create-only/.github/workflows/deploy.yml +9 -9
  11. package/npm-package/create-only/.github/workflows/publish-to-npm.yml +1 -1
  12. package/package.json +5 -4
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-agy/plugin.json +1 -1
  26. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  46. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-agy/plugin.json +1 -1
  51. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  56. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  61. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  63. package/rails/create-only/.github/workflows/deploy.yml +3 -3
  64. package/scripts/check-third-party-action-pins.mjs +455 -0
  65. package/scripts/two-channel-couplings.json +2 -2
  66. package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +0 -14
  67. package/typescript/deletions.json +3 -0
  68. package/ui/index.html +0 -18
  69. package/typescript/create-only/.github/workflows/auto-update-pr-branches-dispatch.yml +0 -27
  70. package/typescript/create-only/.github/workflows/auto-update-pr-branches.yml +0 -31
@@ -83,7 +83,7 @@ jobs:
83
83
  fetch-depth: 0
84
84
  ref: ${{ github.ref_name }}
85
85
 
86
- - uses: noliran/branch-based-secrets@v1
86
+ - uses: noliran/branch-based-secrets@ef9182f16f118c7c3d05c36973546ebc990b8c69 # v1 (2020-10-09, no semver tag)
87
87
  with:
88
88
  secrets: AWS_ACCOUNT_ID
89
89
 
@@ -91,7 +91,7 @@ jobs:
91
91
  run: echo "APP_STAGE=$([[ $GITHUB_REF_NAME == 'main' ]] && echo 'production' || echo $GITHUB_REF_NAME)" >> $GITHUB_ENV
92
92
 
93
93
  - name: Configure AWS credentials
94
- uses: aws-actions/configure-aws-credentials@v4
94
+ uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
95
95
  with:
96
96
  role-to-assume: arn:aws:iam::${{ secrets[env.AWS_ACCOUNT_ID_NAME] }}:role/DeployServiceRole
97
97
  role-session-name: fedeploysession
@@ -117,7 +117,7 @@ jobs:
117
117
 
118
118
  - name: Amazon ECR Login
119
119
  id: login-ecr
120
- uses: aws-actions/amazon-ecr-login@v2
120
+ uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2.1.7
121
121
 
122
122
  - name: Prepare AWS credentials file for BuildKit
123
123
  run: |
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deterministic gate that every THIRD-PARTY GitHub Action this repository uses
4
+ * — in its own workflows and in the workflow templates it seeds into consumers
5
+ * — is referenced by a full 40-character commit SHA (issue #3585).
6
+ *
7
+ * A mutable ref is not a version. `@master`, `@main`, `@v1` and `@v0.9.0` are
8
+ * pointers that resolve, at job start, to whatever the upstream owner has
9
+ * pushed at that instant, and for a third-party action that owner is a
10
+ * stranger. A tag is mutable by definition — `git push --force` moves it — and
11
+ * a floating major is *designed* to move.
12
+ *
13
+ * The exposure is not theoretical here. When this gate was written the tree
14
+ * held 19 distinct third-party references, none pinned, thirteen of them in
15
+ * jobs holding a credential:
16
+ *
17
+ * - `snyk/actions/node@master` with the Snyk token in the same step's `env:`;
18
+ * - `fossas/fossa-action@main` handed the FOSSA key as an input;
19
+ * - `noliran/branch-based-secrets@v1`, whose ENTIRE FUNCTION is to resolve
20
+ * repository secrets into the job environment, in the publish path;
21
+ * - `GeoWerkstatt/create-jira-release@v1`, handed a webhook secret.
22
+ *
23
+ * `quality.yml` and `release.yml` are reusable workflows the fleet calls at
24
+ * `@main`, so a compromised upstream would execute inside every consumer's
25
+ * quality gate on their next run.
26
+ *
27
+ * Pinning today does not stop the next `uses:` line, which is why the durable
28
+ * artifact is this detector rather than the pins. A stale comment in a
29
+ * consumer repository nearly caused two load-bearing references to be
30
+ * unpinned; prose does not hold a boundary, an executable check does.
31
+ *
32
+ * ## What is deliberately NOT in scope
33
+ *
34
+ * **First-party references.** `uses: CodySwannGT/lisa/...@main` is a different
35
+ * threat model — a stability question about an upstream we own and can read,
36
+ * tracked separately in #3488 — and a check that reddens it is the wrong check
37
+ * and will be turned off. First-party references never appear in this gate's
38
+ * findings and are never named in its output. That exemption is a required
39
+ * property with its own test, not an afterthought.
40
+ *
41
+ * **`actions/*` and `github/*`.** These are published by GitHub itself, the
42
+ * same party that runs the job, so pinning them moves no trust boundary. The
43
+ * decision is recorded here so it stays consistent, and the exemption is
44
+ * narrow: it is an OWNER allowlist, not a substring match.
45
+ *
46
+ * **Fixture trees** (`tests/fixtures/`, `parity/fixtures/`). These are inputs
47
+ * to other checks, never executed by GitHub Actions, and pinning them would
48
+ * distort the shapes those checks exist to recognise. The count of skipped
49
+ * fixture files is REPORTED in every run rather than silently dropped, so the
50
+ * boundary is visible in the output instead of buried in this comment.
51
+ *
52
+ * **Consumer repositories already seeded from the `create-only` templates.**
53
+ * A `create-only` file is written once and never overwritten, so correcting a
54
+ * template changes what FUTURE repositories are seeded with and changes
55
+ * nothing about any repository that already exists. This gate runs in this
56
+ * repository only; it cannot and does not report seeded consumers as fixed.
57
+ *
58
+ * ## Why a bare SHA is also a finding
59
+ *
60
+ * `uses: <vendor>/<action>@<40-hex>` with no trailing comment is
61
+ * immutable but unreadable: nobody can tell what is installed, so nobody
62
+ * upgrades it, so it rots at whatever commit it was pinned at. The pin and the
63
+ * human-readable version are one artifact, and the gate treats them that way.
64
+ *
65
+ * Determinism guarantees (so the unit test is reproducible and CI is stable):
66
+ * - zero third-party dependencies (Node built-ins only),
67
+ * - no network access,
68
+ * - no `Date` / `Math.random`,
69
+ * - the file list comes from `git ls-files`, so the gate sees exactly what a
70
+ * release would carry.
71
+ *
72
+ * CLI:
73
+ * node scripts/check-third-party-action-pins.mjs [--root <dir>] [--json]
74
+ *
75
+ * Exit codes (mirroring the sibling gates in this directory):
76
+ * 0 — every third-party reference is a 40-hex SHA carrying a version comment.
77
+ * 1 — >=1 reference is mutable, or pinned without a version comment.
78
+ * 2 — operational/usage error: unknown flag, a flag missing its value,
79
+ * `--root` absent or not a git repository, git unavailable, or zero
80
+ * workflow files discovered. Finding nothing to check is a broken
81
+ * invocation, not conformance.
82
+ *
83
+ * @module scripts/check-third-party-action-pins
84
+ */
85
+ import fs from "node:fs";
86
+ import path from "node:path";
87
+ import process from "node:process";
88
+ import { fileURLToPath } from "node:url";
89
+
90
+ import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
91
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
92
+
93
+ const REPO_ROOT = path.resolve(
94
+ path.dirname(fileURLToPath(import.meta.url)),
95
+ ".."
96
+ );
97
+
98
+ /**
99
+ * Any workflow file, at any depth: this repository's own `.github/workflows/`
100
+ * and every seeded template tree, so a mutable ref cannot be introduced by
101
+ * seeding rather than by running.
102
+ */
103
+ const WORKFLOW_RE = /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/;
104
+
105
+ /**
106
+ * Fixture trees: inputs to other checks, never executed by GitHub Actions.
107
+ * Prefix match on the repo-relative path, so a real workflow cannot hide
108
+ * behind a directory that merely contains the word "fixtures".
109
+ */
110
+ const FIXTURE_PREFIXES = ["tests/fixtures/", "parity/fixtures/"];
111
+
112
+ /**
113
+ * The owner of this repository's own reusable workflows. Compared
114
+ * case-insensitively because GitHub owners are case-insensitive and a `uses:`
115
+ * line may spell one either way.
116
+ */
117
+ const FIRST_PARTY_OWNERS = ["codyswanngt"];
118
+
119
+ /** GitHub's own action namespaces — the runner's publisher, see header. */
120
+ const GITHUB_OWNED_OWNERS = ["actions", "github"];
121
+
122
+ /** Classification of an action's owner against the two documented exemptions. */
123
+ const FIRST_PARTY = "first-party";
124
+ const GITHUB_OWNED = "github-owned";
125
+ const THIRD_PARTY = "third-party";
126
+
127
+ /** Verdicts `evaluateReference` returns; `MUTABLE` is the headline finding. */
128
+ const OK = "ok";
129
+ const MUTABLE = "mutable-ref";
130
+ const NO_VERSION = "missing-version-comment";
131
+
132
+ /**
133
+ * `uses: <owner>/<repo>[/<subpath>]@<ref>` with anything after it captured, so
134
+ * a trailing `# v1.2.3` can be read. Local (`./path`) and container
135
+ * (`docker://`) references have no owner and never match.
136
+ */
137
+ const USES_RE =
138
+ /^\s*(?:-\s*)?uses:\s*(?<owner>[A-Za-z0-9][A-Za-z0-9-]*)\/(?<rest>[A-Za-z0-9_.][A-Za-z0-9_./-]*)@(?<ref>[^\s"'#]+)(?<trailer>.*)$/;
139
+
140
+ /** A full commit SHA. Abbreviated SHAs are ambiguous and do not count. */
141
+ const FULL_SHA_RE = /^[0-9a-f]{40}$/;
142
+
143
+ /** A trailing `# <version>` comment with at least one non-space character. */
144
+ const VERSION_COMMENT_RE = /#\s*(?<version>\S.*?)\s*$/;
145
+
146
+ /** Max bytes of `git ls-files` output (7k+ tracked paths is ~0.3 MB today). */
147
+ const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
148
+
149
+ /**
150
+ * Usage error — thrown for an invalid invocation or an unusable state so
151
+ * `main` can distinguish it (exit 2) from a finding (exit 1).
152
+ */
153
+ export class UsageError extends Error {}
154
+
155
+ /**
156
+ * Classify an action owner against the two documented exemptions.
157
+ *
158
+ * @param {string} owner - the owner segment of a `uses:` reference.
159
+ * @returns {"first-party" | "github-owned" | "third-party"} the classification.
160
+ */
161
+ export function classifyOwner(owner) {
162
+ const normalized = String(owner).toLowerCase();
163
+ if (FIRST_PARTY_OWNERS.includes(normalized)) return FIRST_PARTY;
164
+ if (GITHUB_OWNED_OWNERS.includes(normalized)) return GITHUB_OWNED;
165
+ return THIRD_PARTY;
166
+ }
167
+
168
+ /**
169
+ * Extract every action reference in `content`, already classified.
170
+ *
171
+ * Every reference is returned, first-party ones included, so the caller can
172
+ * count what it examined. Only third-party ones are ever eligible to become a
173
+ * finding — see `evaluateReference`.
174
+ *
175
+ * @param {string} content - full text of a workflow file.
176
+ * @returns {{ action: string, owner: string, ref: string, version: string | null, kind: string, line: number }[]}
177
+ * one entry per reference, with 1-based line numbers, in file order.
178
+ */
179
+ export function findActionRefs(content) {
180
+ const lines = String(content).split(/\r?\n/);
181
+ const refs = [];
182
+ for (let index = 0; index < lines.length; index++) {
183
+ const match = USES_RE.exec(lines[index]);
184
+ if (match === null) continue;
185
+ const { owner, ref, rest, trailer } = match.groups;
186
+ const comment = VERSION_COMMENT_RE.exec(trailer);
187
+ refs.push({
188
+ action: `${owner}/${rest}`,
189
+ kind: classifyOwner(owner),
190
+ line: index + 1,
191
+ owner,
192
+ ref,
193
+ version: comment === null ? null : comment.groups.version,
194
+ });
195
+ }
196
+ return refs;
197
+ }
198
+
199
+ /**
200
+ * Decide whether one reference is a finding.
201
+ *
202
+ * @param {{ kind: string, ref: string, version: string | null }} reference -
203
+ * a reference from `findActionRefs`.
204
+ * @returns {"ok" | "mutable-ref" | "missing-version-comment"} the verdict.
205
+ */
206
+ export function evaluateReference(reference) {
207
+ if (reference.kind !== THIRD_PARTY) return OK;
208
+ if (!FULL_SHA_RE.test(reference.ref)) return MUTABLE;
209
+ return reference.version === null ? NO_VERSION : OK;
210
+ }
211
+
212
+ /**
213
+ * Whether a repo-relative path is a workflow this gate governs.
214
+ *
215
+ * @param {string} file - repo-relative path.
216
+ * @returns {boolean} whether the file is an in-scope workflow.
217
+ */
218
+ export function isGovernedWorkflow(file) {
219
+ return WORKFLOW_RE.test(file) && !isFixtureWorkflow(file);
220
+ }
221
+
222
+ /**
223
+ * Whether a repo-relative path is an excluded fixture workflow.
224
+ *
225
+ * @param {string} file - repo-relative path.
226
+ * @returns {boolean} whether the file sits in an excluded fixture tree.
227
+ */
228
+ export function isFixtureWorkflow(file) {
229
+ return (
230
+ WORKFLOW_RE.test(file) &&
231
+ FIXTURE_PREFIXES.some(prefix => file.startsWith(prefix))
232
+ );
233
+ }
234
+
235
+ /**
236
+ * List every tracked file in `root`, relative to it. Throws `UsageError` when
237
+ * git is unavailable or `root` is not a repository.
238
+ *
239
+ * @param {string} root - the repository root.
240
+ * @returns {string[]} tracked paths, relative to `root`.
241
+ */
242
+ function listTrackedFiles(root) {
243
+ let stdout;
244
+ try {
245
+ stdout = boundedExecFileSync("git", ["-C", root, "ls-files", "-z"], {
246
+ encoding: "utf8",
247
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
248
+ stdio: ["ignore", "pipe", "ignore"],
249
+ });
250
+ } catch (error) {
251
+ throw new UsageError(
252
+ `could not list tracked files in ${root}: ${error.message}`
253
+ );
254
+ }
255
+ return stdout.split("\0").filter(entry => entry !== "");
256
+ }
257
+
258
+ /**
259
+ * Assemble the machine-readable report.
260
+ *
261
+ * @param {ReadonlyArray<Record<string, unknown>>} findings - non-ok references.
262
+ * @param {{ root: string, workflows: number, fixturesSkipped: number, thirdParty: number, checked: number }} opts
263
+ * resolved options plus scan size.
264
+ * @returns {Record<string, unknown>} the report object.
265
+ */
266
+ export function buildReport(findings, opts) {
267
+ return {
268
+ findings,
269
+ root: opts.root,
270
+ schemaVersion: 1,
271
+ summary: {
272
+ checked: opts.checked,
273
+ fixturesSkipped: opts.fixturesSkipped,
274
+ missingVersionComment: findings.filter(
275
+ finding => finding.verdict === NO_VERSION
276
+ ).length,
277
+ mutable: findings.filter(finding => finding.verdict === MUTABLE).length,
278
+ thirdParty: opts.thirdParty,
279
+ workflows: opts.workflows,
280
+ },
281
+ };
282
+ }
283
+
284
+ /**
285
+ * The remediation text. A guard that reddens without teaching the fix gets
286
+ * bypassed, so the exact command that produces the SHA is in the output.
287
+ *
288
+ * @returns {string[]} lines of remediation guidance.
289
+ */
290
+ function remediation() {
291
+ return [
292
+ "Pin every third-party action to a full 40-character commit SHA, with the",
293
+ "human-readable version in a trailing comment so the pin stays auditable:",
294
+ "",
295
+ " - uses: <vendor>/<action>@<40-hex-sha> # v1.2.3",
296
+ "",
297
+ "Resolve the SHA for the ref the workflow uses TODAY — pinning and",
298
+ "upgrading in one commit makes a regression unattributable:",
299
+ "",
300
+ " gh api repos/<owner>/<repo>/commits/<ref> --jq .sha",
301
+ "",
302
+ "Then name the version that SHA carries:",
303
+ "",
304
+ " gh api repos/<owner>/<repo>/tags --jq '.[] | select(.commit.sha==\"<sha>\") | .name'",
305
+ "",
306
+ "A branch head with no tag is pinned the same way; say so in the comment,",
307
+ "e.g. `# main (untagged, 2026-08-28)`.",
308
+ "",
309
+ "First-party workflow references and GitHub's own actions/* and github/*",
310
+ "are out of scope for this gate by design and are never reported here.",
311
+ ];
312
+ }
313
+
314
+ /**
315
+ * Render the human-readable report.
316
+ *
317
+ * @param {{ findings: ReadonlyArray<Record<string, unknown>>, summary: Record<string, number> }} report
318
+ * the report object.
319
+ * @returns {string} the rendered report.
320
+ */
321
+ function humanReport(report) {
322
+ const { summary } = report;
323
+ const scanned =
324
+ `${summary.thirdParty} third-party reference(s) across ` +
325
+ `${summary.workflows} workflow file(s) ` +
326
+ `(${summary.fixturesSkipped} fixture workflow(s) skipped)`;
327
+ if (report.findings.length === 0) {
328
+ return `✓ ${scanned} — all pinned to a full commit SHA with a version comment`;
329
+ }
330
+ const lines = report.findings.map(finding =>
331
+ finding.verdict === MUTABLE
332
+ ? `✗ ${finding.file}:${finding.line}\n ${finding.action}@${finding.ref} — mutable ref, not a commit SHA`
333
+ : `✗ ${finding.file}:${finding.line}\n ${finding.action}@${finding.ref} — pinned but no version comment`
334
+ );
335
+ return [
336
+ ...lines,
337
+ "",
338
+ `${report.findings.length} of ${scanned} are not safely pinned.`,
339
+ "",
340
+ ...remediation(),
341
+ ].join("\n");
342
+ }
343
+
344
+ /**
345
+ * Parse argv into resolved options. Throws `UsageError` on a bad invocation.
346
+ *
347
+ * @param {readonly string[]} argv - arguments (without node/script prefix).
348
+ * @returns {{ root: string, json: boolean }} options.
349
+ */
350
+ export function parseArgs(argv) {
351
+ let root = null;
352
+ let json = false;
353
+ for (let i = 0; i < argv.length; i++) {
354
+ const arg = argv[i];
355
+ if (arg === "--json") {
356
+ json = true;
357
+ } else if (arg === "--root") {
358
+ const next = argv[i + 1];
359
+ if (next === undefined || next.startsWith("--")) {
360
+ throw new UsageError("--root requires a value");
361
+ }
362
+ root = next;
363
+ i += 1;
364
+ } else {
365
+ throw new UsageError(`unknown argument: ${arg}`);
366
+ }
367
+ }
368
+ return { json, root: path.resolve(root ?? REPO_ROOT) };
369
+ }
370
+
371
+ /**
372
+ * Resolve the file list for a run, or throw `UsageError`.
373
+ *
374
+ * @param {string} root - the repository root.
375
+ * @returns {{ workflows: string[], fixturesSkipped: number }} the scan inputs.
376
+ */
377
+ function collectWorkflows(root) {
378
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
379
+ throw new UsageError(`--root is not a directory: ${root}`);
380
+ }
381
+ const tracked = listTrackedFiles(root);
382
+ const workflows = tracked.filter(file => isGovernedWorkflow(file));
383
+ if (workflows.length === 0) {
384
+ // Finding nothing to check is a broken invocation, not conformance.
385
+ throw new UsageError(
386
+ `no workflow files found under ${root} — expected paths like ` +
387
+ ".github/workflows/*.yml. Refusing to report a clean run for a scan " +
388
+ "that examined nothing."
389
+ );
390
+ }
391
+ return {
392
+ fixturesSkipped: tracked.filter(file => isFixtureWorkflow(file)).length,
393
+ workflows,
394
+ };
395
+ }
396
+
397
+ /**
398
+ * Run the gate. Returns the process exit code (does not call `exit`).
399
+ *
400
+ * @param {readonly string[]} argv - arguments (without node/script prefix).
401
+ * @param {{ stdout?: { write(s: string): void }, stderr?: { write(s: string): void } }} [io]
402
+ * injectable streams (defaults to process streams).
403
+ * @returns {number} exit code (0 clean, 1 finding, 2 usage error).
404
+ */
405
+ export function main(argv, io = {}) {
406
+ const out = io.stdout ?? process.stdout;
407
+ const err = io.stderr ?? process.stderr;
408
+ let opts;
409
+ let scan;
410
+ try {
411
+ opts = parseArgs(argv);
412
+ scan = collectWorkflows(opts.root);
413
+ } catch (error) {
414
+ err.write(`error: ${error.message}\n`);
415
+ return 2;
416
+ }
417
+
418
+ let checked = 0;
419
+ let thirdParty = 0;
420
+ const findings = [];
421
+ try {
422
+ for (const file of scan.workflows) {
423
+ const content = fs.readFileSync(path.join(opts.root, file), "utf8");
424
+ for (const reference of findActionRefs(content)) {
425
+ checked += 1;
426
+ if (reference.kind === THIRD_PARTY) thirdParty += 1;
427
+ const verdict = evaluateReference(reference);
428
+ if (verdict !== OK) {
429
+ findings.push({ ...reference, file, verdict });
430
+ }
431
+ }
432
+ }
433
+ } catch (error) {
434
+ err.write(`error: failed to scan workflows: ${error.message}\n`);
435
+ return 2;
436
+ }
437
+
438
+ const report = buildReport(findings, {
439
+ checked,
440
+ fixturesSkipped: scan.fixturesSkipped,
441
+ root: opts.root,
442
+ thirdParty,
443
+ workflows: scan.workflows.length,
444
+ });
445
+ out.write(
446
+ `${opts.json ? JSON.stringify(report, null, 2) : humanReport(report)}\n`
447
+ );
448
+ return report.findings.length === 0 ? 0 : 1;
449
+ }
450
+
451
+ if (invokedAsScript(import.meta.url)) {
452
+ // exitCode (not process.exit): when stdout is a pipe, writes are async and
453
+ // process.exit() truncates the report mid-flush.
454
+ process.exitCode = main(process.argv.slice(2));
455
+ }
@@ -4,8 +4,8 @@
4
4
  "quality.yml::scripts/generate-nightly-e2e-guard-certificate.mjs": "Lisa-source-only branch. Both reusable jobs require `github.repository == 'CodySwannGT/lisa'` before the step can start, and the run body repeats that exact repository guard before checking this artifact path. Ordinary and private host callers therefore skip the step without reading or fetching from their own tree. The scoped branch fetches only the immutable refs needed to behavior-test Lisa's own retained certificate inputs. Ratified because the caller-tree read is unreachable outside this repository, not because an undelivered host dependency is tolerated."
5
5
  },
6
6
  "inspected": {
7
- "workflows": 24,
8
- "steps": 680,
7
+ "workflows": 22,
8
+ "steps": 670,
9
9
  "couplings": 33,
10
10
  "inventory": 68
11
11
  },
@@ -306,19 +306,6 @@ This workflow is read-only — it does not modify code or create PRs. It only re
306
306
 
307
307
  3. The workflow auto-enables once all three variables and both secrets are set. No feature flag needed.
308
308
 
309
- ### Auto-update PR Branches (`auto-update-pr-branches.yml`)
310
-
311
- **Triggers**: Push to `main`, `staging`, or `dev`
312
-
313
- Automatically updates all open pull requests targeting the pushed branch by merging the latest base branch changes into PR branches. Uses [`chinthakagodawita/autoupdate`](https://github.com/chinthakagodawita/autoupdate) (v1.7.0).
314
-
315
- - Updates all open PRs (including drafts) targeting the updated branch
316
- - Skips PRs with merge conflicts (does not fail the workflow)
317
- - Retries up to 5 times on transient failures
318
- - No additional secrets required (uses `GITHUB_TOKEN`)
319
-
320
- **Note**: The merge commit will be attributed to `github-actions[bot]`. To attribute it to a specific user, add a PAT as a repository secret and update the workflow.
321
-
322
309
  ### Load Testing (`load-test.yml`)
323
310
 
324
311
  **Type**: Reusable workflow
@@ -651,7 +638,6 @@ with:
651
638
  │ ├── claude-nightly-test-improvement.yml # Nightly test quality
652
639
  │ ├── claude-nightly-test-coverage.yml # Nightly test coverage
653
640
  │ ├── claude-nightly-code-complexity.yml # Nightly code complexity
654
- │ ├── auto-update-pr-branches.yml # Auto-update PRs from base
655
641
  │ └── .env.example # Secrets template
656
642
  └── k6/
657
643
  ├── scripts/ # Test scripts
@@ -26,6 +26,9 @@
26
26
  ".github/workflows/create-jira-issue-on-failure.yml",
27
27
  ".github/workflows/create-sentry-issue-on-failure.yml",
28
28
  ".github/workflows/reusable-auto-update-pr-branches.yml",
29
+ ".github/workflows/reusable-auto-update-pr-branches-dispatch.yml",
30
+ ".github/workflows/auto-update-pr-branches.yml",
31
+ ".github/workflows/auto-update-pr-branches-dispatch.yml",
29
32
  ".github/workflows/reusable-claude.yml",
30
33
  ".github/workflows/reusable-claude-ci-auto-fix.yml",
31
34
  ".github/workflows/reusable-claude-code-review-response.yml",
package/ui/index.html CHANGED
@@ -8755,24 +8755,6 @@
8755
8755
  { t: "mono", v: "pull_request" },
8756
8756
  { t: "text", v: "Deploy & environments" },
8757
8757
  ],
8758
- [
8759
- { t: "mono", v: "auto-update-pr-branches.yml" },
8760
- {
8761
- t: "text",
8762
- v: "Upkeep — keeps open PRs rebased on their base",
8763
- },
8764
- { t: "mono", v: "push · pull_request" },
8765
- { t: "text", v: "—" },
8766
- ],
8767
- [
8768
- { t: "mono", v: "auto-update-pr-branches-dispatch.yml" },
8769
- {
8770
- t: "text",
8771
- v: "Upkeep — on-demand handler for the same rebase, via repository dispatch",
8772
- },
8773
- { t: "mono", v: "repository_dispatch" },
8774
- { t: "text", v: "—" },
8775
- ],
8776
8758
  [
8777
8759
  { t: "mono", v: "zap-baseline.yml" },
8778
8760
  {
@@ -1,27 +0,0 @@
1
- # Seeded by Lisa on first setup — this file is YOURS.
2
- # Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
3
- # You can customize this file — Lisa will not overwrite it.
4
-
5
- name: Auto-update PR branches (Dispatch Handler)
6
-
7
- on:
8
- repository_dispatch:
9
- types:
10
- - auto-update-pr-branches
11
- - auto-update-pr-branch
12
-
13
- permissions:
14
- contents: write
15
- pull-requests: write
16
- id-token: write
17
-
18
- jobs:
19
- dispatch:
20
- uses: CodySwannGT/lisa/.github/workflows/reusable-auto-update-pr-branches-dispatch.yml@main
21
- with:
22
- action: ${{ github.event.action }}
23
- ref_name: ${{ github.event.client_payload.ref_name || '' }}
24
- repo_full_name: ${{ github.event.client_payload.repo_full_name }}
25
- pr_number: ${{ github.event.client_payload.pr_number || 0 }}
26
- secrets:
27
- CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
@@ -1,31 +0,0 @@
1
- # Seeded by Lisa on first setup — this file is YOURS.
2
- # Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
3
- # You can customize this file — Lisa will not overwrite it.
4
-
5
- name: Auto-update PR branches
6
-
7
- on:
8
- push:
9
- branches:
10
- - main
11
- - staging
12
- - dev
13
- pull_request:
14
- types: [opened, reopened, ready_for_review]
15
-
16
- permissions:
17
- contents: write
18
- pull-requests: write
19
- id-token: write
20
-
21
- jobs:
22
- autoupdate:
23
- uses: CodySwannGT/lisa/.github/workflows/reusable-auto-update-pr-branches.yml@main
24
- with:
25
- event_name: ${{ github.event_name }}
26
- ref_name: ${{ github.ref_name }}
27
- pr_base_ref: ${{ github.event.pull_request.base.ref || '' }}
28
- pr_number: ${{ github.event.pull_request.number || 0 }}
29
- repo_full_name: ${{ github.repository }}
30
- secrets:
31
- CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}