@intentius/chant-lexicon-github 0.124.31 → 0.124.32

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 (151) hide show
  1. package/dist/action-pins.js +53 -0
  2. package/dist/codegen/docs-cli.js +2 -0
  3. package/dist/codegen/docs.js +142 -0
  4. package/dist/codegen/generate-cli.js +26 -0
  5. package/dist/codegen/generate-lexicon.js +36 -0
  6. package/dist/codegen/generate-typescript.js +108 -0
  7. package/dist/codegen/generate.js +111 -0
  8. package/dist/codegen/naming.js +49 -0
  9. package/dist/codegen/package.js +40 -0
  10. package/dist/codegen/parse.js +595 -0
  11. package/dist/codegen/patches.js +36 -0
  12. package/dist/components/ci-jobs.js +84 -0
  13. package/dist/components/generate-op-pipeline.js +440 -0
  14. package/dist/components/generate-op-waves-pipeline.js +158 -0
  15. package/dist/components/generate-pipeline.d.ts.map +1 -1
  16. package/dist/components/generate-pipeline.js +476 -0
  17. package/dist/composites/cache.js +17 -0
  18. package/dist/composites/catalog.js +859 -0
  19. package/dist/composites/checkout.js +25 -0
  20. package/dist/composites/dependabot.js +43 -0
  21. package/dist/composites/deploy-environment.js +38 -0
  22. package/dist/composites/docker-build.js +70 -0
  23. package/dist/composites/download-artifact.js +18 -0
  24. package/dist/composites/go-ci.js +62 -0
  25. package/dist/composites/index.js +15 -0
  26. package/dist/composites/node-ci.js +44 -0
  27. package/dist/composites/node-pipeline.js +96 -0
  28. package/dist/composites/pr-plan-report.js +129 -0
  29. package/dist/composites/python-ci.js +60 -0
  30. package/dist/composites/setup-go.js +18 -0
  31. package/dist/composites/setup-node.js +21 -0
  32. package/dist/composites/setup-python.js +18 -0
  33. package/dist/composites/upload-artifact.js +19 -0
  34. package/dist/coverage.js +32 -0
  35. package/dist/expression.js +177 -0
  36. package/dist/generated/index.js +26 -0
  37. package/dist/generated/runtime.js +4 -0
  38. package/dist/graph-edges.js +174 -0
  39. package/dist/import/generator.js +108 -0
  40. package/dist/import/parser.js +69 -0
  41. package/dist/index.js +22 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/lint/audit-catalog.js +69 -0
  44. package/dist/lint/audit-lineage.js +168 -0
  45. package/dist/lint/post-synth/gha006.js +50 -0
  46. package/dist/lint/post-synth/gha009.js +35 -0
  47. package/dist/lint/post-synth/gha011.js +34 -0
  48. package/dist/lint/post-synth/gha013.js +40 -0
  49. package/dist/lint/post-synth/gha017.js +25 -0
  50. package/dist/lint/post-synth/gha018.js +33 -0
  51. package/dist/lint/post-synth/gha019.js +58 -0
  52. package/dist/lint/post-synth/gha021.js +42 -0
  53. package/dist/lint/post-synth/gha022.js +44 -0
  54. package/dist/lint/post-synth/gha023.js +37 -0
  55. package/dist/lint/post-synth/gha024.js +32 -0
  56. package/dist/lint/post-synth/gha025.js +33 -0
  57. package/dist/lint/post-synth/gha026.js +32 -0
  58. package/dist/lint/post-synth/gha027.js +46 -0
  59. package/dist/lint/post-synth/gha028.js +28 -0
  60. package/dist/lint/post-synth/gha029.js +56 -0
  61. package/dist/lint/post-synth/gha030.js +35 -0
  62. package/dist/lint/post-synth/gha031.js +75 -0
  63. package/dist/lint/post-synth/gha032.js +35 -0
  64. package/dist/lint/post-synth/gha033.js +39 -0
  65. package/dist/lint/post-synth/gha034.js +35 -0
  66. package/dist/lint/post-synth/gha035.js +50 -0
  67. package/dist/lint/post-synth/gha036.js +37 -0
  68. package/dist/lint/post-synth/gha037.js +41 -0
  69. package/dist/lint/post-synth/gha038.js +35 -0
  70. package/dist/lint/post-synth/gha039.js +34 -0
  71. package/dist/lint/post-synth/gha040.js +36 -0
  72. package/dist/lint/post-synth/gha041.js +32 -0
  73. package/dist/lint/post-synth/gha042.js +33 -0
  74. package/dist/lint/post-synth/gha043.js +35 -0
  75. package/dist/lint/post-synth/gha044.js +36 -0
  76. package/dist/lint/post-synth/gha045.js +33 -0
  77. package/dist/lint/post-synth/gha046.js +55 -0
  78. package/dist/lint/post-synth/gha047.js +44 -0
  79. package/dist/lint/post-synth/gha048.js +34 -0
  80. package/dist/lint/post-synth/gha049.js +36 -0
  81. package/dist/lint/post-synth/gha050.js +37 -0
  82. package/dist/lint/post-synth/gha051.js +38 -0
  83. package/dist/lint/post-synth/gha052.js +35 -0
  84. package/dist/lint/post-synth/gha053.js +34 -0
  85. package/dist/lint/post-synth/gha054.js +35 -0
  86. package/dist/lint/post-synth/gha055.js +44 -0
  87. package/dist/lint/post-synth/gha056.js +32 -0
  88. package/dist/lint/post-synth/gha057.js +35 -0
  89. package/dist/lint/post-synth/gha058.js +42 -0
  90. package/dist/lint/post-synth/gha059.js +119 -0
  91. package/dist/lint/post-synth/gha060.js +129 -0
  92. package/dist/lint/post-synth/gha061.js +78 -0
  93. package/dist/lint/post-synth/gha062.js +65 -0
  94. package/dist/lint/post-synth/gha063.js +56 -0
  95. package/dist/lint/post-synth/gha064.js +47 -0
  96. package/dist/lint/post-synth/gha065.js +125 -0
  97. package/dist/lint/post-synth/gha066.js +40 -0
  98. package/dist/lint/post-synth/gha067.js +75 -0
  99. package/dist/lint/post-synth/gha068.js +47 -0
  100. package/dist/lint/post-synth/gha069.js +99 -0
  101. package/dist/lint/post-synth/index.js +114 -0
  102. package/dist/lint/post-synth/yaml-helpers.js +465 -0
  103. package/dist/lint/rules/data/action-usage-policy.js +16 -0
  104. package/dist/lint/rules/data/advisory-feed.js +15 -0
  105. package/dist/lint/rules/data/deprecated-versions.js +14 -0
  106. package/dist/lint/rules/data/flagged-actions.js +18 -0
  107. package/dist/lint/rules/data/known-action-slugs.js +38 -0
  108. package/dist/lint/rules/data/known-actions.js +12 -0
  109. package/dist/lint/rules/data/preinstalled-tools.js +36 -0
  110. package/dist/lint/rules/data/recommended-inputs.js +9 -0
  111. package/dist/lint/rules/data/risky-features.js +22 -0
  112. package/dist/lint/rules/data/secret-patterns.js +30 -0
  113. package/dist/lint/rules/data/trusted-action-owners.js +12 -0
  114. package/dist/lint/rules/data/untrusted-contexts.js +44 -0
  115. package/dist/lint/rules/deprecated-action-version.js +44 -0
  116. package/dist/lint/rules/detect-secrets.js +45 -0
  117. package/dist/lint/rules/extract-inline-structs.js +55 -0
  118. package/dist/lint/rules/file-job-limit.js +43 -0
  119. package/dist/lint/rules/index.js +16 -0
  120. package/dist/lint/rules/job-timeout.js +53 -0
  121. package/dist/lint/rules/missing-recommended-inputs.js +51 -0
  122. package/dist/lint/rules/no-hardcoded-secrets.js +39 -0
  123. package/dist/lint/rules/no-raw-expressions.js +44 -0
  124. package/dist/lint/rules/suggest-cache.js +60 -0
  125. package/dist/lint/rules/use-condition-builders.js +37 -0
  126. package/dist/lint/rules/use-matrix-builder.js +36 -0
  127. package/dist/lint/rules/use-typed-actions.js +41 -0
  128. package/dist/lint/rules/validate-concurrency.js +59 -0
  129. package/dist/lsp/completions.d.ts.map +1 -1
  130. package/dist/lsp/completions.js +18 -0
  131. package/dist/lsp/hover.d.ts.map +1 -1
  132. package/dist/lsp/hover.js +33 -0
  133. package/dist/manifest.json +1 -1
  134. package/dist/mcp/context-tools.js +152 -0
  135. package/dist/package-cli.js +20 -0
  136. package/dist/plugin.js +344 -0
  137. package/dist/serializer.js +572 -0
  138. package/dist/spec/fetch.js +46 -0
  139. package/dist/validate-cli.js +16 -0
  140. package/dist/validate.js +38 -0
  141. package/dist/variables.js +41 -0
  142. package/package.json +6 -6
  143. package/src/codegen/package.ts +2 -2
  144. package/src/components/__fixtures__/pr-loop-build.github.golden.yml +6 -2
  145. package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +178 -0
  146. package/src/components/__fixtures__/pr-loop.github.golden.yml +6 -2
  147. package/src/components/generate-pipeline.ts +66 -29
  148. package/src/components/generate-pr-pipeline.test.ts +44 -20
  149. package/src/components/generate-pr-waves-pipeline.test.ts +117 -0
  150. package/src/lsp/completions.ts +2 -1
  151. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Helpers for parsing serialized GitHub Actions YAML in post-synth checks.
3
+ */
4
+ import { parseYAML } from "@intentius/chant/yaml";
5
+ export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
6
+ /**
7
+ * Return the emitted `.github/dependabot.yml` content from a serializer output,
8
+ * if present (it rides as an additional file alongside the workflow output).
9
+ */
10
+ export function getDependabotYaml(output) {
11
+ if (typeof output === "string")
12
+ return undefined;
13
+ return output.files?.["dependabot.yml"];
14
+ }
15
+ /** Parse the `updates:` array from a dependabot.yml document. */
16
+ export function extractDependabotUpdates(dependabotYaml) {
17
+ let doc;
18
+ try {
19
+ doc = parseYAML(dependabotYaml);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ const updates = doc.updates;
25
+ return Array.isArray(updates) ? updates : [];
26
+ }
27
+ /**
28
+ * Extract jobs from a serialized GitHub Actions workflow YAML.
29
+ *
30
+ * Built on the structural parser ({@link jobEntries}), so every step of every
31
+ * job is read whatever key the step starts with, and `needs:` is read in all
32
+ * three forms (a scalar, a flow list, a block list). The line-based version
33
+ * this replaced stopped after the first line of a job's first step, so a step
34
+ * that starts with `name:` (the shape chant's serializer emits) had no `uses:`
35
+ * or `run:` and later steps were never seen (#3201).
36
+ *
37
+ * `run` is the whole script, block scalars included. `permissions` is not
38
+ * filled here; use {@link extractJobPermissions}.
39
+ */
40
+ export function extractJobs(yaml) {
41
+ const jobs = new Map();
42
+ for (const [name, jobObj] of jobEntries(yaml)) {
43
+ const job = { name };
44
+ const needs = jobObj.needs;
45
+ if (typeof needs === "string") {
46
+ job.needs = [needs];
47
+ }
48
+ else if (Array.isArray(needs)) {
49
+ job.needs = needs.filter((n) => typeof n === "string" || typeof n === "number").map(String);
50
+ }
51
+ const steps = jobObj.steps;
52
+ if (Array.isArray(steps)) {
53
+ job.steps = [];
54
+ for (const raw of steps) {
55
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
56
+ continue;
57
+ const step = raw;
58
+ job.steps.push({
59
+ uses: typeof step.uses === "string" ? stripUsesComment(step.uses) : undefined,
60
+ run: typeof step.run === "string" ? step.run : undefined,
61
+ name: typeof step.name === "string" ? step.name : undefined,
62
+ });
63
+ }
64
+ }
65
+ jobs.set(name, job);
66
+ }
67
+ return jobs;
68
+ }
69
+ /**
70
+ * Extract trigger events from the YAML.
71
+ */
72
+ export function extractTriggers(yaml) {
73
+ const onMatch = yaml.match(/^on:\n([\s\S]*?)(?=\n[a-z]|$)/m);
74
+ if (!onMatch)
75
+ return {};
76
+ const triggers = {};
77
+ const lines = onMatch[1].split("\n");
78
+ for (const line of lines) {
79
+ const triggerMatch = line.match(/^\s{2}([a-z_]+):/);
80
+ if (triggerMatch) {
81
+ triggers[triggerMatch[1]] = true;
82
+ }
83
+ }
84
+ return triggers;
85
+ }
86
+ /**
87
+ * Check if any step in the list uses a checkout action.
88
+ */
89
+ export function hasCheckoutAction(steps) {
90
+ return steps.some((s) => s.uses?.startsWith("actions/checkout"));
91
+ }
92
+ /**
93
+ * Build a needs dependency graph from the YAML.
94
+ */
95
+ export function buildNeedsGraph(yaml) {
96
+ const jobs = extractJobs(yaml);
97
+ const graph = new Map();
98
+ for (const [name, job] of jobs) {
99
+ graph.set(name, job.needs ?? []);
100
+ }
101
+ return graph;
102
+ }
103
+ /**
104
+ * Extract the workflow name from the YAML.
105
+ */
106
+ export function extractWorkflowName(yaml) {
107
+ const match = yaml.match(/^name:\s+(.+)$/m);
108
+ return match?.[1]?.trim().replace(/^'|'$/g, "");
109
+ }
110
+ /**
111
+ * Check if the YAML has an explicit permissions block.
112
+ */
113
+ export function hasPermissions(yaml) {
114
+ return /^permissions:/m.test(yaml);
115
+ }
116
+ /** Parse a workflow YAML into a structured document, tolerant of parse errors. */
117
+ export function parseDoc(yaml) {
118
+ try {
119
+ return parseYAML(yaml);
120
+ }
121
+ catch {
122
+ return undefined;
123
+ }
124
+ }
125
+ /** Every job in the workflow as `[name, rawJobObject]`, via the structural parser. */
126
+ export function jobEntries(yaml) {
127
+ const doc = parseDoc(yaml);
128
+ const jobs = doc?.jobs;
129
+ if (!jobs || typeof jobs !== "object")
130
+ return [];
131
+ const out = [];
132
+ for (const [name, val] of Object.entries(jobs)) {
133
+ if (val && typeof val === "object" && !Array.isArray(val)) {
134
+ out.push([name, val]);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /**
140
+ * Every step object (raw, as parsed from YAML — `uses`, `with`, `id`, `run`,
141
+ * `env`, etc.) grouped by owning job. Unlike {@link extractJobs} (which only
142
+ * lifts `uses`/`run`/`name`), this hands back each step's full object so a
143
+ * check can read arbitrary keys (`with:` inputs, `id:`) without growing a new
144
+ * bespoke regex extractor per key. See GHA060.
145
+ */
146
+ export function extractStepsByJob(yaml) {
147
+ const out = new Map();
148
+ for (const [job, jobObj] of jobEntries(yaml)) {
149
+ const steps = jobObj.steps;
150
+ if (!Array.isArray(steps))
151
+ continue;
152
+ out.set(job, steps.filter((s) => !!s && typeof s === "object" && !Array.isArray(s)));
153
+ }
154
+ return out;
155
+ }
156
+ /**
157
+ * Extract every `uses:` reference from the workflow — both step-level actions
158
+ * and job-level reusable-workflow calls. Uses the structural YAML parser so
159
+ * nested jobs/steps are handled reliably.
160
+ */
161
+ export function extractActionRefs(yaml) {
162
+ const refs = [];
163
+ for (const [job, jobObj] of jobEntries(yaml)) {
164
+ if (typeof jobObj.uses === "string") {
165
+ refs.push({ job, ref: jobObj.uses, level: "job" });
166
+ }
167
+ const steps = jobObj.steps;
168
+ if (Array.isArray(steps)) {
169
+ for (const step of steps) {
170
+ if (step && typeof step === "object" && typeof step.uses === "string") {
171
+ refs.push({ job, ref: step.uses, level: "step" });
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return refs;
177
+ }
178
+ /**
179
+ * Extract every container/service/`docker://` image reference from the workflow.
180
+ */
181
+ export function extractImageRefs(yaml) {
182
+ const refs = [];
183
+ for (const [job, jobObj] of jobEntries(yaml)) {
184
+ const container = jobObj.container;
185
+ if (typeof container === "string") {
186
+ refs.push({ job, image: container, source: "container" });
187
+ }
188
+ else if (container && typeof container === "object" && typeof container.image === "string") {
189
+ refs.push({ job, image: container.image, source: "container" });
190
+ }
191
+ const services = jobObj.services;
192
+ if (services && typeof services === "object" && !Array.isArray(services)) {
193
+ for (const svc of Object.values(services)) {
194
+ if (typeof svc === "string") {
195
+ refs.push({ job, image: svc, source: "service" });
196
+ }
197
+ else if (svc && typeof svc === "object" && typeof svc.image === "string") {
198
+ refs.push({ job, image: svc.image, source: "service" });
199
+ }
200
+ }
201
+ }
202
+ const steps = jobObj.steps;
203
+ if (Array.isArray(steps)) {
204
+ for (const step of steps) {
205
+ const uses = step && typeof step === "object" ? step.uses : undefined;
206
+ if (typeof uses === "string" && uses.startsWith("docker://")) {
207
+ refs.push({ job, image: uses.slice("docker://".length), source: "step" });
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return refs;
213
+ }
214
+ /**
215
+ * Walk the `jobs:` block line-by-line, tracking the current job. Robust to `|`
216
+ * block scalars (which the structural parser does not handle), so it is the
217
+ * right tool for scanning `run:`/`if:`/`runs-on:` content.
218
+ */
219
+ function scanJobLines(yaml, onLine) {
220
+ const lines = yaml.split("\n");
221
+ let inJobs = false;
222
+ let currentJob = "";
223
+ for (let i = 0; i < lines.length; i++) {
224
+ const line = lines[i];
225
+ if (/^jobs:\s*$/.test(line)) {
226
+ inJobs = true;
227
+ continue;
228
+ }
229
+ if (!inJobs)
230
+ continue;
231
+ if (/^\S/.test(line)) {
232
+ inJobs = false;
233
+ continue;
234
+ } // dedented to a new top-level key
235
+ const jobHeader = line.match(/^ {2}([A-Za-z0-9_-]+):\s*$/);
236
+ if (jobHeader) {
237
+ currentJob = jobHeader[1];
238
+ continue;
239
+ }
240
+ if (!currentJob)
241
+ continue;
242
+ const indent = line.search(/\S/);
243
+ if (indent < 0)
244
+ continue;
245
+ onLine(currentJob, line, indent, i, lines);
246
+ }
247
+ }
248
+ /**
249
+ * Extract every `run:` script (inline and `|`/`>` block scalars) with its
250
+ * owning job.
251
+ */
252
+ export function extractRunBlocks(yaml) {
253
+ const out = [];
254
+ const consumed = new Set();
255
+ scanJobLines(yaml, (job, line, _indent, lineNo, lines) => {
256
+ if (consumed.has(lineNo))
257
+ return;
258
+ const m = line.match(/^(\s*)(- )?run:\s*(.*)$/);
259
+ if (!m)
260
+ return;
261
+ const keyIndent = m[1].length + (m[2] ? 2 : 0); // column where `run:` begins
262
+ const value = m[3].trim();
263
+ if (value === "|" || value === "|-" || value === ">" || value === ">-" || value === "|+" || value === ">+") {
264
+ const blockLines = [];
265
+ for (let j = lineNo + 1; j < lines.length; j++) {
266
+ const bl = lines[j];
267
+ if (bl.trim() === "") {
268
+ blockLines.push("");
269
+ consumed.add(j);
270
+ continue;
271
+ }
272
+ const bi = bl.search(/\S/);
273
+ if (bi <= keyIndent)
274
+ break;
275
+ blockLines.push(bl.trimStart());
276
+ consumed.add(j);
277
+ }
278
+ out.push({ job, run: blockLines.join("\n") });
279
+ }
280
+ else {
281
+ out.push({ job, run: value.replace(/^['"]|['"]$/g, "") });
282
+ }
283
+ });
284
+ return out;
285
+ }
286
+ /** Extract every `if:` condition (job-level and step-level) with its job. */
287
+ export function extractIfConditions(yaml) {
288
+ const out = [];
289
+ scanJobLines(yaml, (job, line) => {
290
+ const m = line.match(/^\s+(?:- )?if:\s*(.+)$/);
291
+ if (m)
292
+ out.push({ job, expr: m[1].trim().replace(/^['"]|['"]$/g, "") });
293
+ });
294
+ return out;
295
+ }
296
+ /** Extract each job's `runs-on:` value(s) as a flat list of labels. */
297
+ export function extractRunsOnByJob(yaml) {
298
+ const out = new Map();
299
+ scanJobLines(yaml, (job, line, indent, lineNo, lines) => {
300
+ const m = line.match(/^\s+runs-on:\s*(.*)$/);
301
+ if (!m)
302
+ return;
303
+ const value = m[1].trim();
304
+ const labels = [];
305
+ if (value === "") {
306
+ for (let j = lineNo + 1; j < lines.length; j++) {
307
+ const item = lines[j].match(/^\s+- (.+)$/);
308
+ if (!item)
309
+ break;
310
+ labels.push(item[1].trim().replace(/^['"]|['"]$/g, ""));
311
+ }
312
+ }
313
+ else if (value.startsWith("[")) {
314
+ for (const part of value.replace(/^\[|\]$/g, "").split(",")) {
315
+ const t = part.trim().replace(/^['"]|['"]$/g, "");
316
+ if (t)
317
+ labels.push(t);
318
+ }
319
+ }
320
+ else {
321
+ labels.push(value.replace(/^['"]|['"]$/g, ""));
322
+ }
323
+ out.set(job, labels);
324
+ });
325
+ return out;
326
+ }
327
+ /** Pull every `${{ ... }}` expression body out of a string. */
328
+ export function extractExpressions(text) {
329
+ const out = [];
330
+ const re = /\$\{\{(.+?)\}\}/g;
331
+ let m;
332
+ while ((m = re.exec(text)) !== null) {
333
+ out.push(m[1].trim());
334
+ }
335
+ return out;
336
+ }
337
+ /** Every line inside the `jobs:` block paired with its owning job name. */
338
+ export function jobLines(yaml) {
339
+ const out = [];
340
+ scanJobLines(yaml, (job, line) => out.push({ job, line }));
341
+ return out;
342
+ }
343
+ /** All lines inside the `jobs:` block grouped by owning job. */
344
+ export function linesByJob(yaml) {
345
+ const out = new Map();
346
+ scanJobLines(yaml, (job, line) => {
347
+ const arr = out.get(job) ?? [];
348
+ arr.push(line);
349
+ out.set(job, arr);
350
+ });
351
+ return out;
352
+ }
353
+ /** Set of jobs that declare an `environment:`. */
354
+ export function extractJobEnvironments(yaml) {
355
+ const set = new Set();
356
+ scanJobLines(yaml, (job, line) => {
357
+ if (/^\s+environment:/.test(line))
358
+ set.add(job);
359
+ });
360
+ return set;
361
+ }
362
+ /** Set of jobs whose body references a `secrets.` context. */
363
+ export function jobsReferencingSecrets(yaml) {
364
+ const set = new Set();
365
+ scanJobLines(yaml, (job, line) => {
366
+ if (/secrets\./.test(line))
367
+ set.add(job);
368
+ });
369
+ return set;
370
+ }
371
+ /** The workflow-level `permissions:` value, if present. */
372
+ export function extractWorkflowPermissions(yaml) {
373
+ const doc = parseDoc(yaml);
374
+ const perms = doc?.permissions;
375
+ if (typeof perms === "string")
376
+ return perms;
377
+ if (perms && typeof perms === "object" && !Array.isArray(perms))
378
+ return perms;
379
+ return undefined;
380
+ }
381
+ /** Per-job `permissions:` values, keyed by job name. Only jobs that declare one. */
382
+ export function extractJobPermissions(yaml) {
383
+ const out = new Map();
384
+ for (const [job, jobObj] of jobEntries(yaml)) {
385
+ const perms = jobObj.permissions;
386
+ if (typeof perms === "string")
387
+ out.set(job, perms);
388
+ else if (perms && typeof perms === "object" && !Array.isArray(perms))
389
+ out.set(job, perms);
390
+ }
391
+ return out;
392
+ }
393
+ /**
394
+ * Classify a permissions value's write surface.
395
+ * `writeAll` is the `write-all` blanket preset; `scopes` lists the individual
396
+ * scopes granted `write` in map form.
397
+ */
398
+ export function writeSurface(perms) {
399
+ if (typeof perms === "string") {
400
+ return { writeAll: perms === "write-all", scopes: [] };
401
+ }
402
+ const scopes = [];
403
+ for (const [scope, level] of Object.entries(perms)) {
404
+ if (level === "write")
405
+ scopes.push(scope);
406
+ }
407
+ return { writeAll: false, scopes };
408
+ }
409
+ /** True if the permissions value grants any write access (blanket or scoped). */
410
+ export function grantsWrite(perms) {
411
+ const { writeAll, scopes } = writeSurface(perms);
412
+ return writeAll || scopes.length > 0;
413
+ }
414
+ /**
415
+ * Split an action `uses:` value into its `owner/repo` slug and git ref.
416
+ * Returns undefined for local (`./`, `../`) and `docker://` references, which
417
+ * are not GitHub action repository references.
418
+ */
419
+ /**
420
+ * Strip a trailing inline YAML comment from a `uses:` value. A `uses` ref
421
+ * cannot contain whitespace, so `<ref> # v1.2.3` (the recommended SHA-pin
422
+ * style) is safe to trim back to `<ref>`.
423
+ */
424
+ export function stripUsesComment(uses) {
425
+ return uses.replace(/\s+#.*$/, "").trim();
426
+ }
427
+ /**
428
+ * Extract a `uses:` value's trailing inline comment, if any — the
429
+ * human-readable version label reviewers rely on when a ref is pinned to an
430
+ * opaque commit SHA (e.g. `actions/setup-node@1a2b…9a0b # v4.0.2` → `v4.0.2`).
431
+ * Returns undefined for no comment or an empty (`# ` with nothing after it)
432
+ * one, since both mean "no annotation" to a reviewer. See GHA059.
433
+ */
434
+ export function extractUsesComment(rawUses) {
435
+ const m = rawUses.match(/\s+#\s*(.*)$/);
436
+ const comment = m?.[1]?.trim();
437
+ return comment ? comment : undefined;
438
+ }
439
+ /**
440
+ * Every `uses:` line in the workflow text, in document order, with its
441
+ * trailing comment. The parsed document has no comments (core's parseYAML
442
+ * drops them, #3006), so a check that reads the pin label (GHA059) takes it
443
+ * from here.
444
+ */
445
+ export function usesLineComments(yaml) {
446
+ const out = [];
447
+ for (const line of yaml.split(/\r?\n/)) {
448
+ const m = line.match(/^\s*(?:-\s+)?uses:\s*(["']?)([^\s"'#]+)\1(\s+#.*)?\s*$/);
449
+ if (m)
450
+ out.push({ ref: m[2], comment: m[3] ? extractUsesComment(m[3]) : undefined });
451
+ }
452
+ return out;
453
+ }
454
+ export function parseActionUses(rawUses) {
455
+ const uses = stripUsesComment(rawUses);
456
+ if (uses.startsWith("./") || uses.startsWith("../") || uses.startsWith("docker://"))
457
+ return undefined;
458
+ const at = uses.lastIndexOf("@");
459
+ const path = at === -1 ? uses : uses.slice(0, at);
460
+ const gitRef = at === -1 ? "" : uses.slice(at + 1);
461
+ const segments = path.split("/");
462
+ if (segments.length < 2 || !segments[0] || !segments[1])
463
+ return undefined;
464
+ return { owner: segments[0], repo: segments[1], slug: `${segments[0]}/${segments[1]}`, gitRef };
465
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Default action-usage policy for GHA061 — opt-in, so it ships with an empty
3
+ * allow/deny list: with nothing configured, GHA061 reports nothing (#445
4
+ * acceptance criteria: "silent when unconfigured").
5
+ *
6
+ * `PostSynthContext` carries no per-check runtime config channel today (see
7
+ * `trusted-action-owners.ts`'s doc for the same constraint), so — like that
8
+ * file — this is a vendored, code-level default. A project that wants a real
9
+ * policy doesn't edit this file: it authors a `lint.policies` entry
10
+ * (`chant.config.ts`'s `lint.policies`, see `packages/core/src/lint/config.ts`)
11
+ * that imports `evaluateUsagePolicy` from `../../post-synth/gha061` and calls
12
+ * it with its own `ActionUsagePolicy`, wrapped in a `PostSynthCheck`. That is
13
+ * the supported opt-in path; this default only documents the "nothing
14
+ * configured" shape and is what the shipped GHA061 check itself passes.
15
+ */
16
+ export const DEFAULT_ACTION_USAGE_POLICY = {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default advisory feed for GHA062 — ships empty, so the check degrades to
3
+ * "no findings" rather than erroring when nothing is supplied (#445
4
+ * acceptance criteria: "degrades gracefully when the feed is unreachable").
5
+ *
6
+ * Per epic #350's design spine, the pure core never fetches: a live feed is
7
+ * the CALLER's concern (a `chant audit` CLI flag reading a local JSON file,
8
+ * a pre-fetched structure a hosted service passes in) — never a `fetch()`
9
+ * inside a rule. This module documents the "nothing supplied" shape and is
10
+ * what the shipped GHA062 check itself passes; a caller with a real feed
11
+ * calls `checkAdvisories` (`../../post-synth/gha062`) directly with its own
12
+ * `AdvisoryFeed`, the same way a `lint.policies` entry supplies GHA061 its
13
+ * own `ActionUsagePolicy` (see `action-usage-policy.ts`).
14
+ */
15
+ export const DEFAULT_ADVISORY_FEED = { entries: [] };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Deprecated action versions and recommended replacements.
3
+ */
4
+ import { ACTION_PINS } from "../../../action-pins.js";
5
+ export const deprecatedVersions = {
6
+ // v4 runs on Node 20, which GitHub has deprecated for actions.
7
+ "actions/checkout": { deprecated: ["v1", "v2", "v3", "v4"], recommended: ACTION_PINS["actions/checkout"].major },
8
+ "actions/setup-node": { deprecated: ["v1", "v2", "v3", "v4"], recommended: ACTION_PINS["actions/setup-node"].major },
9
+ "actions/setup-go": { deprecated: ["v1", "v2", "v3", "v4"], recommended: "v5" },
10
+ "actions/setup-python": { deprecated: ["v1", "v2", "v3", "v4"], recommended: "v5" },
11
+ "actions/cache": { deprecated: ["v1", "v2", "v3"], recommended: "v4" },
12
+ "actions/upload-artifact": { deprecated: ["v1", "v2", "v3"], recommended: "v4" },
13
+ "actions/download-artifact": { deprecated: ["v1", "v2", "v3"], recommended: "v4" },
14
+ };
@@ -0,0 +1,18 @@
1
+ export const FLAGGED_ACTIONS = {
2
+ "tj-actions/changed-files": {
3
+ reason: "supply-chain compromise disclosed March 2025 (CVE-2025-30066) — tags were repointed to leak CI secrets",
4
+ remediation: "pin to a vetted commit SHA from before the compromise, or migrate to an audited alternative",
5
+ },
6
+ "actions/setup-ruby": {
7
+ reason: "archived and unmaintained",
8
+ remediation: "use ruby/setup-ruby",
9
+ },
10
+ "actions/create-release": {
11
+ reason: "archived and unmaintained",
12
+ remediation: "use softprops/action-gh-release or the gh CLI",
13
+ },
14
+ "actions/upload-release-asset": {
15
+ reason: "archived and unmaintained",
16
+ remediation: "use softprops/action-gh-release or the gh CLI",
17
+ },
18
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Vendored snapshot of widely-used GitHub Action `owner/repo` slugs.
3
+ *
4
+ * Used by the look-alike check (GHA031) as the reference set: a `uses:` slug
5
+ * that is a near-miss (edit distance 1–2) of one of these — but not an exact
6
+ * match — is likely a typo or an impersonation of the popular action.
7
+ *
8
+ * Advisory only. Refresh source: the GitHub Marketplace "most-used" actions and
9
+ * the actions/* org. Accept staleness — regenerate periodically by editing this
10
+ * list; a stale entry only weakens look-alike detection, it never blocks a build.
11
+ */
12
+ export const KNOWN_ACTION_SLUGS = [
13
+ "actions/checkout",
14
+ "actions/setup-node",
15
+ "actions/setup-python",
16
+ "actions/setup-go",
17
+ "actions/setup-java",
18
+ "actions/setup-dotnet",
19
+ "actions/cache",
20
+ "actions/upload-artifact",
21
+ "actions/download-artifact",
22
+ "actions/github-script",
23
+ "actions/stale",
24
+ "actions/labeler",
25
+ "actions/dependency-review-action",
26
+ "docker/build-push-action",
27
+ "docker/login-action",
28
+ "docker/setup-buildx-action",
29
+ "docker/setup-qemu-action",
30
+ "docker/metadata-action",
31
+ "aws-actions/configure-aws-credentials",
32
+ "azure/login",
33
+ "google-github-actions/auth",
34
+ "hashicorp/setup-terraform",
35
+ "codecov/codecov-action",
36
+ "softprops/action-gh-release",
37
+ "peter-evans/create-pull-request",
38
+ ];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Registry of well-known GitHub Actions and their typed composite wrappers.
3
+ */
4
+ export const knownActions = {
5
+ "actions/checkout": { composite: "Checkout", importPath: "@intentius/chant-lexicon-github" },
6
+ "actions/setup-node": { composite: "SetupNode", importPath: "@intentius/chant-lexicon-github" },
7
+ "actions/setup-go": { composite: "SetupGo", importPath: "@intentius/chant-lexicon-github" },
8
+ "actions/setup-python": { composite: "SetupPython", importPath: "@intentius/chant-lexicon-github" },
9
+ "actions/cache": { composite: "Cache", importPath: "@intentius/chant-lexicon-github" },
10
+ "actions/upload-artifact": { composite: "UploadArtifact", importPath: "@intentius/chant-lexicon-github" },
11
+ "actions/download-artifact": { composite: "DownloadArtifact", importPath: "@intentius/chant-lexicon-github" },
12
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Vendored snapshot of tools already present on GitHub-hosted runner images,
3
+ * used by GHA056 to flag redundant runtime installs that add third-party
4
+ * surface for no benefit.
5
+ *
6
+ * Advisory. Refresh source: the runner-images repository's installed-software
7
+ * manifests. Conservative on purpose — only tools that are reliably present
8
+ * across the common ubuntu/macos/windows images are listed.
9
+ */
10
+ export const PREINSTALLED_TOOLS = [
11
+ "git",
12
+ "curl",
13
+ "wget",
14
+ "jq",
15
+ "make",
16
+ "gcc",
17
+ "g++",
18
+ "zip",
19
+ "unzip",
20
+ "tar",
21
+ "docker",
22
+ "docker-compose",
23
+ "python3",
24
+ "pip3",
25
+ "node",
26
+ "npm",
27
+ "yarn",
28
+ "go",
29
+ "gh",
30
+ "aws",
31
+ "az",
32
+ "gcloud",
33
+ "kubectl",
34
+ "helm",
35
+ "terraform",
36
+ ];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Recommended inputs for setup action composites.
3
+ * If a setup action is used without any of the listed inputs, a warning is raised.
4
+ */
5
+ export const recommendedInputs = {
6
+ SetupNode: ["nodeVersion", "node-version"],
7
+ SetupGo: ["goVersion", "go-version"],
8
+ SetupPython: ["pythonVersion", "python-version"],
9
+ };
@@ -0,0 +1,22 @@
1
+ export const RISKY_FEATURES = [
2
+ {
3
+ pattern: /::set-output\s/,
4
+ label: "deprecated ::set-output:: workflow command",
5
+ advice: "write to $GITHUB_OUTPUT instead",
6
+ },
7
+ {
8
+ pattern: /::save-state\s/,
9
+ label: "deprecated ::save-state:: workflow command",
10
+ advice: "write to $GITHUB_STATE instead",
11
+ },
12
+ {
13
+ pattern: /ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION/,
14
+ label: "opt-in to an end-of-life Node.js runtime",
15
+ advice: "upgrade the action to a supported Node runtime instead of forcing the unsupported one",
16
+ },
17
+ {
18
+ pattern: /::add-mask::\$\{\{/,
19
+ label: "masking a dynamic value after it may have already been logged",
20
+ advice: "mask secrets at their source; ::add-mask:: on an interpolated value can leak before the mask applies",
21
+ },
22
+ ];