@intentius/chant-lexicon-github 0.0.18
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 +31 -0
- package/dist/manifest.json +15 -0
- package/dist/meta.json +135 -0
- package/dist/rules/deprecated-action-version.ts +49 -0
- package/dist/rules/detect-secrets.ts +53 -0
- package/dist/rules/extract-inline-structs.ts +62 -0
- package/dist/rules/file-job-limit.ts +49 -0
- package/dist/rules/gha006.ts +58 -0
- package/dist/rules/gha009.ts +42 -0
- package/dist/rules/gha011.ts +40 -0
- package/dist/rules/gha017.ts +32 -0
- package/dist/rules/gha018.ts +40 -0
- package/dist/rules/gha019.ts +72 -0
- package/dist/rules/job-timeout.ts +59 -0
- package/dist/rules/missing-recommended-inputs.ts +61 -0
- package/dist/rules/no-hardcoded-secrets.ts +46 -0
- package/dist/rules/no-raw-expressions.ts +51 -0
- package/dist/rules/suggest-cache.ts +71 -0
- package/dist/rules/use-condition-builders.ts +45 -0
- package/dist/rules/use-matrix-builder.ts +44 -0
- package/dist/rules/use-typed-actions.ts +47 -0
- package/dist/rules/validate-concurrency.ts +66 -0
- package/dist/rules/yaml-helpers.ts +129 -0
- package/dist/skills/chant-github.md +29 -0
- package/dist/skills/github-actions-patterns.md +93 -0
- package/dist/types/index.d.ts +358 -0
- package/package.json +33 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1138 -0
- package/src/codegen/generate-cli.ts +36 -0
- package/src/codegen/generate-lexicon.ts +58 -0
- package/src/codegen/generate-typescript.ts +149 -0
- package/src/codegen/generate.ts +141 -0
- package/src/codegen/naming.ts +57 -0
- package/src/codegen/package.ts +65 -0
- package/src/codegen/parse.ts +700 -0
- package/src/codegen/patches.ts +46 -0
- package/src/composites/cache.ts +25 -0
- package/src/composites/checkout.ts +31 -0
- package/src/composites/composites.test.ts +675 -0
- package/src/composites/deploy-environment.ts +77 -0
- package/src/composites/docker-build.ts +120 -0
- package/src/composites/download-artifact.ts +24 -0
- package/src/composites/go-ci.ts +91 -0
- package/src/composites/index.ts +26 -0
- package/src/composites/node-ci.ts +71 -0
- package/src/composites/node-pipeline.ts +151 -0
- package/src/composites/python-ci.ts +92 -0
- package/src/composites/setup-go.ts +24 -0
- package/src/composites/setup-node.ts +26 -0
- package/src/composites/setup-python.ts +24 -0
- package/src/composites/upload-artifact.ts +27 -0
- package/src/coverage.ts +49 -0
- package/src/expression.test.ts +147 -0
- package/src/expression.ts +214 -0
- package/src/generated/index.d.ts +358 -0
- package/src/generated/index.ts +29 -0
- package/src/generated/lexicon-github.json +135 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +110 -0
- package/src/import/generator.ts +119 -0
- package/src/import/parser.test.ts +98 -0
- package/src/import/parser.ts +73 -0
- package/src/index.ts +53 -0
- package/src/lint/post-synth/gha006.ts +58 -0
- package/src/lint/post-synth/gha009.ts +42 -0
- package/src/lint/post-synth/gha011.ts +40 -0
- package/src/lint/post-synth/gha017.ts +32 -0
- package/src/lint/post-synth/gha018.ts +40 -0
- package/src/lint/post-synth/gha019.ts +72 -0
- package/src/lint/post-synth/post-synth.test.ts +318 -0
- package/src/lint/post-synth/yaml-helpers.ts +129 -0
- package/src/lint/rules/data/deprecated-versions.ts +13 -0
- package/src/lint/rules/data/known-actions.ts +13 -0
- package/src/lint/rules/data/recommended-inputs.ts +10 -0
- package/src/lint/rules/data/secret-patterns.ts +31 -0
- package/src/lint/rules/deprecated-action-version.ts +49 -0
- package/src/lint/rules/detect-secrets.ts +53 -0
- package/src/lint/rules/extract-inline-structs.ts +62 -0
- package/src/lint/rules/file-job-limit.ts +49 -0
- package/src/lint/rules/index.ts +17 -0
- package/src/lint/rules/job-timeout.ts +59 -0
- package/src/lint/rules/missing-recommended-inputs.ts +61 -0
- package/src/lint/rules/no-hardcoded-secrets.ts +46 -0
- package/src/lint/rules/no-raw-expressions.ts +51 -0
- package/src/lint/rules/rules.test.ts +365 -0
- package/src/lint/rules/suggest-cache.ts +71 -0
- package/src/lint/rules/use-condition-builders.ts +45 -0
- package/src/lint/rules/use-matrix-builder.ts +44 -0
- package/src/lint/rules/use-typed-actions.ts +47 -0
- package/src/lint/rules/validate-concurrency.ts +66 -0
- package/src/lsp/completions.test.ts +9 -0
- package/src/lsp/completions.ts +20 -0
- package/src/lsp/hover.test.ts +9 -0
- package/src/lsp/hover.ts +38 -0
- package/src/package-cli.ts +42 -0
- package/src/plugin.test.ts +128 -0
- package/src/plugin.ts +408 -0
- package/src/serializer.test.ts +270 -0
- package/src/serializer.ts +383 -0
- package/src/skills/github-actions-patterns.md +93 -0
- package/src/spec/fetch.ts +55 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.test.ts +12 -0
- package/src/validate.ts +32 -0
- package/src/variables.ts +44 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { Checkout } from "./checkout";
|
|
3
|
+
import { SetupNode } from "./setup-node";
|
|
4
|
+
import { SetupGo } from "./setup-go";
|
|
5
|
+
import { SetupPython } from "./setup-python";
|
|
6
|
+
import { CacheAction } from "./cache";
|
|
7
|
+
import { UploadArtifact } from "./upload-artifact";
|
|
8
|
+
import { DownloadArtifact } from "./download-artifact";
|
|
9
|
+
import { NodeCI } from "./node-ci";
|
|
10
|
+
import { NodePipeline, BunPipeline, PnpmPipeline, YarnPipeline } from "./node-pipeline";
|
|
11
|
+
import { PythonCI } from "./python-ci";
|
|
12
|
+
import { DockerBuild } from "./docker-build";
|
|
13
|
+
import { DeployEnvironment } from "./deploy-environment";
|
|
14
|
+
import { GoCI } from "./go-ci";
|
|
15
|
+
|
|
16
|
+
// ── Checkout ────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("Checkout", () => {
|
|
19
|
+
test("returns step with checkout action", () => {
|
|
20
|
+
const result = Checkout({});
|
|
21
|
+
expect(result.step).toBeDefined();
|
|
22
|
+
expect(result.step.props.uses).toBe("actions/checkout@v4");
|
|
23
|
+
expect(result.step.props.name).toBe("Checkout");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("passes ref as with input", () => {
|
|
27
|
+
const result = Checkout({ ref: "develop" });
|
|
28
|
+
expect(result.step.props.with).toBeDefined();
|
|
29
|
+
expect((result.step.props.with as Record<string, string>).ref).toBe("develop");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("passes fetchDepth as with input", () => {
|
|
33
|
+
const result = Checkout({ fetchDepth: 0 });
|
|
34
|
+
expect((result.step.props.with as Record<string, string>)["fetch-depth"]).toBe("0");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("omits with when no optional props", () => {
|
|
38
|
+
const result = Checkout({});
|
|
39
|
+
expect(result.step.props.with).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── SetupNode ───────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("SetupNode", () => {
|
|
46
|
+
test("returns step with setup-node action", () => {
|
|
47
|
+
const result = SetupNode({});
|
|
48
|
+
expect(result.step.props.uses).toBe("actions/setup-node@v4");
|
|
49
|
+
expect(result.step.props.name).toBe("Setup Node.js");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("passes nodeVersion", () => {
|
|
53
|
+
const result = SetupNode({ nodeVersion: "22" });
|
|
54
|
+
expect((result.step.props.with as Record<string, string>)["node-version"]).toBe("22");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("passes cache option", () => {
|
|
58
|
+
const result = SetupNode({ cache: "npm" });
|
|
59
|
+
expect((result.step.props.with as Record<string, string>).cache).toBe("npm");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── SetupGo ─────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe("SetupGo", () => {
|
|
66
|
+
test("returns step with setup-go action", () => {
|
|
67
|
+
const result = SetupGo({});
|
|
68
|
+
expect(result.step.props.uses).toBe("actions/setup-go@v5");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("passes goVersion", () => {
|
|
72
|
+
const result = SetupGo({ goVersion: "1.22" });
|
|
73
|
+
expect((result.step.props.with as Record<string, string>)["go-version"]).toBe("1.22");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── SetupPython ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
describe("SetupPython", () => {
|
|
80
|
+
test("returns step with setup-python action", () => {
|
|
81
|
+
const result = SetupPython({});
|
|
82
|
+
expect(result.step.props.uses).toBe("actions/setup-python@v5");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("passes pythonVersion", () => {
|
|
86
|
+
const result = SetupPython({ pythonVersion: "3.12" });
|
|
87
|
+
expect((result.step.props.with as Record<string, string>)["python-version"]).toBe("3.12");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── CacheAction ─────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
describe("CacheAction", () => {
|
|
94
|
+
test("returns step with cache action", () => {
|
|
95
|
+
const result = CacheAction({ path: "node_modules", key: "npm-${{ hashFiles('package-lock.json') }}" });
|
|
96
|
+
expect(result.step.props.uses).toBe("actions/cache@v4");
|
|
97
|
+
expect((result.step.props.with as Record<string, string>).path).toBe("node_modules");
|
|
98
|
+
expect((result.step.props.with as Record<string, string>).key).toBe("npm-${{ hashFiles('package-lock.json') }}");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── UploadArtifact ──────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe("UploadArtifact", () => {
|
|
105
|
+
test("returns step with upload action", () => {
|
|
106
|
+
const result = UploadArtifact({ name: "build", path: "dist/" });
|
|
107
|
+
expect(result.step.props.uses).toBe("actions/upload-artifact@v4");
|
|
108
|
+
expect((result.step.props.with as Record<string, string>).name).toBe("build");
|
|
109
|
+
expect((result.step.props.with as Record<string, string>).path).toBe("dist/");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("passes retentionDays", () => {
|
|
113
|
+
const result = UploadArtifact({ name: "build", path: "dist/", retentionDays: 7 });
|
|
114
|
+
expect((result.step.props.with as Record<string, string>)["retention-days"]).toBe("7");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── DownloadArtifact ────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
describe("DownloadArtifact", () => {
|
|
121
|
+
test("returns step with download action", () => {
|
|
122
|
+
const result = DownloadArtifact({ name: "build" });
|
|
123
|
+
expect(result.step.props.uses).toBe("actions/download-artifact@v4");
|
|
124
|
+
expect((result.step.props.with as Record<string, string>).name).toBe("build");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── NodeCI ──────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe("NodeCI", () => {
|
|
131
|
+
test("returns workflow and job", () => {
|
|
132
|
+
const result = NodeCI({ nodeVersion: "22" });
|
|
133
|
+
expect(result.workflow).toBeDefined();
|
|
134
|
+
expect(result.job).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("workflow has CI name", () => {
|
|
138
|
+
const result = NodeCI({});
|
|
139
|
+
expect(result.workflow.props.name).toBe("CI");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("job has ubuntu-latest runner", () => {
|
|
143
|
+
const result = NodeCI({});
|
|
144
|
+
expect(result.job.props["runs-on"]).toBe("ubuntu-latest");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("job has steps array", () => {
|
|
148
|
+
const result = NodeCI({});
|
|
149
|
+
expect(Array.isArray(result.job.props.steps)).toBe(true);
|
|
150
|
+
expect((result.job.props.steps as unknown[]).length).toBe(5);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("uses default npm commands", () => {
|
|
154
|
+
const result = NodeCI({});
|
|
155
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
156
|
+
const installStep = steps[2];
|
|
157
|
+
expect(installStep.props.run).toBe("npm ci");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("uses pnpm when specified", () => {
|
|
161
|
+
const result = NodeCI({ packageManager: "pnpm" });
|
|
162
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
163
|
+
const installStep = steps[2];
|
|
164
|
+
expect(installStep.props.run).toBe("pnpm install");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("uses custom installCommand", () => {
|
|
168
|
+
const result = NodeCI({ installCommand: "yarn install --frozen-lockfile" });
|
|
169
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
170
|
+
const installStep = steps[2];
|
|
171
|
+
expect(installStep.props.run).toBe("yarn install --frozen-lockfile");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ── NodePipeline ────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
describe("NodePipeline", () => {
|
|
178
|
+
test("returns workflow, buildJob, testJob", () => {
|
|
179
|
+
const result = NodePipeline({});
|
|
180
|
+
expect(result.workflow).toBeDefined();
|
|
181
|
+
expect(result.buildJob).toBeDefined();
|
|
182
|
+
expect(result.testJob).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("workflow has Node Pipeline name", () => {
|
|
186
|
+
const result = NodePipeline({});
|
|
187
|
+
expect(result.workflow.props.name).toBe("Node Pipeline");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("buildJob has 5 steps (checkout, setup, install, build, upload)", () => {
|
|
191
|
+
const result = NodePipeline({});
|
|
192
|
+
const steps = result.buildJob.props.steps as unknown[];
|
|
193
|
+
expect(steps.length).toBe(5);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("testJob has 5 steps (checkout, setup, install, download, test)", () => {
|
|
197
|
+
const result = NodePipeline({});
|
|
198
|
+
const steps = result.testJob.props.steps as unknown[];
|
|
199
|
+
expect(steps.length).toBe(5);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("testJob depends on build", () => {
|
|
203
|
+
const result = NodePipeline({});
|
|
204
|
+
expect(result.testJob.props.needs).toEqual(["build"]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("uses npm ci by default", () => {
|
|
208
|
+
const result = NodePipeline({});
|
|
209
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
210
|
+
expect(steps[2].props.run).toBe("npm ci");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("uses pnpm install with frozen lockfile", () => {
|
|
214
|
+
const result = NodePipeline({ packageManager: "pnpm" });
|
|
215
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
216
|
+
expect(steps[2].props.run).toBe("pnpm install --frozen-lockfile");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("uses yarn install with frozen lockfile", () => {
|
|
220
|
+
const result = NodePipeline({ packageManager: "yarn" });
|
|
221
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
222
|
+
expect(steps[2].props.run).toBe("yarn install --frozen-lockfile");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("bun uses oven-sh/setup-bun instead of setup-node", () => {
|
|
226
|
+
const result = NodePipeline({ packageManager: "bun" });
|
|
227
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
228
|
+
expect(steps[1].props.uses).toBe("oven-sh/setup-bun@v2");
|
|
229
|
+
expect(steps[1].props.name).toBe("Setup Bun");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("bun uses bun install with frozen lockfile", () => {
|
|
233
|
+
const result = NodePipeline({ packageManager: "bun" });
|
|
234
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
235
|
+
expect(steps[2].props.run).toBe("bun install --frozen-lockfile");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("npm setup step has cache: npm", () => {
|
|
239
|
+
const result = NodePipeline({ packageManager: "npm" });
|
|
240
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
241
|
+
expect((steps[1].props.with as Record<string, string>).cache).toBe("npm");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("upload step uses upload-artifact with configured name", () => {
|
|
245
|
+
const result = NodePipeline({ artifactName: "my-build" });
|
|
246
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
247
|
+
expect(steps[4].props.uses).toBe("actions/upload-artifact@v4");
|
|
248
|
+
expect((steps[4].props.with as Record<string, string>).name).toBe("my-build");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("download step in testJob uses configured artifact name", () => {
|
|
252
|
+
const result = NodePipeline({ artifactName: "my-build" });
|
|
253
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
254
|
+
expect(steps[3].props.uses).toBe("actions/download-artifact@v4");
|
|
255
|
+
expect((steps[3].props.with as Record<string, string>).name).toBe("my-build");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("uses custom runsOn", () => {
|
|
259
|
+
const result = NodePipeline({ runsOn: "self-hosted" });
|
|
260
|
+
expect(result.buildJob.props["runs-on"]).toBe("self-hosted");
|
|
261
|
+
expect(result.testJob.props["runs-on"]).toBe("self-hosted");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("uses custom installCommand", () => {
|
|
265
|
+
const result = NodePipeline({ installCommand: "npm install --legacy-peer-deps" });
|
|
266
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
267
|
+
expect(steps[2].props.run).toBe("npm install --legacy-peer-deps");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("build step uses configured script", () => {
|
|
271
|
+
const result = NodePipeline({ buildScript: "compile" });
|
|
272
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
273
|
+
expect(steps[3].props.run).toBe("npm run compile");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("test step uses configured script", () => {
|
|
277
|
+
const result = NodePipeline({ testScript: "test:ci" });
|
|
278
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
279
|
+
expect(steps[4].props.run).toBe("npm run test:ci");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ── NodePipeline Presets ────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
describe("BunPipeline", () => {
|
|
286
|
+
test("defaults to bun package manager", () => {
|
|
287
|
+
const result = BunPipeline({});
|
|
288
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
289
|
+
expect(steps[1].props.uses).toBe("oven-sh/setup-bun@v2");
|
|
290
|
+
expect(steps[2].props.run).toBe("bun install --frozen-lockfile");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("PnpmPipeline", () => {
|
|
295
|
+
test("defaults to pnpm package manager", () => {
|
|
296
|
+
const result = PnpmPipeline({});
|
|
297
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
298
|
+
expect((steps[1].props.with as Record<string, string>).cache).toBe("pnpm");
|
|
299
|
+
expect(steps[2].props.run).toBe("pnpm install --frozen-lockfile");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("YarnPipeline", () => {
|
|
304
|
+
test("defaults to yarn package manager", () => {
|
|
305
|
+
const result = YarnPipeline({});
|
|
306
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
307
|
+
expect((steps[1].props.with as Record<string, string>).cache).toBe("yarn");
|
|
308
|
+
expect(steps[2].props.run).toBe("yarn install --frozen-lockfile");
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ── PythonCI ────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
describe("PythonCI", () => {
|
|
315
|
+
test("returns workflow and testJob", () => {
|
|
316
|
+
const result = PythonCI({});
|
|
317
|
+
expect(result.workflow).toBeDefined();
|
|
318
|
+
expect(result.testJob).toBeDefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("workflow has Python CI name", () => {
|
|
322
|
+
const result = PythonCI({});
|
|
323
|
+
expect(result.workflow.props.name).toBe("Python CI");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("includes lintJob by default", () => {
|
|
327
|
+
const result = PythonCI({});
|
|
328
|
+
expect(result.lintJob).toBeDefined();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("omits lintJob when lintCommand is null", () => {
|
|
332
|
+
const result = PythonCI({ lintCommand: null });
|
|
333
|
+
expect(result.lintJob).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("testJob uses setup-python with default version", () => {
|
|
337
|
+
const result = PythonCI({});
|
|
338
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
339
|
+
const setupStep = steps[1];
|
|
340
|
+
expect(setupStep.props.uses).toBe("actions/setup-python@v5");
|
|
341
|
+
expect((setupStep.props.with as Record<string, string>)["python-version"]).toBe("3.12");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("testJob uses pip cache by default", () => {
|
|
345
|
+
const result = PythonCI({});
|
|
346
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
347
|
+
expect((steps[1].props.with as Record<string, string>).cache).toBe("pip");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("poetry mode uses poetry cache", () => {
|
|
351
|
+
const result = PythonCI({ usePoetry: true });
|
|
352
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
353
|
+
expect((steps[1].props.with as Record<string, string>).cache).toBe("poetry");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("poetry mode installs poetry first", () => {
|
|
357
|
+
const result = PythonCI({ usePoetry: true });
|
|
358
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
359
|
+
expect(steps[2].props.run).toBe("pip install poetry");
|
|
360
|
+
expect(steps[3].props.run).toBe("poetry install");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("pip mode installs from requirements.txt", () => {
|
|
364
|
+
const result = PythonCI({});
|
|
365
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
366
|
+
expect(steps[2].props.run).toBe("pip install -r requirements.txt");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("uses custom requirements file", () => {
|
|
370
|
+
const result = PythonCI({ requirementsFile: "requirements-dev.txt" });
|
|
371
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
372
|
+
expect(steps[2].props.run).toBe("pip install -r requirements-dev.txt");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("test step uses configured command", () => {
|
|
376
|
+
const result = PythonCI({ testCommand: "python -m pytest" });
|
|
377
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
378
|
+
const lastStep = steps[steps.length - 1];
|
|
379
|
+
expect(lastStep.props.run).toBe("python -m pytest");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("lint step uses configured command", () => {
|
|
383
|
+
const result = PythonCI({ lintCommand: "flake8 ." });
|
|
384
|
+
const steps = result.lintJob!.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
385
|
+
const lastStep = steps[steps.length - 1];
|
|
386
|
+
expect(lastStep.props.run).toBe("flake8 .");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("uses custom python version", () => {
|
|
390
|
+
const result = PythonCI({ pythonVersion: "3.11" });
|
|
391
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
392
|
+
expect((steps[1].props.with as Record<string, string>)["python-version"]).toBe("3.11");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("uses custom runsOn", () => {
|
|
396
|
+
const result = PythonCI({ runsOn: "macos-latest" });
|
|
397
|
+
expect(result.testJob.props["runs-on"]).toBe("macos-latest");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ── DockerBuild ─────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe("DockerBuild", () => {
|
|
404
|
+
test("returns workflow and job", () => {
|
|
405
|
+
const result = DockerBuild({});
|
|
406
|
+
expect(result.workflow).toBeDefined();
|
|
407
|
+
expect(result.job).toBeDefined();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("workflow has Docker Build name", () => {
|
|
411
|
+
const result = DockerBuild({});
|
|
412
|
+
expect(result.workflow.props.name).toBe("Docker Build");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("workflow has packages write permission", () => {
|
|
416
|
+
const result = DockerBuild({});
|
|
417
|
+
expect((result.workflow.props.permissions as Record<string, string>).packages).toBe("write");
|
|
418
|
+
expect((result.workflow.props.permissions as Record<string, string>).contents).toBe("read");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("job has 5 steps", () => {
|
|
422
|
+
const result = DockerBuild({});
|
|
423
|
+
const steps = result.job.props.steps as unknown[];
|
|
424
|
+
expect(steps.length).toBe(5);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("uses docker/login-action", () => {
|
|
428
|
+
const result = DockerBuild({});
|
|
429
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
430
|
+
expect(steps[1].props.uses).toBe("docker/login-action@v3");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("uses docker/setup-buildx-action", () => {
|
|
434
|
+
const result = DockerBuild({});
|
|
435
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
436
|
+
expect(steps[2].props.uses).toBe("docker/setup-buildx-action@v3");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("uses docker/metadata-action", () => {
|
|
440
|
+
const result = DockerBuild({});
|
|
441
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
442
|
+
expect(steps[3].props.uses).toBe("docker/metadata-action@v5");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("uses docker/build-push-action", () => {
|
|
446
|
+
const result = DockerBuild({});
|
|
447
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
448
|
+
expect(steps[4].props.uses).toBe("docker/build-push-action@v6");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("login step uses ghcr.io by default", () => {
|
|
452
|
+
const result = DockerBuild({});
|
|
453
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
454
|
+
expect((steps[1].props.with as Record<string, string>).registry).toBe("ghcr.io");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("uses custom registry", () => {
|
|
458
|
+
const result = DockerBuild({ registry: "docker.io" });
|
|
459
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
460
|
+
expect((steps[1].props.with as Record<string, string>).registry).toBe("docker.io");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("build-push step has push: true by default", () => {
|
|
464
|
+
const result = DockerBuild({});
|
|
465
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
466
|
+
expect((steps[4].props.with as Record<string, string>).push).toBe("true");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("build-push step uses configured dockerfile", () => {
|
|
470
|
+
const result = DockerBuild({ dockerfile: "Dockerfile.prod" });
|
|
471
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
472
|
+
expect((steps[4].props.with as Record<string, string>).file).toBe("Dockerfile.prod");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("build-push step includes platforms when specified", () => {
|
|
476
|
+
const result = DockerBuild({ platforms: ["linux/amd64", "linux/arm64"] });
|
|
477
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
478
|
+
expect((steps[4].props.with as Record<string, string>).platforms).toBe("linux/amd64,linux/arm64");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("build-push step omits platforms when not specified", () => {
|
|
482
|
+
const result = DockerBuild({});
|
|
483
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
484
|
+
expect((steps[4].props.with as Record<string, string>).platforms).toBeUndefined();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("build-push step includes build-args when specified", () => {
|
|
488
|
+
const result = DockerBuild({ buildArgs: { NODE_ENV: "production", VERSION: "1.0" } });
|
|
489
|
+
const steps = result.job.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
490
|
+
expect((steps[4].props.with as Record<string, string>)["build-args"]).toBe("NODE_ENV=production\nVERSION=1.0");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("uses custom runsOn", () => {
|
|
494
|
+
const result = DockerBuild({ runsOn: "self-hosted" });
|
|
495
|
+
expect(result.job.props["runs-on"]).toBe("self-hosted");
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ── DeployEnvironment ───────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
describe("DeployEnvironment", () => {
|
|
502
|
+
test("returns deployJob and cleanupJob", () => {
|
|
503
|
+
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
504
|
+
expect(result.deployJob).toBeDefined();
|
|
505
|
+
expect(result.cleanupJob).toBeDefined();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("deployJob has environment with name", () => {
|
|
509
|
+
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
510
|
+
expect((result.deployJob.props.environment as Record<string, string>).name).toBe("staging");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("deployJob has environment url when specified", () => {
|
|
514
|
+
const result = DeployEnvironment({
|
|
515
|
+
name: "staging",
|
|
516
|
+
deployScript: "npm run deploy",
|
|
517
|
+
url: "https://staging.example.com",
|
|
518
|
+
});
|
|
519
|
+
expect((result.deployJob.props.environment as Record<string, string>).url).toBe("https://staging.example.com");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("deployJob has concurrency group", () => {
|
|
523
|
+
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
524
|
+
const concurrency = result.deployJob.props.concurrency as Record<string, unknown>;
|
|
525
|
+
expect(concurrency.group).toBe("deploy-staging");
|
|
526
|
+
expect(concurrency["cancel-in-progress"]).toBe(true);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("uses custom concurrency group", () => {
|
|
530
|
+
const result = DeployEnvironment({
|
|
531
|
+
name: "staging",
|
|
532
|
+
deployScript: "npm run deploy",
|
|
533
|
+
concurrencyGroup: "custom-group",
|
|
534
|
+
});
|
|
535
|
+
const concurrency = result.deployJob.props.concurrency as Record<string, unknown>;
|
|
536
|
+
expect(concurrency.group).toBe("custom-group");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("deployJob has checkout + deploy steps", () => {
|
|
540
|
+
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
541
|
+
const steps = result.deployJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
542
|
+
expect(steps.length).toBe(2);
|
|
543
|
+
expect(steps[0].props.uses).toBe("actions/checkout@v4");
|
|
544
|
+
expect(steps[1].props.run).toBe("npm run deploy");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("deployScript accepts array", () => {
|
|
548
|
+
const result = DeployEnvironment({
|
|
549
|
+
name: "staging",
|
|
550
|
+
deployScript: ["npm run build", "npm run deploy"],
|
|
551
|
+
});
|
|
552
|
+
const steps = result.deployJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
553
|
+
expect(steps.length).toBe(3);
|
|
554
|
+
expect(steps[1].props.run).toBe("npm run build");
|
|
555
|
+
expect(steps[2].props.run).toBe("npm run deploy");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("cleanupJob has default cleanup script", () => {
|
|
559
|
+
const result = DeployEnvironment({ name: "staging", deployScript: "npm run deploy" });
|
|
560
|
+
const steps = result.cleanupJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
561
|
+
expect(steps[1].props.run).toBe('echo "Cleaning up..."');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("cleanupJob uses custom cleanup script", () => {
|
|
565
|
+
const result = DeployEnvironment({
|
|
566
|
+
name: "staging",
|
|
567
|
+
deployScript: "npm run deploy",
|
|
568
|
+
cleanupScript: "npm run teardown",
|
|
569
|
+
});
|
|
570
|
+
const steps = result.cleanupJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
571
|
+
expect(steps[1].props.run).toBe("npm run teardown");
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("cleanupJob has environment with name", () => {
|
|
575
|
+
const result = DeployEnvironment({ name: "production", deployScript: "npm run deploy" });
|
|
576
|
+
expect((result.cleanupJob.props.environment as Record<string, string>).name).toBe("production");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("uses custom runsOn", () => {
|
|
580
|
+
const result = DeployEnvironment({
|
|
581
|
+
name: "staging",
|
|
582
|
+
deployScript: "npm run deploy",
|
|
583
|
+
runsOn: "self-hosted",
|
|
584
|
+
});
|
|
585
|
+
expect(result.deployJob.props["runs-on"]).toBe("self-hosted");
|
|
586
|
+
expect(result.cleanupJob.props["runs-on"]).toBe("self-hosted");
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// ── GoCI ────────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
describe("GoCI", () => {
|
|
593
|
+
test("returns workflow, buildJob, testJob", () => {
|
|
594
|
+
const result = GoCI({});
|
|
595
|
+
expect(result.workflow).toBeDefined();
|
|
596
|
+
expect(result.buildJob).toBeDefined();
|
|
597
|
+
expect(result.testJob).toBeDefined();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("workflow has Go CI name", () => {
|
|
601
|
+
const result = GoCI({});
|
|
602
|
+
expect(result.workflow.props.name).toBe("Go CI");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("includes lintJob by default", () => {
|
|
606
|
+
const result = GoCI({});
|
|
607
|
+
expect(result.lintJob).toBeDefined();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("omits lintJob when lintCommand is null", () => {
|
|
611
|
+
const result = GoCI({ lintCommand: null });
|
|
612
|
+
expect(result.lintJob).toBeUndefined();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("buildJob has 3 steps (checkout, setup-go, build)", () => {
|
|
616
|
+
const result = GoCI({});
|
|
617
|
+
const steps = result.buildJob.props.steps as unknown[];
|
|
618
|
+
expect(steps.length).toBe(3);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("testJob has 3 steps (checkout, setup-go, test)", () => {
|
|
622
|
+
const result = GoCI({});
|
|
623
|
+
const steps = result.testJob.props.steps as unknown[];
|
|
624
|
+
expect(steps.length).toBe(3);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("buildJob uses setup-go with default version", () => {
|
|
628
|
+
const result = GoCI({});
|
|
629
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
630
|
+
expect(steps[1].props.uses).toBe("actions/setup-go@v5");
|
|
631
|
+
expect((steps[1].props.with as Record<string, string>)["go-version"]).toBe("1.22");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("uses custom go version", () => {
|
|
635
|
+
const result = GoCI({ goVersion: "1.21" });
|
|
636
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
637
|
+
expect((steps[1].props.with as Record<string, string>)["go-version"]).toBe("1.21");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("buildJob runs go build by default", () => {
|
|
641
|
+
const result = GoCI({});
|
|
642
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
643
|
+
expect(steps[2].props.run).toBe("go build ./...");
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("testJob runs go test with race detector by default", () => {
|
|
647
|
+
const result = GoCI({});
|
|
648
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
649
|
+
expect(steps[2].props.run).toBe("go test ./... -v -race");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("uses custom build command", () => {
|
|
653
|
+
const result = GoCI({ buildCommand: "go build -o bin/app ./cmd/app" });
|
|
654
|
+
const steps = result.buildJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
655
|
+
expect(steps[2].props.run).toBe("go build -o bin/app ./cmd/app");
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("uses custom test command", () => {
|
|
659
|
+
const result = GoCI({ testCommand: "go test -short ./..." });
|
|
660
|
+
const steps = result.testJob.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
661
|
+
expect(steps[2].props.run).toBe("go test -short ./...");
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("lintJob uses golangci-lint-action", () => {
|
|
665
|
+
const result = GoCI({});
|
|
666
|
+
const steps = result.lintJob!.props.steps as Array<{ props: Record<string, unknown> }>;
|
|
667
|
+
expect(steps[2].props.uses).toBe("golangci/golangci-lint-action@v6");
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("uses custom runsOn", () => {
|
|
671
|
+
const result = GoCI({ runsOn: "macos-latest" });
|
|
672
|
+
expect(result.buildJob.props["runs-on"]).toBe("macos-latest");
|
|
673
|
+
expect(result.testJob.props["runs-on"]).toBe("macos-latest");
|
|
674
|
+
});
|
|
675
|
+
});
|