@intentius/chant-lexicon-github 0.101.0 → 0.102.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/dist/action-pins.d.ts +53 -0
- package/dist/action-pins.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/composites/checkout.d.ts +7 -0
- package/dist/composites/checkout.d.ts.map +1 -1
- package/dist/composites/dependabot.d.ts.map +1 -1
- package/dist/composites/deploy-environment.d.ts.map +1 -1
- package/dist/composites/docker-build.d.ts.map +1 -1
- package/dist/composites/go-ci.d.ts.map +1 -1
- package/dist/composites/node-ci.d.ts.map +1 -1
- package/dist/composites/node-pipeline.d.ts +30 -30
- package/dist/composites/node-pipeline.d.ts.map +1 -1
- package/dist/composites/python-ci.d.ts.map +1 -1
- package/dist/composites/setup-node.d.ts +7 -0
- package/dist/composites/setup-node.d.ts.map +1 -1
- package/dist/expression.d.ts.map +1 -1
- package/dist/generated/index.d.ts +22 -22
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +7 -6
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +13 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/gha021.d.ts.map +1 -1
- package/dist/lint/post-synth/gha029.d.ts.map +1 -1
- package/dist/lint/post-synth/gha059.d.ts.map +1 -1
- package/dist/lint/post-synth/gha069.d.ts +33 -0
- package/dist/lint/post-synth/gha069.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/yaml-helpers.d.ts +20 -0
- package/dist/lint/post-synth/yaml-helpers.d.ts.map +1 -1
- package/dist/lint/rules/data/deprecated-versions.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/GHA069.md +16 -0
- package/dist/okf/types/Job.md +1 -0
- package/dist/okf/types/Workflow.md +1 -0
- package/dist/rules/gha021.ts +24 -24
- package/dist/rules/gha029.ts +3 -2
- package/dist/rules/gha059.ts +10 -2
- package/dist/rules/gha069.ts +111 -0
- package/dist/rules/yaml-helpers.ts +41 -47
- package/dist/variables.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/action-pins.test.ts +87 -0
- package/src/action-pins.ts +75 -0
- package/src/codegen/docs.ts +1 -1
- package/src/components/generate-op-pipeline.test.ts +4 -4
- package/src/components/generate-op-pipeline.ts +8 -7
- package/src/components/generate-pipeline.test.ts +1 -1
- package/src/components/generate-pipeline.ts +8 -14
- package/src/composites/checkout.ts +8 -1
- package/src/composites/composites.test.ts +4 -4
- package/src/composites/deploy-environment.ts +3 -2
- package/src/composites/docker-build.ts +2 -1
- package/src/composites/go-ci.ts +4 -3
- package/src/composites/node-ci.ts +3 -2
- package/src/composites/node-pipeline.ts +8 -7
- package/src/composites/python-ci.ts +3 -2
- package/src/composites/setup-node.ts +8 -1
- package/src/index.ts +4 -0
- package/src/lint/audit-catalog.ts +1 -0
- package/src/lint/audit-lineage.ts +13 -0
- package/src/lint/post-synth/fixtures/GHA069/negative-no-job-block.yml +16 -0
- package/src/lint/post-synth/fixtures/GHA069/negative-no-oidc-step.yml +22 -0
- package/src/lint/post-synth/fixtures/GHA069/negative.yml +21 -0
- package/src/lint/post-synth/fixtures/GHA069/positive.yml +20 -0
- package/src/lint/post-synth/gha011.test.ts +16 -0
- package/src/lint/post-synth/gha018.test.ts +18 -0
- package/src/lint/post-synth/gha019.test.ts +42 -0
- package/src/lint/post-synth/gha021.test.ts +18 -0
- package/src/lint/post-synth/gha021.ts +24 -24
- package/src/lint/post-synth/gha022.test.ts +15 -0
- package/src/lint/post-synth/gha023.test.ts +19 -0
- package/src/lint/post-synth/gha029.ts +3 -2
- package/src/lint/post-synth/gha038.test.ts +20 -0
- package/src/lint/post-synth/gha059.ts +10 -2
- package/src/lint/post-synth/gha069.test.ts +173 -0
- package/src/lint/post-synth/gha069.ts +111 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/yaml-helpers.test.ts +83 -0
- package/src/lint/post-synth/yaml-helpers.ts +41 -47
- package/src/lint/rules/data/deprecated-versions.ts +5 -2
- package/src/lint/rules/deprecated-action-version.test.ts +1 -1
- package/src/lint/rules/rules.test.ts +1 -1
- package/src/mcp/context-tools.test.ts +17 -0
- package/src/plugin.test.ts +1 -1
- package/src/serializer.ts +15 -4
|
@@ -37,66 +37,45 @@ export interface ParsedJob {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Extract jobs from a serialized GitHub Actions workflow YAML.
|
|
40
|
+
*
|
|
41
|
+
* Built on the structural parser ({@link jobEntries}), so every step of every
|
|
42
|
+
* job is read whatever key the step starts with, and `needs:` is read in all
|
|
43
|
+
* three forms (a scalar, a flow list, a block list). The line-based version
|
|
44
|
+
* this replaced stopped after the first line of a job's first step, so a step
|
|
45
|
+
* that starts with `name:` (the shape chant's serializer emits) had no `uses:`
|
|
46
|
+
* or `run:` and later steps were never seen (#3201).
|
|
47
|
+
*
|
|
48
|
+
* `run` is the whole script, block scalars included. `permissions` is not
|
|
49
|
+
* filled here; use {@link extractJobPermissions}.
|
|
40
50
|
*/
|
|
41
51
|
export function extractJobs(yaml: string): Map<string, ParsedJob> {
|
|
42
52
|
const jobs = new Map<string, ParsedJob>();
|
|
43
|
-
|
|
44
|
-
// Find the jobs: section — take everything after `jobs:\n`
|
|
45
|
-
const jobsIdx = yaml.search(/^jobs:\s*$/m);
|
|
46
|
-
if (jobsIdx === -1) return jobs;
|
|
47
|
-
|
|
48
|
-
// Content after the `jobs:` line. Stop at the next top-level key (non-indented) or EOF.
|
|
49
|
-
const afterJobs = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
|
|
50
|
-
const endMatch = afterJobs.search(/^[a-z]/m);
|
|
51
|
-
const jobsContent = endMatch === -1 ? afterJobs : afterJobs.slice(0, endMatch);
|
|
52
|
-
|
|
53
|
-
// Split into individual jobs by finding lines with exactly 2 spaces of indent followed by a key
|
|
54
|
-
const jobSections = jobsContent.split(/\n(?= [a-z][a-z0-9-]*:)/);
|
|
55
|
-
|
|
56
|
-
for (const section of jobSections) {
|
|
57
|
-
const nameMatch = section.match(/^\s{2}([a-z][a-z0-9-]*):/);
|
|
58
|
-
if (!nameMatch) continue;
|
|
59
|
-
|
|
60
|
-
const name = nameMatch[1];
|
|
53
|
+
for (const [name, jobObj] of jobEntries(yaml)) {
|
|
61
54
|
const job: ParsedJob = { name };
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const needsList = section.match(/^\s{4}needs:\n((?:\s{6}- .+\n?)+)/m);
|
|
69
|
-
if (needsList) {
|
|
70
|
-
job.needs = [];
|
|
71
|
-
for (const line of needsList[1].split("\n")) {
|
|
72
|
-
const item = line.match(/^\s{6}- (.+)$/);
|
|
73
|
-
if (item) job.needs.push(item[1].trim().replace(/^'|'$/g, "").replace(/^"|"$/g, ""));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
56
|
+
const needs = jobObj.needs;
|
|
57
|
+
if (typeof needs === "string") {
|
|
58
|
+
job.needs = [needs];
|
|
59
|
+
} else if (Array.isArray(needs)) {
|
|
60
|
+
job.needs = needs.filter((n) => typeof n === "string" || typeof n === "number").map(String);
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (stepsMatch) {
|
|
63
|
+
const steps = jobObj.steps;
|
|
64
|
+
if (Array.isArray(steps)) {
|
|
81
65
|
job.steps = [];
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
run: runMatch?.[1]?.trim(),
|
|
91
|
-
name: stepNameMatch?.[1]?.trim().replace(/^'|'$/g, ""),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
66
|
+
for (const raw of steps) {
|
|
67
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
68
|
+
const step = raw as Record<string, unknown>;
|
|
69
|
+
job.steps.push({
|
|
70
|
+
uses: typeof step.uses === "string" ? stripUsesComment(step.uses) : undefined,
|
|
71
|
+
run: typeof step.run === "string" ? step.run : undefined,
|
|
72
|
+
name: typeof step.name === "string" ? step.name : undefined,
|
|
73
|
+
});
|
|
94
74
|
}
|
|
95
75
|
}
|
|
96
76
|
|
|
97
77
|
jobs.set(name, job);
|
|
98
78
|
}
|
|
99
|
-
|
|
100
79
|
return jobs;
|
|
101
80
|
}
|
|
102
81
|
|
|
@@ -497,6 +476,21 @@ export function extractUsesComment(rawUses: string): string | undefined {
|
|
|
497
476
|
return comment ? comment : undefined;
|
|
498
477
|
}
|
|
499
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Every `uses:` line in the workflow text, in document order, with its
|
|
481
|
+
* trailing comment. The parsed document has no comments (core's parseYAML
|
|
482
|
+
* drops them, #3006), so a check that reads the pin label (GHA059) takes it
|
|
483
|
+
* from here.
|
|
484
|
+
*/
|
|
485
|
+
export function usesLineComments(yaml: string): Array<{ ref: string; comment: string | undefined }> {
|
|
486
|
+
const out: Array<{ ref: string; comment: string | undefined }> = [];
|
|
487
|
+
for (const line of yaml.split(/\r?\n/)) {
|
|
488
|
+
const m = line.match(/^\s*(?:-\s+)?uses:\s*(["']?)([^\s"'#]+)\1(\s+#.*)?\s*$/);
|
|
489
|
+
if (m) out.push({ ref: m[2], comment: m[3] ? extractUsesComment(m[3]) : undefined });
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
500
494
|
export function parseActionUses(rawUses: string): { owner: string; repo: string; slug: string; gitRef: string } | undefined {
|
|
501
495
|
const uses = stripUsesComment(rawUses);
|
|
502
496
|
if (uses.startsWith("./") || uses.startsWith("../") || uses.startsWith("docker://")) return undefined;
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* Deprecated action versions and recommended replacements.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { ACTION_PINS } from "../../../action-pins";
|
|
6
|
+
|
|
5
7
|
export const deprecatedVersions: Record<string, { deprecated: string[]; recommended: string }> = {
|
|
6
|
-
|
|
7
|
-
"actions/
|
|
8
|
+
// v4 runs on Node 20, which GitHub has deprecated for actions.
|
|
9
|
+
"actions/checkout": { deprecated: ["v1", "v2", "v3", "v4"], recommended: ACTION_PINS["actions/checkout"].major },
|
|
10
|
+
"actions/setup-node": { deprecated: ["v1", "v2", "v3", "v4"], recommended: ACTION_PINS["actions/setup-node"].major },
|
|
8
11
|
"actions/setup-go": { deprecated: ["v1", "v2", "v3", "v4"], recommended: "v5" },
|
|
9
12
|
"actions/setup-python": { deprecated: ["v1", "v2", "v3", "v4"], recommended: "v5" },
|
|
10
13
|
"actions/cache": { deprecated: ["v1", "v2", "v3"], recommended: "v4" },
|
|
@@ -19,7 +19,7 @@ describe("GHA012: deprecated-action-version", () => {
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
test("does not flag current version", () => {
|
|
22
|
-
const ctx = createContext(`const s = "actions/checkout@
|
|
22
|
+
const ctx = createContext(`const s = "actions/checkout@v7";`);
|
|
23
23
|
const diags = deprecatedActionVersionRule.check(ctx);
|
|
24
24
|
expect(diags).toHaveLength(0);
|
|
25
25
|
});
|
|
@@ -244,7 +244,7 @@ describe("GHA012: deprecated-action-version", () => {
|
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
test("does not flag current version", () => {
|
|
247
|
-
const ctx = createContext(`const s = "actions/checkout@
|
|
247
|
+
const ctx = createContext(`const s = "actions/checkout@v7";`);
|
|
248
248
|
const diags = deprecatedActionVersionRule.check(ctx);
|
|
249
249
|
expect(diags).toHaveLength(0);
|
|
250
250
|
});
|
|
@@ -33,6 +33,23 @@ describe("downstreamJobs / actionPinned (#327)", () => {
|
|
|
33
33
|
expect(downstreamJobs(yaml, "deploy")).toEqual([]);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
test("downstreamJobs follows a scalar needs: (#3201)", () => {
|
|
37
|
+
const yaml = `jobs:
|
|
38
|
+
build:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- name: Build
|
|
42
|
+
run: echo build
|
|
43
|
+
test:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
needs: build
|
|
46
|
+
steps:
|
|
47
|
+
- name: Test
|
|
48
|
+
run: echo test
|
|
49
|
+
`;
|
|
50
|
+
expect(downstreamJobs(yaml, "build")).toEqual(["test"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
36
53
|
test("actionPinned only accepts a full commit SHA", () => {
|
|
37
54
|
expect(actionPinned("actions/checkout@" + "a".repeat(40))).toBe(true);
|
|
38
55
|
expect(actionPinned("actions/checkout@v4")).toBe(false);
|
package/src/plugin.test.ts
CHANGED
|
@@ -24,7 +24,7 @@ describe("githubPlugin", () => {
|
|
|
24
24
|
|
|
25
25
|
test("provides post-synth checks", () => {
|
|
26
26
|
const checks = githubPlugin.postSynthChecks!();
|
|
27
|
-
expect(checks.length).toBe(
|
|
27
|
+
expect(checks.length).toBe(56);
|
|
28
28
|
|
|
29
29
|
const checkIds = checks.map((c) => c.id);
|
|
30
30
|
expect(checkIds).toContain("GHA006");
|
package/src/serializer.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
|
12
12
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
13
13
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
14
14
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
15
|
-
import {
|
|
15
|
+
import { emitYAMLEntry } from "@intentius/chant/yaml";
|
|
16
16
|
|
|
17
17
|
// ── Key conversion ────────────────────────────────────────────────
|
|
18
18
|
|
|
@@ -459,7 +459,7 @@ function emitYAMLDocument(doc: Record<string, unknown>): string {
|
|
|
459
459
|
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
460
460
|
sections.push(`${key}: ${yamlScalar(value)}`);
|
|
461
461
|
} else {
|
|
462
|
-
sections.push(
|
|
462
|
+
sections.push(emitYAMLEntry(key, value));
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
465
|
}
|
|
@@ -470,11 +470,22 @@ function emitYAMLDocument(doc: Record<string, unknown>): string {
|
|
|
470
470
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
471
471
|
sections.push(`${key}: ${yamlScalar(value)}`);
|
|
472
472
|
} else {
|
|
473
|
-
sections.push(
|
|
473
|
+
sections.push(emitYAMLEntry(key, value));
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
return sections.join("\n\n") + "\n";
|
|
477
|
+
return unquotePinComments(sections.join("\n\n") + "\n");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* A SHA-pinned `uses:` carries its version as a trailing comment
|
|
482
|
+
* (`actions/checkout@<sha> # v7.0.1`). The YAML emitter quotes any string with
|
|
483
|
+
* a `#`, which would make the comment part of the ref GitHub resolves. A ref
|
|
484
|
+
* holds no whitespace, so `<ref> # <label>` inside quotes is always this
|
|
485
|
+
* pattern, and writing it unquoted turns the label back into a comment.
|
|
486
|
+
*/
|
|
487
|
+
function unquotePinComments(yaml: string): string {
|
|
488
|
+
return yaml.replace(/^(\s*(?:- )?uses: )'([^\s']+) # ([^'\n]*)'$/gm, "$1$2 # $3");
|
|
478
489
|
}
|
|
479
490
|
|
|
480
491
|
function yamlScalar(value: string | number | boolean): string {
|