@intentius/chant-lexicon-github 0.0.18 → 0.0.24
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/integrity.json +14 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/gha020.ts +40 -0
- package/dist/rules/gha021.ts +48 -0
- package/dist/rules/gha022.ts +50 -0
- package/dist/rules/gha023.ts +44 -0
- package/dist/rules/gha024.ts +42 -0
- package/dist/rules/gha025.ts +42 -0
- package/dist/rules/gha026.ts +40 -0
- package/dist/rules/gha027.ts +57 -0
- package/dist/rules/gha028.ts +37 -0
- package/dist/skills/{github-actions-patterns.md → chant-github-patterns.md} +2 -1
- package/dist/skills/chant-github-security.md +88 -0
- package/dist/skills/chant-github.md +569 -4
- package/package.json +20 -2
- package/src/codegen/docs.test.ts +19 -0
- package/src/codegen/generate.test.ts +12 -0
- package/src/codegen/package.test.ts +8 -0
- package/src/composites/cache.ts +8 -3
- package/src/composites/checkout.ts +8 -3
- package/src/composites/composites.test.ts +106 -0
- package/src/composites/deploy-environment.ts +11 -5
- package/src/composites/docker-build.ts +11 -5
- package/src/composites/download-artifact.ts +8 -3
- package/src/composites/go-ci.ts +17 -9
- package/src/composites/node-ci.ts +11 -5
- package/src/composites/node-pipeline.ts +14 -7
- package/src/composites/python-ci.ts +14 -7
- package/src/composites/setup-go.ts +8 -3
- package/src/composites/setup-node.ts +8 -3
- package/src/composites/setup-python.ts +8 -3
- package/src/composites/upload-artifact.ts +8 -3
- package/src/coverage.test.ts +23 -0
- package/src/import/roundtrip.test.ts +206 -0
- package/src/lint/post-synth/gha006.test.ts +56 -0
- package/src/lint/post-synth/gha009.test.ts +56 -0
- package/src/lint/post-synth/gha011.test.ts +61 -0
- package/src/lint/post-synth/gha017.test.ts +51 -0
- package/src/lint/post-synth/gha018.test.ts +66 -0
- package/src/lint/post-synth/gha019.test.ts +66 -0
- package/src/lint/post-synth/gha020.test.ts +66 -0
- package/src/lint/post-synth/gha020.ts +40 -0
- package/src/lint/post-synth/gha021.test.ts +67 -0
- package/src/lint/post-synth/gha021.ts +48 -0
- package/src/lint/post-synth/gha022.test.ts +45 -0
- package/src/lint/post-synth/gha022.ts +50 -0
- package/src/lint/post-synth/gha023.test.ts +52 -0
- package/src/lint/post-synth/gha023.ts +44 -0
- package/src/lint/post-synth/gha024.test.ts +67 -0
- package/src/lint/post-synth/gha024.ts +42 -0
- package/src/lint/post-synth/gha025.test.ts +65 -0
- package/src/lint/post-synth/gha025.ts +42 -0
- package/src/lint/post-synth/gha026.test.ts +65 -0
- package/src/lint/post-synth/gha026.ts +40 -0
- package/src/lint/post-synth/gha027.test.ts +48 -0
- package/src/lint/post-synth/gha027.ts +57 -0
- package/src/lint/post-synth/gha028.test.ts +48 -0
- package/src/lint/post-synth/gha028.ts +37 -0
- package/src/lint/rules/deprecated-action-version.test.ts +26 -0
- package/src/lint/rules/detect-secrets.test.ts +25 -0
- package/src/lint/rules/extract-inline-structs.test.ts +25 -0
- package/src/lint/rules/file-job-limit.test.ts +28 -0
- package/src/lint/rules/job-timeout.test.ts +31 -0
- package/src/lint/rules/missing-recommended-inputs.test.ts +26 -0
- package/src/lint/rules/no-hardcoded-secrets.test.ts +31 -0
- package/src/lint/rules/no-raw-expressions.test.ts +31 -0
- package/src/lint/rules/suggest-cache.test.ts +25 -0
- package/src/lint/rules/use-condition-builders.test.ts +31 -0
- package/src/lint/rules/use-matrix-builder.test.ts +25 -0
- package/src/lint/rules/use-typed-actions.test.ts +39 -0
- package/src/lint/rules/validate-concurrency.test.ts +31 -0
- package/src/plugin.test.ts +1 -1
- package/src/plugin.ts +70 -145
- package/src/skills/{github-actions-patterns.md → chant-github-patterns.md} +2 -1
- package/src/skills/chant-github-security.md +88 -0
- package/src/skills/chant-github.md +594 -0
- package/src/validate.ts +14 -1
- package/src/variables.test.ts +48 -0
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { Composite } from "@intentius/chant";
|
|
1
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
|
+
import type { Step } from "../generated/index";
|
|
2
3
|
|
|
3
4
|
export interface UploadArtifactProps {
|
|
4
5
|
name: string;
|
|
5
6
|
path: string;
|
|
6
7
|
retentionDays?: number;
|
|
7
8
|
compressionLevel?: number;
|
|
9
|
+
defaults?: {
|
|
10
|
+
step?: Partial<ConstructorParameters<typeof Step>[0]>;
|
|
11
|
+
};
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
export const UploadArtifact = Composite<UploadArtifactProps>((props) => {
|
|
15
|
+
const { defaults } = props;
|
|
11
16
|
const withObj: Record<string, string> = {
|
|
12
17
|
name: props.name,
|
|
13
18
|
path: props.path,
|
|
@@ -17,11 +22,11 @@ export const UploadArtifact = Composite<UploadArtifactProps>((props) => {
|
|
|
17
22
|
|
|
18
23
|
const { createProperty } = require("@intentius/chant/runtime");
|
|
19
24
|
const StepClass = createProperty("GitHub::Actions::Step", "github");
|
|
20
|
-
const step = new StepClass({
|
|
25
|
+
const step = new StepClass(mergeDefaults({
|
|
21
26
|
name: "Upload Artifact",
|
|
22
27
|
uses: "actions/upload-artifact@v4",
|
|
23
28
|
with: withObj,
|
|
24
|
-
});
|
|
29
|
+
}, defaults?.step));
|
|
25
30
|
|
|
26
31
|
return { step };
|
|
27
32
|
}, "UploadArtifact");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
analyzeGitHubCoverage,
|
|
4
|
+
computeCoverage,
|
|
5
|
+
overallPct,
|
|
6
|
+
formatSummary,
|
|
7
|
+
formatVerbose,
|
|
8
|
+
checkThresholds,
|
|
9
|
+
} from "./coverage";
|
|
10
|
+
|
|
11
|
+
describe("coverage module", () => {
|
|
12
|
+
test("analyzeGitHubCoverage is exported and callable", () => {
|
|
13
|
+
expect(typeof analyzeGitHubCoverage).toBe("function");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("re-exports from core are functions", () => {
|
|
17
|
+
expect(typeof computeCoverage).toBe("function");
|
|
18
|
+
expect(typeof overallPct).toBe("function");
|
|
19
|
+
expect(typeof formatSummary).toBe("function");
|
|
20
|
+
expect(typeof formatVerbose).toBe("function");
|
|
21
|
+
expect(typeof checkThresholds).toBe("function");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { GitHubActionsParser } from "./parser";
|
|
3
|
+
import { GitHubActionsGenerator } from "./generator";
|
|
4
|
+
|
|
5
|
+
const parser = new GitHubActionsParser();
|
|
6
|
+
const generator = new GitHubActionsGenerator();
|
|
7
|
+
|
|
8
|
+
describe("roundtrip: parse → generate", () => {
|
|
9
|
+
test("simple CI workflow roundtrip", () => {
|
|
10
|
+
const yaml = `
|
|
11
|
+
name: CI
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: [main]
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- run: npm ci
|
|
21
|
+
- run: npm test
|
|
22
|
+
`;
|
|
23
|
+
const ir = parser.parse(yaml);
|
|
24
|
+
const files = generator.generate(ir);
|
|
25
|
+
|
|
26
|
+
expect(files).toHaveLength(1);
|
|
27
|
+
const content = files[0].content;
|
|
28
|
+
|
|
29
|
+
expect(content).toContain("import");
|
|
30
|
+
expect(content).toContain("export const");
|
|
31
|
+
expect(content).toContain("new Workflow(");
|
|
32
|
+
expect(content).toContain("new Job(");
|
|
33
|
+
expect(content).toContain("CI");
|
|
34
|
+
expect(content).toContain("npm ci");
|
|
35
|
+
expect(content).toContain("npm test");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("multi-job workflow roundtrip", () => {
|
|
39
|
+
const yaml = `
|
|
40
|
+
name: Build and Deploy
|
|
41
|
+
on:
|
|
42
|
+
push:
|
|
43
|
+
branches: [main]
|
|
44
|
+
pull_request:
|
|
45
|
+
branches: [main]
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
jobs:
|
|
49
|
+
build:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- uses: actions/setup-node@v4
|
|
54
|
+
with:
|
|
55
|
+
node-version: '22'
|
|
56
|
+
- run: npm ci
|
|
57
|
+
- run: npm run build
|
|
58
|
+
test:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: [build]
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
- run: npm test
|
|
64
|
+
deploy:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
needs: [test]
|
|
67
|
+
steps:
|
|
68
|
+
- run: echo "deploy"
|
|
69
|
+
`;
|
|
70
|
+
const ir = parser.parse(yaml);
|
|
71
|
+
expect(ir.resources.length).toBeGreaterThanOrEqual(3);
|
|
72
|
+
|
|
73
|
+
const files = generator.generate(ir);
|
|
74
|
+
const content = files[0].content;
|
|
75
|
+
|
|
76
|
+
expect(content).toContain("new Workflow(");
|
|
77
|
+
expect(content).toContain("new Job(");
|
|
78
|
+
expect(content).toContain("build");
|
|
79
|
+
expect(content).toContain("test");
|
|
80
|
+
expect(content).toContain("deploy");
|
|
81
|
+
expect(content).toContain("needs");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("matrix strategy roundtrip", () => {
|
|
85
|
+
const yaml = `
|
|
86
|
+
name: Matrix
|
|
87
|
+
on:
|
|
88
|
+
push:
|
|
89
|
+
jobs:
|
|
90
|
+
test:
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
strategy:
|
|
93
|
+
matrix:
|
|
94
|
+
node-version: [18, 20, 22]
|
|
95
|
+
fail-fast: false
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@v4
|
|
98
|
+
- run: npm test
|
|
99
|
+
`;
|
|
100
|
+
const ir = parser.parse(yaml);
|
|
101
|
+
const files = generator.generate(ir);
|
|
102
|
+
const content = files[0].content;
|
|
103
|
+
|
|
104
|
+
expect(content).toContain("matrix");
|
|
105
|
+
expect(content).toContain("node-version");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("concurrency and permissions roundtrip", () => {
|
|
109
|
+
const yaml = `
|
|
110
|
+
name: Deploy
|
|
111
|
+
on:
|
|
112
|
+
push:
|
|
113
|
+
branches: [main]
|
|
114
|
+
permissions:
|
|
115
|
+
contents: read
|
|
116
|
+
pages: write
|
|
117
|
+
concurrency:
|
|
118
|
+
group: deploy
|
|
119
|
+
cancel-in-progress: true
|
|
120
|
+
jobs:
|
|
121
|
+
deploy:
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
steps:
|
|
124
|
+
- run: echo deploy
|
|
125
|
+
`;
|
|
126
|
+
const ir = parser.parse(yaml);
|
|
127
|
+
const files = generator.generate(ir);
|
|
128
|
+
const content = files[0].content;
|
|
129
|
+
|
|
130
|
+
expect(content).toContain("concurrency");
|
|
131
|
+
expect(content).toContain("permissions");
|
|
132
|
+
expect(content).toContain("deploy");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("reusable workflow call roundtrip", () => {
|
|
136
|
+
const yaml = `
|
|
137
|
+
name: Caller
|
|
138
|
+
on:
|
|
139
|
+
push:
|
|
140
|
+
jobs:
|
|
141
|
+
call-deploy:
|
|
142
|
+
uses: ./.github/workflows/deploy.yml
|
|
143
|
+
with:
|
|
144
|
+
environment: production
|
|
145
|
+
secrets: inherit
|
|
146
|
+
`;
|
|
147
|
+
const ir = parser.parse(yaml);
|
|
148
|
+
const files = generator.generate(ir);
|
|
149
|
+
const content = files[0].content;
|
|
150
|
+
|
|
151
|
+
expect(content).toContain("Workflow");
|
|
152
|
+
expect(content).toContain("export const");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("workflow dispatch with inputs roundtrip", () => {
|
|
156
|
+
const yaml = `
|
|
157
|
+
name: Manual Deploy
|
|
158
|
+
on:
|
|
159
|
+
workflow_dispatch:
|
|
160
|
+
inputs:
|
|
161
|
+
environment:
|
|
162
|
+
description: "Target environment"
|
|
163
|
+
required: true
|
|
164
|
+
type: choice
|
|
165
|
+
options:
|
|
166
|
+
- staging
|
|
167
|
+
- production
|
|
168
|
+
jobs:
|
|
169
|
+
deploy:
|
|
170
|
+
runs-on: ubuntu-latest
|
|
171
|
+
steps:
|
|
172
|
+
- run: echo deploying
|
|
173
|
+
`;
|
|
174
|
+
const ir = parser.parse(yaml);
|
|
175
|
+
const files = generator.generate(ir);
|
|
176
|
+
const content = files[0].content;
|
|
177
|
+
|
|
178
|
+
expect(content).toContain("import");
|
|
179
|
+
expect(content).toContain("export const");
|
|
180
|
+
expect(content).toContain("Manual Deploy");
|
|
181
|
+
expect(content).toContain("workflow_dispatch");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("environment protection roundtrip", () => {
|
|
185
|
+
const yaml = `
|
|
186
|
+
name: Production Deploy
|
|
187
|
+
on:
|
|
188
|
+
push:
|
|
189
|
+
branches: [main]
|
|
190
|
+
jobs:
|
|
191
|
+
deploy:
|
|
192
|
+
runs-on: ubuntu-latest
|
|
193
|
+
environment:
|
|
194
|
+
name: production
|
|
195
|
+
url: https://example.com
|
|
196
|
+
steps:
|
|
197
|
+
- run: deploy.sh
|
|
198
|
+
`;
|
|
199
|
+
const ir = parser.parse(yaml);
|
|
200
|
+
const files = generator.generate(ir);
|
|
201
|
+
const content = files[0].content;
|
|
202
|
+
|
|
203
|
+
expect(content).toContain("environment");
|
|
204
|
+
expect(content).toContain("production");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
|
+
import { gha006 } from "./gha006";
|
|
5
|
+
|
|
6
|
+
function makeMultiCtx(files: Record<string, string>): PostSynthContext {
|
|
7
|
+
const result = {
|
|
8
|
+
primary: Object.values(files)[0] ?? "",
|
|
9
|
+
files,
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
outputs: new Map([["github", result as unknown as string]]),
|
|
13
|
+
entities: new Map(),
|
|
14
|
+
buildResult: {
|
|
15
|
+
outputs: new Map([["github", result as unknown as string]]),
|
|
16
|
+
entities: new Map(),
|
|
17
|
+
warnings: [],
|
|
18
|
+
errors: [],
|
|
19
|
+
sourceFileCount: 1,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("GHA006: duplicate workflow name", () => {
|
|
25
|
+
test("flags duplicate workflow names across files", () => {
|
|
26
|
+
const ctx = makeMultiCtx({
|
|
27
|
+
"ci.yml": `name: CI\non:\n push:\njobs:\n build:\n runs-on: ubuntu-latest\n`,
|
|
28
|
+
"deploy.yml": `name: CI\non:\n push:\njobs:\n deploy:\n runs-on: ubuntu-latest\n`,
|
|
29
|
+
});
|
|
30
|
+
const diags = gha006.check(ctx);
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("GHA006");
|
|
33
|
+
expect(diags[0].severity).toBe("error");
|
|
34
|
+
expect(diags[0].message).toContain("CI");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("does not flag unique workflow names", () => {
|
|
38
|
+
const ctx = makeMultiCtx({
|
|
39
|
+
"ci.yml": `name: CI\non:\n push:\njobs:\n build:\n runs-on: ubuntu-latest\n`,
|
|
40
|
+
"deploy.yml": `name: Deploy\non:\n push:\njobs:\n deploy:\n runs-on: ubuntu-latest\n`,
|
|
41
|
+
});
|
|
42
|
+
const diags = gha006.check(ctx);
|
|
43
|
+
expect(diags).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("flags three files with same name", () => {
|
|
47
|
+
const ctx = makeMultiCtx({
|
|
48
|
+
"a.yml": `name: Build\non:\n push:\njobs:\n a:\n runs-on: ubuntu-latest\n`,
|
|
49
|
+
"b.yml": `name: Build\non:\n push:\njobs:\n b:\n runs-on: ubuntu-latest\n`,
|
|
50
|
+
"c.yml": `name: Build\non:\n push:\njobs:\n c:\n runs-on: ubuntu-latest\n`,
|
|
51
|
+
});
|
|
52
|
+
const diags = gha006.check(ctx);
|
|
53
|
+
expect(diags).toHaveLength(1);
|
|
54
|
+
expect(diags[0].message).toContain("Build");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha009 } from "./gha009";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA009: empty matrix dimension", () => {
|
|
20
|
+
test("flags empty matrix array", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
strategy:
|
|
28
|
+
matrix:
|
|
29
|
+
node-version: []
|
|
30
|
+
steps:
|
|
31
|
+
- run: echo test
|
|
32
|
+
`;
|
|
33
|
+
const diags = gha009.check(makeCtx(yaml));
|
|
34
|
+
expect(diags).toHaveLength(1);
|
|
35
|
+
expect(diags[0].checkId).toBe("GHA009");
|
|
36
|
+
expect(diags[0].severity).toBe("error");
|
|
37
|
+
expect(diags[0].message).toContain("node-version");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("does not flag non-empty matrix", () => {
|
|
41
|
+
const yaml = `name: CI
|
|
42
|
+
on:
|
|
43
|
+
push:
|
|
44
|
+
jobs:
|
|
45
|
+
build:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
strategy:
|
|
48
|
+
matrix:
|
|
49
|
+
node-version: [18, 20, 22]
|
|
50
|
+
steps:
|
|
51
|
+
- run: echo test
|
|
52
|
+
`;
|
|
53
|
+
const diags = gha009.check(makeCtx(yaml));
|
|
54
|
+
expect(diags).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha011 } from "./gha011";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA011: invalid needs reference", () => {
|
|
20
|
+
test("flags needs referencing non-existent job", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- run: echo build
|
|
29
|
+
deploy:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
needs: [build, test]
|
|
32
|
+
steps:
|
|
33
|
+
- run: echo deploy
|
|
34
|
+
`;
|
|
35
|
+
const diags = gha011.check(makeCtx(yaml));
|
|
36
|
+
expect(diags).toHaveLength(1);
|
|
37
|
+
expect(diags[0].checkId).toBe("GHA011");
|
|
38
|
+
expect(diags[0].severity).toBe("error");
|
|
39
|
+
expect(diags[0].message).toContain("test");
|
|
40
|
+
expect(diags[0].message).toContain("deploy");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("does not flag valid needs", () => {
|
|
44
|
+
const yaml = `name: CI
|
|
45
|
+
on:
|
|
46
|
+
push:
|
|
47
|
+
jobs:
|
|
48
|
+
build:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- run: echo build
|
|
52
|
+
deploy:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
needs: [build]
|
|
55
|
+
steps:
|
|
56
|
+
- run: echo deploy
|
|
57
|
+
`;
|
|
58
|
+
const diags = gha011.check(makeCtx(yaml));
|
|
59
|
+
expect(diags).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha017 } from "./gha017";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA017: missing permissions", () => {
|
|
20
|
+
test("flags workflow without permissions", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- run: echo test
|
|
29
|
+
`;
|
|
30
|
+
const diags = gha017.check(makeCtx(yaml));
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("GHA017");
|
|
33
|
+
expect(diags[0].severity).toBe("info");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("does not flag workflow with permissions", () => {
|
|
37
|
+
const yaml = `name: CI
|
|
38
|
+
on:
|
|
39
|
+
push:
|
|
40
|
+
permissions:
|
|
41
|
+
contents: read
|
|
42
|
+
jobs:
|
|
43
|
+
build:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- run: echo test
|
|
47
|
+
`;
|
|
48
|
+
const diags = gha017.check(makeCtx(yaml));
|
|
49
|
+
expect(diags).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha018 } from "./gha018";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA018: pull_request_target + checkout", () => {
|
|
20
|
+
test("flags pull_request_target with checkout", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
pull_request_target:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- run: npm test
|
|
30
|
+
`;
|
|
31
|
+
const diags = gha018.check(makeCtx(yaml));
|
|
32
|
+
expect(diags).toHaveLength(1);
|
|
33
|
+
expect(diags[0].checkId).toBe("GHA018");
|
|
34
|
+
expect(diags[0].severity).toBe("warning");
|
|
35
|
+
expect(diags[0].message).toContain("checkout");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("does not flag pull_request_target without checkout", () => {
|
|
39
|
+
const yaml = `name: CI
|
|
40
|
+
on:
|
|
41
|
+
pull_request_target:
|
|
42
|
+
jobs:
|
|
43
|
+
label:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- run: echo "label"
|
|
47
|
+
`;
|
|
48
|
+
const diags = gha018.check(makeCtx(yaml));
|
|
49
|
+
expect(diags).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("does not flag push trigger with checkout", () => {
|
|
53
|
+
const yaml = `name: CI
|
|
54
|
+
on:
|
|
55
|
+
push:
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
- run: npm test
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha018.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha019 } from "./gha019";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA019: circular needs chain", () => {
|
|
20
|
+
test("detects simple cycle", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
needs: [deploy]
|
|
28
|
+
steps:
|
|
29
|
+
- run: echo build
|
|
30
|
+
deploy:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
needs: [build]
|
|
33
|
+
steps:
|
|
34
|
+
- run: echo deploy
|
|
35
|
+
`;
|
|
36
|
+
const diags = gha019.check(makeCtx(yaml));
|
|
37
|
+
expect(diags.length).toBeGreaterThanOrEqual(1);
|
|
38
|
+
expect(diags[0].checkId).toBe("GHA019");
|
|
39
|
+
expect(diags[0].severity).toBe("error");
|
|
40
|
+
expect(diags[0].message).toContain("\u2192");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("does not flag acyclic graph", () => {
|
|
44
|
+
const yaml = `name: CI
|
|
45
|
+
on:
|
|
46
|
+
push:
|
|
47
|
+
jobs:
|
|
48
|
+
build:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- run: echo build
|
|
52
|
+
test:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
needs: [build]
|
|
55
|
+
steps:
|
|
56
|
+
- run: echo test
|
|
57
|
+
deploy:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
needs: [test]
|
|
60
|
+
steps:
|
|
61
|
+
- run: echo deploy
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha019.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha020 } from "./gha020";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA020: missing job-level permissions for sensitive triggers", () => {
|
|
20
|
+
test("flags job without permissions when using pull_request_target", () => {
|
|
21
|
+
const yaml = `name: Review
|
|
22
|
+
on:
|
|
23
|
+
pull_request_target:
|
|
24
|
+
jobs:
|
|
25
|
+
review:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- run: echo review
|
|
29
|
+
`;
|
|
30
|
+
const diags = gha020.check(makeCtx(yaml));
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("GHA020");
|
|
33
|
+
expect(diags[0].severity).toBe("warning");
|
|
34
|
+
expect(diags[0].message).toContain("review");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("flags job without permissions when using workflow_dispatch", () => {
|
|
38
|
+
const yaml = `name: Manual
|
|
39
|
+
on:
|
|
40
|
+
workflow_dispatch:
|
|
41
|
+
jobs:
|
|
42
|
+
deploy:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- run: echo deploy
|
|
46
|
+
`;
|
|
47
|
+
const diags = gha020.check(makeCtx(yaml));
|
|
48
|
+
expect(diags).toHaveLength(1);
|
|
49
|
+
expect(diags[0].checkId).toBe("GHA020");
|
|
50
|
+
expect(diags[0].message).toContain("deploy");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("does not flag when trigger is not sensitive", () => {
|
|
54
|
+
const yaml = `name: CI
|
|
55
|
+
on:
|
|
56
|
+
push:
|
|
57
|
+
jobs:
|
|
58
|
+
build:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- run: echo build
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha020.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|