@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
|
@@ -72,7 +72,7 @@ describe("generateGithubOpPipeline: one file per scheduled Op", () => {
|
|
|
72
72
|
const job = doc.jobs!["actions-audit"];
|
|
73
73
|
|
|
74
74
|
expect(job["runs-on"]).toBe("ubuntu-latest");
|
|
75
|
-
expect(job.steps[0].uses).toBe("actions/checkout@
|
|
75
|
+
expect(job.steps[0].uses).toBe("actions/checkout@v7");
|
|
76
76
|
const runStep = job.steps.find((s) => typeof s.run === "string")!;
|
|
77
77
|
expect(runStep.run).toBe("chant run actions-audit");
|
|
78
78
|
});
|
|
@@ -327,7 +327,7 @@ describe("generateGithubOpPipeline: setup steps and additive permissions (#2242)
|
|
|
327
327
|
const steps = doc.jobs!["app-apply"].steps;
|
|
328
328
|
|
|
329
329
|
expect(steps.slice(0, 3).map((s) => s.uses ?? s.run)).toEqual([
|
|
330
|
-
"actions/checkout@
|
|
330
|
+
"actions/checkout@v7",
|
|
331
331
|
"aws-actions/configure-aws-credentials@v6",
|
|
332
332
|
"install terraform",
|
|
333
333
|
]);
|
|
@@ -385,7 +385,7 @@ describe("generateGithubOpPipeline: setup steps and additive permissions (#2242)
|
|
|
385
385
|
];
|
|
386
386
|
const steps = parseFile(generateGithubOpPipeline(specs).files[0].yaml).jobs!["app-apply"].steps;
|
|
387
387
|
expect(steps.map((s) => s.uses).filter(Boolean)).toEqual([
|
|
388
|
-
"actions/checkout@
|
|
388
|
+
"actions/checkout@v7",
|
|
389
389
|
"github/codeql-action/upload-sarif@v4",
|
|
390
390
|
"aws-actions/configure-aws-credentials@0e613a0980cbf65ed5b322eb7a1e075d28913a83",
|
|
391
391
|
]);
|
|
@@ -622,7 +622,7 @@ describe("generateGithubOpPipeline: a deployment environment on the Op's job (#2
|
|
|
622
622
|
" runs-on: ubuntu-latest",
|
|
623
623
|
" container: node:22-slim",
|
|
624
624
|
" steps:",
|
|
625
|
-
" - uses: actions/checkout@
|
|
625
|
+
" - uses: actions/checkout@v7",
|
|
626
626
|
" - run: chant run actions-audit",
|
|
627
627
|
" env:",
|
|
628
628
|
" GITHUB_TOKEN: '${{ github.token }}'",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
* that job either, same as `environment:` above does not).
|
|
71
71
|
*/
|
|
72
72
|
|
|
73
|
-
import { emitYAML } from "@intentius/chant/yaml";
|
|
73
|
+
import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
|
|
74
74
|
import { memberPathFilter, resolveOpTrigger } from "@intentius/chant/lexicon";
|
|
75
75
|
import type {
|
|
76
76
|
ComponentPipelineOptions as GenerateGithubOpOptions,
|
|
@@ -83,6 +83,7 @@ import type {
|
|
|
83
83
|
PipelineMember,
|
|
84
84
|
ScheduledOpSpec,
|
|
85
85
|
} from "@intentius/chant/lexicon";
|
|
86
|
+
import { actionRef } from "../action-pins";
|
|
86
87
|
|
|
87
88
|
export type { GenerateGithubOpOptions, GenerateGithubOpResult };
|
|
88
89
|
|
|
@@ -709,7 +710,7 @@ export function buildGithubOpPipelineDocs(
|
|
|
709
710
|
? [...runParts, ...GATED_EXIT_FLAG, "--json"].join(" ")
|
|
710
711
|
: runParts.join(" ");
|
|
711
712
|
|
|
712
|
-
const steps: Array<Record<string, unknown>> = [{ uses: "actions/checkout
|
|
713
|
+
const steps: Array<Record<string, unknown>> = [{ uses: actionRef("actions/checkout") }];
|
|
713
714
|
for (const step of setup) steps.push(setupStepDoc(step));
|
|
714
715
|
for (const line of beforeScript) steps.push({ run: line });
|
|
715
716
|
steps.push(
|
|
@@ -822,14 +823,14 @@ export function emitOpPipelineYAML(doc: GithubOpPipelineDoc): string {
|
|
|
822
823
|
// it was before the field existed.
|
|
823
824
|
if (doc.header && doc.header.length > 0) sections.push(doc.header.join("\n"));
|
|
824
825
|
if (doc.name) sections.push("name: " + emitYAML(doc.name, 0));
|
|
825
|
-
sections.push("on
|
|
826
|
-
if (doc.env && Object.keys(doc.env).length > 0) sections.push("env
|
|
827
|
-
sections.push("concurrency
|
|
828
|
-
if (Object.keys(doc.permissions).length > 0) sections.push("permissions
|
|
826
|
+
sections.push(emitYAMLEntry("on", doc.on));
|
|
827
|
+
if (doc.env && Object.keys(doc.env).length > 0) sections.push(emitYAMLEntry("env", doc.env));
|
|
828
|
+
sections.push(emitYAMLEntry("concurrency", doc.concurrency));
|
|
829
|
+
if (Object.keys(doc.permissions).length > 0) sections.push(emitYAMLEntry("permissions", doc.permissions));
|
|
829
830
|
// The gated-apply notice job rides in `jobs:` beside the Op's own job, but
|
|
830
831
|
// is carried separately on the doc so a dialect that cannot run it (forgejo,
|
|
831
832
|
// whose runner has no `gh` pointed at its own instance) drops it by omission.
|
|
832
|
-
sections.push("jobs
|
|
833
|
+
sections.push(emitYAMLEntry("jobs", { ...doc.jobsDoc, ...(doc.gatedNoticeDoc ?? {}) }));
|
|
833
834
|
return sections.join("\n\n") + "\n";
|
|
834
835
|
}
|
|
835
836
|
|
|
@@ -104,7 +104,7 @@ describe("generateGithubPipeline: structurally valid YAML", () => {
|
|
|
104
104
|
expect(job.steps.length).toBeGreaterThan(0);
|
|
105
105
|
// Every job starts with a checkout step — a job runs on a fresh runner
|
|
106
106
|
// with no repo present, unlike GitLab's auto-cloned workspace.
|
|
107
|
-
expect(job.steps[0].uses).toBe("actions/checkout@
|
|
107
|
+
expect(job.steps[0].uses).toBe("actions/checkout@v7");
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
* touching the component declarations.
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
|
-
import { emitYAML } from "@intentius/chant/yaml";
|
|
59
|
+
import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
|
|
60
60
|
import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/components/driver";
|
|
61
61
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
62
62
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
@@ -66,6 +66,7 @@ import type {
|
|
|
66
66
|
ComponentPipelineResult as GenerateGithubResult,
|
|
67
67
|
PipelineMember,
|
|
68
68
|
} from "@intentius/chant/lexicon";
|
|
69
|
+
import { actionRef } from "../action-pins";
|
|
69
70
|
|
|
70
71
|
export type { GeneratedJob, GenerateGithubOptions, GenerateGithubResult };
|
|
71
72
|
|
|
@@ -232,7 +233,7 @@ export function buildGithubPipelineDoc(
|
|
|
232
233
|
// One step per script line — mirrors gitlab's `script:` array of
|
|
233
234
|
// discrete shell lines, rather than a single multi-line `run:` block, so
|
|
234
235
|
// each line is independently inspectable (and machine-parseable).
|
|
235
|
-
const steps: Array<Record<string, unknown>> = [{ uses: "actions/checkout
|
|
236
|
+
const steps: Array<Record<string, unknown>> = [{ uses: actionRef("actions/checkout") }];
|
|
236
237
|
|
|
237
238
|
for (const dep of component.dependsOn ?? []) {
|
|
238
239
|
steps.push({
|
|
@@ -297,7 +298,7 @@ export function buildGithubPipelineDoc(
|
|
|
297
298
|
for (const name of [...pinned].sort()) {
|
|
298
299
|
command.push("--digest", `"${name}=\${{ needs.${jobNameByComponent.get(name)!}.outputs.digest }}"`);
|
|
299
300
|
}
|
|
300
|
-
const steps: Array<Record<string, unknown>> = [{ uses: "actions/checkout
|
|
301
|
+
const steps: Array<Record<string, unknown>> = [{ uses: actionRef("actions/checkout") }];
|
|
301
302
|
for (const [name, paths] of archives) {
|
|
302
303
|
steps.push({
|
|
303
304
|
name: `Download ${name} build archive`,
|
|
@@ -375,21 +376,14 @@ function memberArtifactStep(step: Record<string, unknown>, member: PipelineMembe
|
|
|
375
376
|
* Shared with the forgejo dialect (#969), which transforms the doc first.
|
|
376
377
|
*/
|
|
377
378
|
export function emitPipelineYAML(doc: GithubPipelineDoc): string {
|
|
378
|
-
// A block value starts with a newline; an inline one (`{}` for an empty
|
|
379
|
-
// component set) needs the space after the colon, or `jobs:{}` is a plain
|
|
380
|
-
// scalar rather than a key (#2991).
|
|
381
|
-
const section = (key: string, value: unknown): string => {
|
|
382
|
-
const emitted = emitYAML(value, 1);
|
|
383
|
-
return `${key}:${emitted.startsWith("\n") ? "" : " "}${emitted}`;
|
|
384
|
-
};
|
|
385
379
|
const sections: string[] = [];
|
|
386
380
|
sections.push("name: " + emitYAML(doc.name, 0));
|
|
387
|
-
sections.push(
|
|
381
|
+
sections.push(emitYAMLEntry("on", doc.on));
|
|
388
382
|
if (doc.env && Object.keys(doc.env).length > 0) {
|
|
389
|
-
sections.push(
|
|
383
|
+
sections.push(emitYAMLEntry("env", doc.env));
|
|
390
384
|
}
|
|
391
|
-
if (doc.defaults) sections.push(
|
|
392
|
-
sections.push(
|
|
385
|
+
if (doc.defaults) sections.push(emitYAMLEntry("defaults", doc.defaults));
|
|
386
|
+
sections.push(emitYAMLEntry("jobs", doc.jobsDoc));
|
|
393
387
|
return sections.join("\n\n") + "\n";
|
|
394
388
|
}
|
|
395
389
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Step } from "../generated/index";
|
|
3
|
+
import { actionRef, type ActionPinMode } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface CheckoutProps {
|
|
5
6
|
ref?: string;
|
|
@@ -8,6 +9,12 @@ export interface CheckoutProps {
|
|
|
8
9
|
token?: string;
|
|
9
10
|
submodules?: boolean | string;
|
|
10
11
|
sshKey?: string;
|
|
12
|
+
/**
|
|
13
|
+
* `"tag"` (default) emits the action's major tag. `"sha"` emits the commit
|
|
14
|
+
* SHA from the lexicon's pin table with the version as a YAML comment, which
|
|
15
|
+
* passes GHA021, GHA029 and GHA059.
|
|
16
|
+
*/
|
|
17
|
+
pin?: ActionPinMode;
|
|
11
18
|
defaults?: {
|
|
12
19
|
step?: Partial<ConstructorParameters<typeof Step>[0]>;
|
|
13
20
|
};
|
|
@@ -25,7 +32,7 @@ export const Checkout = Composite((props: CheckoutProps) => {
|
|
|
25
32
|
|
|
26
33
|
const step = new Step(mergeDefaults({
|
|
27
34
|
name: "Checkout",
|
|
28
|
-
uses: "actions/checkout
|
|
35
|
+
uses: actionRef("actions/checkout", props.pin),
|
|
29
36
|
...(Object.keys(withObj).length > 0 ? { with: withObj } : {}),
|
|
30
37
|
}, defaults?.step));
|
|
31
38
|
|
|
@@ -19,7 +19,7 @@ describe("Checkout", () => {
|
|
|
19
19
|
test("returns step with checkout action", () => {
|
|
20
20
|
const result = Checkout({});
|
|
21
21
|
expect(result.step).toBeDefined();
|
|
22
|
-
expect(result.step.props.uses).toBe("actions/checkout@
|
|
22
|
+
expect(result.step.props.uses).toBe("actions/checkout@v7");
|
|
23
23
|
expect(result.step.props.name).toBe("Checkout");
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -45,7 +45,7 @@ describe("Checkout", () => {
|
|
|
45
45
|
describe("SetupNode", () => {
|
|
46
46
|
test("returns step with setup-node action", () => {
|
|
47
47
|
const result = SetupNode({});
|
|
48
|
-
expect(result.step.props.uses).toBe("actions/setup-node@
|
|
48
|
+
expect(result.step.props.uses).toBe("actions/setup-node@v7");
|
|
49
49
|
expect(result.step.props.name).toBe("Setup Node.js");
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -540,7 +540,7 @@ describe("DeployEnvironment", () => {
|
|
|
540
540
|
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
541
541
|
const steps = result.deployJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
542
542
|
expect(steps.length).toBe(2);
|
|
543
|
-
expect(steps[0].props.uses).toBe("actions/checkout@
|
|
543
|
+
expect(steps[0].props.uses).toBe("actions/checkout@v7");
|
|
544
544
|
expect(steps[1].props.run).toBe("npm run deploy");
|
|
545
545
|
});
|
|
546
546
|
|
|
@@ -683,7 +683,7 @@ describe("defaults overrides", () => {
|
|
|
683
683
|
defaults: { step: { id: "my-checkout" } },
|
|
684
684
|
});
|
|
685
685
|
expect(result.step.props.id).toBe("my-checkout");
|
|
686
|
-
expect(result.step.props.uses).toBe("actions/checkout@
|
|
686
|
+
expect(result.step.props.uses).toBe("actions/checkout@v7");
|
|
687
687
|
});
|
|
688
688
|
|
|
689
689
|
test("SetupNode defaults override step props", () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface DeployEnvironmentProps {
|
|
5
6
|
/** Environment name. Required. */
|
|
@@ -40,7 +41,7 @@ export const DeployEnvironment = Composite((props: DeployEnvironmentProps) => {
|
|
|
40
41
|
|
|
41
42
|
// ── Deploy job ─────────────────────────────────────────────────────
|
|
42
43
|
const deploySteps = [
|
|
43
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
44
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
44
45
|
...deployScriptArr.map(
|
|
45
46
|
(cmd: string) => new Step({ name: "Deploy", run: cmd }),
|
|
46
47
|
),
|
|
@@ -63,7 +64,7 @@ export const DeployEnvironment = Composite((props: DeployEnvironmentProps) => {
|
|
|
63
64
|
|
|
64
65
|
// ── Cleanup job ────────────────────────────────────────────────────
|
|
65
66
|
const cleanupSteps = [
|
|
66
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
67
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
67
68
|
...cleanupScriptArr.map(
|
|
68
69
|
(cmd: string) => new Step({ name: "Cleanup", run: cmd }),
|
|
69
70
|
),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface DockerBuildProps {
|
|
5
6
|
/** Image tag. Default: "${{ github.sha }}" */
|
|
@@ -44,7 +45,7 @@ export const DockerBuild = Composite((props: DockerBuildProps) => {
|
|
|
44
45
|
} = props;
|
|
45
46
|
|
|
46
47
|
// ── Steps ──────────────────────────────────────────────────────────
|
|
47
|
-
const checkout = new Step({ name: "Checkout", uses: "actions/checkout
|
|
48
|
+
const checkout = new Step({ name: "Checkout", uses: actionRef("actions/checkout") });
|
|
48
49
|
|
|
49
50
|
const login = new Step({
|
|
50
51
|
name: "Log in to container registry",
|
package/src/composites/go-ci.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface GoCIProps {
|
|
5
6
|
/** Go version. Default: "1.22" */
|
|
@@ -34,7 +35,7 @@ export const GoCI = Composite((props: GoCIProps) => {
|
|
|
34
35
|
const buildJob = new Job(mergeDefaults({
|
|
35
36
|
"runs-on": runsOn,
|
|
36
37
|
steps: [
|
|
37
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
38
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
38
39
|
new Step({
|
|
39
40
|
name: "Setup Go",
|
|
40
41
|
uses: "actions/setup-go@v5",
|
|
@@ -48,7 +49,7 @@ export const GoCI = Composite((props: GoCIProps) => {
|
|
|
48
49
|
const testJob = new Job(mergeDefaults({
|
|
49
50
|
"runs-on": runsOn,
|
|
50
51
|
steps: [
|
|
51
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
52
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
52
53
|
new Step({
|
|
53
54
|
name: "Setup Go",
|
|
54
55
|
uses: "actions/setup-go@v5",
|
|
@@ -64,7 +65,7 @@ export const GoCI = Composite((props: GoCIProps) => {
|
|
|
64
65
|
? new Job(mergeDefaults({
|
|
65
66
|
"runs-on": runsOn,
|
|
66
67
|
steps: [
|
|
67
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
68
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
68
69
|
new Step({
|
|
69
70
|
name: "Setup Go",
|
|
70
71
|
uses: "actions/setup-go@v5",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface NodeCIProps {
|
|
5
6
|
nodeVersion?: string;
|
|
@@ -28,12 +29,12 @@ export const NodeCI = Composite((props: NodeCIProps) => {
|
|
|
28
29
|
|
|
29
30
|
const checkoutStep = new Step({
|
|
30
31
|
name: "Checkout",
|
|
31
|
-
uses: "actions/checkout
|
|
32
|
+
uses: actionRef("actions/checkout"),
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
const setupNodeStep = new Step({
|
|
35
36
|
name: "Setup Node.js",
|
|
36
|
-
uses: "actions/setup-node
|
|
37
|
+
uses: actionRef("actions/setup-node"),
|
|
37
38
|
with: {
|
|
38
39
|
"node-version": nodeVersion,
|
|
39
40
|
cache: packageManager === "bun" ? undefined : packageManager,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, withDefaults, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface NodePipelineProps {
|
|
5
6
|
/** Node.js version. Default: "22" */
|
|
@@ -32,19 +33,19 @@ const cacheConfig = {
|
|
|
32
33
|
cache: "npm",
|
|
33
34
|
installCmd: "npm ci",
|
|
34
35
|
runPrefix: "npm run",
|
|
35
|
-
setupAction: "actions/setup-node
|
|
36
|
+
setupAction: actionRef("actions/setup-node"),
|
|
36
37
|
},
|
|
37
38
|
pnpm: {
|
|
38
39
|
cache: "pnpm",
|
|
39
40
|
installCmd: "pnpm install --frozen-lockfile",
|
|
40
41
|
runPrefix: "pnpm run",
|
|
41
|
-
setupAction: "actions/setup-node
|
|
42
|
+
setupAction: actionRef("actions/setup-node"),
|
|
42
43
|
},
|
|
43
44
|
yarn: {
|
|
44
45
|
cache: "yarn",
|
|
45
46
|
installCmd: "yarn install --frozen-lockfile",
|
|
46
47
|
runPrefix: "yarn",
|
|
47
|
-
setupAction: "actions/setup-node
|
|
48
|
+
setupAction: actionRef("actions/setup-node"),
|
|
48
49
|
},
|
|
49
50
|
bun: {
|
|
50
51
|
cache: undefined,
|
|
@@ -75,13 +76,13 @@ export const NodePipeline = Composite((props: NodePipelineProps) => {
|
|
|
75
76
|
const isBun = packageManager === "bun";
|
|
76
77
|
|
|
77
78
|
// ── Build job steps ────────────────────────────────────────────────
|
|
78
|
-
const buildCheckout = new Step({ name: "Checkout", uses: "actions/checkout
|
|
79
|
+
const buildCheckout = new Step({ name: "Checkout", uses: actionRef("actions/checkout") });
|
|
79
80
|
|
|
80
81
|
const buildSetup = isBun
|
|
81
82
|
? new Step({ name: "Setup Bun", uses: "oven-sh/setup-bun@v2" })
|
|
82
83
|
: new Step({
|
|
83
84
|
name: "Setup Node.js",
|
|
84
|
-
uses: "actions/setup-node
|
|
85
|
+
uses: actionRef("actions/setup-node"),
|
|
85
86
|
with: { "node-version": nodeVersion, cache: pm.cache },
|
|
86
87
|
});
|
|
87
88
|
|
|
@@ -105,13 +106,13 @@ export const NodePipeline = Composite((props: NodePipelineProps) => {
|
|
|
105
106
|
}, defaults?.buildJob));
|
|
106
107
|
|
|
107
108
|
// ── Test job steps ─────────────────────────────────────────────────
|
|
108
|
-
const testCheckout = new Step({ name: "Checkout", uses: "actions/checkout
|
|
109
|
+
const testCheckout = new Step({ name: "Checkout", uses: actionRef("actions/checkout") });
|
|
109
110
|
|
|
110
111
|
const testSetup = isBun
|
|
111
112
|
? new Step({ name: "Setup Bun", uses: "oven-sh/setup-bun@v2" })
|
|
112
113
|
: new Step({
|
|
113
114
|
name: "Setup Node.js",
|
|
114
|
-
uses: "actions/setup-node
|
|
115
|
+
uses: actionRef("actions/setup-node"),
|
|
115
116
|
with: { "node-version": nodeVersion, cache: pm.cache },
|
|
116
117
|
});
|
|
117
118
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface PythonCIProps {
|
|
5
6
|
/** Python version. Default: "3.12" */
|
|
@@ -46,7 +47,7 @@ export const PythonCI = Composite((props: PythonCIProps) => {
|
|
|
46
47
|
const testJob = new Job(mergeDefaults({
|
|
47
48
|
"runs-on": runsOn,
|
|
48
49
|
steps: [
|
|
49
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
50
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
50
51
|
new Step({
|
|
51
52
|
name: "Setup Python",
|
|
52
53
|
uses: "actions/setup-python@v5",
|
|
@@ -63,7 +64,7 @@ export const PythonCI = Composite((props: PythonCIProps) => {
|
|
|
63
64
|
? new Job(mergeDefaults({
|
|
64
65
|
"runs-on": runsOn,
|
|
65
66
|
steps: [
|
|
66
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
67
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
67
68
|
new Step({
|
|
68
69
|
name: "Setup Python",
|
|
69
70
|
uses: "actions/setup-python@v5",
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Step } from "../generated/index";
|
|
3
|
+
import { actionRef, type ActionPinMode } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface SetupNodeProps {
|
|
5
6
|
nodeVersion?: string;
|
|
6
7
|
registryUrl?: string;
|
|
7
8
|
cache?: string;
|
|
8
9
|
cacheFilePath?: string;
|
|
10
|
+
/**
|
|
11
|
+
* `"tag"` (default) emits the action's major tag. `"sha"` emits the commit
|
|
12
|
+
* SHA from the lexicon's pin table with the version as a YAML comment, which
|
|
13
|
+
* passes GHA021, GHA029 and GHA059.
|
|
14
|
+
*/
|
|
15
|
+
pin?: ActionPinMode;
|
|
9
16
|
defaults?: {
|
|
10
17
|
step?: Partial<ConstructorParameters<typeof Step>[0]>;
|
|
11
18
|
};
|
|
@@ -21,7 +28,7 @@ export const SetupNode = Composite((props: SetupNodeProps) => {
|
|
|
21
28
|
|
|
22
29
|
const step = new Step(mergeDefaults({
|
|
23
30
|
name: "Setup Node.js",
|
|
24
|
-
uses: "actions/setup-node
|
|
31
|
+
uses: actionRef("actions/setup-node", props.pin),
|
|
25
32
|
...(Object.keys(withObj).length > 0 ? { with: withObj } : {}),
|
|
26
33
|
}, defaults?.step));
|
|
27
34
|
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ export { GitHub, Runner } from "./variables";
|
|
|
20
20
|
// After running `chant generate`, this re-exports all entity classes
|
|
21
21
|
export * from "./generated/index";
|
|
22
22
|
|
|
23
|
+
// Action versions the composites emit, and their pinned commits
|
|
24
|
+
export { ACTION_PINS, actionRef, actionPin } from "./action-pins";
|
|
25
|
+
export type { ActionPin, ActionPinMode, PinnedAction } from "./action-pins";
|
|
26
|
+
|
|
23
27
|
// Composites
|
|
24
28
|
export {
|
|
25
29
|
Checkout, SetupNode, SetupGo, SetupPython,
|
|
@@ -66,6 +66,7 @@ export const githubAuditCatalog: Record<string, RuleMeta> = {
|
|
|
66
66
|
GHA066: auditRule("GHA066", "report-only", "guidance", "Unbounded artifact retention", "Set a retention-days sized to how long the artifact is actually needed.", { category: "efficiency" }),
|
|
67
67
|
GHA067: auditRule("GHA067", "report-only", "guidance", "Unconditional heavy step with no path filter", "Scope the trigger's paths, or add an if: guard to the step.", { category: "efficiency" }),
|
|
68
68
|
GHA068: auditRule("GHA068", "report-only", "guidance", "Pull-request workflow missing a concurrency group", "Add a concurrency group with cancel-in-progress: true.", { category: "efficiency" }),
|
|
69
|
+
GHA069: auditRule("GHA069", "merge-worthy", "guidance", "Job permissions block drops the workflow's id-token: write", "Add `id-token: write` to the job's `permissions:` block; keep the job block rather than removing it.", { category: "correctness" }),
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* the registry, the relation vocabulary, and why this is credit rather than
|
|
5
5
|
* authority. Kept by hand; the prior-art sweep (scripts/prior-art-sweep.ts) reports
|
|
6
6
|
* when a credited tool's index no longer lists a rule cited here.
|
|
7
|
+
*
|
|
8
|
+
* GHA069 (a job-level permissions block drops the workflow's `id-token:
|
|
9
|
+
* write`, #2273) has no entry here. Checked 2026-10-02: zizmor's 41 audits
|
|
10
|
+
* (docs.zizmor.sh/audits) cover over-broad scopes (`excessive-permissions`)
|
|
11
|
+
* and undocumented ones (`undocumented-permissions`), not a scope a job
|
|
12
|
+
* block silently loses. actionlint's `permissions` check
|
|
13
|
+
* (github.com/rhysd/actionlint/blob/main/docs/checks.md#permissions)
|
|
14
|
+
* validates scope names and levels only. poutine's rules (opa/rego/rules)
|
|
15
|
+
* have `default_permissions_on_risky_events`, the GHA013 condition, and
|
|
16
|
+
* nothing about a dropped grant. octoscan's `debug-oidc-action` flags OIDC
|
|
17
|
+
* action use as a debug finding and does not read the permission blocks.
|
|
18
|
+
* Scorecard's Token-Permissions rewards narrower blocks, the opposite
|
|
19
|
+
* concern. Ship with no lineage rather than invent a credit.
|
|
7
20
|
*/
|
|
8
21
|
import type { Lineage } from "@intentius/chant/audit/catalog";
|
|
9
22
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
14
|
+
with:
|
|
15
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
16
|
+
aws-region: us-east-1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- run: npm test
|
|
16
|
+
deploy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
20
|
+
with:
|
|
21
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
22
|
+
aws-region: us-east-1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: write
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
18
|
+
with:
|
|
19
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
20
|
+
aws-region: us-east-1
|
|
21
|
+
- run: aws s3 sync ./dist s3://example-bucket
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
17
|
+
with:
|
|
18
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
19
|
+
aws-region: us-east-1
|
|
20
|
+
- run: aws s3 sync ./dist s3://example-bucket
|
|
@@ -58,4 +58,20 @@ jobs:
|
|
|
58
58
|
const diags = gha011.check(makeCtx(yaml));
|
|
59
59
|
expect(diags).toHaveLength(0);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
test("flags a scalar needs: naming a missing job (#3201)", () => {
|
|
63
|
+
const yaml = `name: CI
|
|
64
|
+
on: push
|
|
65
|
+
jobs:
|
|
66
|
+
test:
|
|
67
|
+
needs: biuld
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- name: Test
|
|
71
|
+
run: npm test
|
|
72
|
+
`;
|
|
73
|
+
const diags = gha011.check(makeCtx(yaml));
|
|
74
|
+
expect(diags).toHaveLength(1);
|
|
75
|
+
expect(diags[0].message).toContain('"biuld"');
|
|
76
|
+
});
|
|
61
77
|
});
|
|
@@ -63,4 +63,22 @@ jobs:
|
|
|
63
63
|
const diags = gha018.check(makeCtx(yaml));
|
|
64
64
|
expect(diags).toHaveLength(0);
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
test("flags a checkout step that starts with name: (#3201)", () => {
|
|
68
|
+
const yaml = `name: CI
|
|
69
|
+
on:
|
|
70
|
+
pull_request_target:
|
|
71
|
+
jobs:
|
|
72
|
+
build:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- name: Label
|
|
76
|
+
run: echo label
|
|
77
|
+
- name: Checkout
|
|
78
|
+
uses: actions/checkout@v7
|
|
79
|
+
`;
|
|
80
|
+
const diags = gha018.check(makeCtx(yaml));
|
|
81
|
+
expect(diags).toHaveLength(1);
|
|
82
|
+
expect(diags[0].entity).toBe("build");
|
|
83
|
+
});
|
|
66
84
|
});
|
|
@@ -59,6 +59,48 @@ jobs:
|
|
|
59
59
|
needs: [test]
|
|
60
60
|
steps:
|
|
61
61
|
- run: echo deploy
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha019.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("detects a cycle through scalar needs: (#3201)", () => {
|
|
68
|
+
const yaml = `name: CI
|
|
69
|
+
on: push
|
|
70
|
+
jobs:
|
|
71
|
+
a:
|
|
72
|
+
needs: b
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- run: echo a
|
|
76
|
+
b:
|
|
77
|
+
needs: a
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
steps:
|
|
80
|
+
- run: echo b
|
|
81
|
+
`;
|
|
82
|
+
const diags = gha019.check(makeCtx(yaml));
|
|
83
|
+
expect(diags).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("no false cycle when a later job id has an underscore (#3201)", () => {
|
|
87
|
+
// The line-based extractJobs did not split on "extra_thing", so its
|
|
88
|
+
// needs: landed on shared-alb and read as shared-alb -> shared-alb.
|
|
89
|
+
// generateGithubPipeline emits this shape for a component named Extra_Thing.
|
|
90
|
+
const yaml = `name: chant-components
|
|
91
|
+
on:
|
|
92
|
+
workflow_dispatch: {}
|
|
93
|
+
jobs:
|
|
94
|
+
shared-alb:
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@v7
|
|
98
|
+
extra_thing:
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
needs:
|
|
101
|
+
- shared-alb
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v7
|
|
62
104
|
`;
|
|
63
105
|
const diags = gha019.check(makeCtx(yaml));
|
|
64
106
|
expect(diags).toHaveLength(0);
|