@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.
- package/cdk/create-only/.github/workflows/ci.yml +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- 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 +16 -20
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/workflows/deploy.yml +4 -4
- package/harper-fabric/create-only/.github/workflows/deploy.yml +1 -1
- package/harper-fabric/create-only/.github/workflows/zap-baseline.yml +1 -1
- package/nestjs/create-only/.github/workflows/deploy.yml +9 -9
- package/npm-package/create-only/.github/workflows/publish-to-npm.yml +1 -1
- package/package.json +5 -4
- 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/rails/create-only/.github/workflows/deploy.yml +3 -3
- package/scripts/check-third-party-action-pins.mjs +455 -0
- package/scripts/two-channel-couplings.json +2 -2
- package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +0 -14
- package/typescript/deletions.json +3 -0
- package/ui/index.html +0 -18
- package/typescript/create-only/.github/workflows/auto-update-pr-branches-dispatch.yml +0 -27
- 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":
|
|
8
|
-
"steps":
|
|
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 }}
|