@intentius/chant-lexicon-gitlab 0.0.8 → 0.0.9
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 +10 -6
- package/dist/manifest.json +1 -1
- package/dist/meta.json +186 -8
- package/dist/rules/wgl012.ts +86 -0
- package/dist/rules/wgl013.ts +62 -0
- package/dist/rules/wgl014.ts +51 -0
- package/dist/rules/wgl015.ts +85 -0
- package/dist/rules/yaml-helpers.ts +65 -3
- package/dist/skills/chant-gitlab.md +467 -24
- package/dist/types/index.d.ts +55 -16
- package/package.json +2 -2
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +58 -0
- package/src/codegen/docs.ts +32 -9
- package/src/codegen/generate-lexicon.ts +6 -1
- package/src/codegen/generate.ts +45 -50
- package/src/codegen/naming.ts +3 -0
- package/src/codegen/parse.test.ts +154 -4
- package/src/codegen/parse.ts +161 -49
- package/src/codegen/snapshot.test.ts +7 -5
- package/src/composites/composites.test.ts +452 -0
- package/src/composites/docker-build.ts +81 -0
- package/src/composites/index.ts +8 -0
- package/src/composites/node-pipeline.ts +104 -0
- package/src/composites/python-pipeline.ts +75 -0
- package/src/composites/review-app.ts +63 -0
- package/src/generated/index.d.ts +55 -16
- package/src/generated/index.ts +3 -0
- package/src/generated/lexicon-gitlab.json +186 -8
- package/src/import/generator.ts +3 -2
- package/src/index.ts +4 -0
- package/src/lint/post-synth/wgl012.test.ts +131 -0
- package/src/lint/post-synth/wgl012.ts +86 -0
- package/src/lint/post-synth/wgl013.test.ts +164 -0
- package/src/lint/post-synth/wgl013.ts +62 -0
- package/src/lint/post-synth/wgl014.test.ts +97 -0
- package/src/lint/post-synth/wgl014.ts +51 -0
- package/src/lint/post-synth/wgl015.test.ts +139 -0
- package/src/lint/post-synth/wgl015.ts +85 -0
- package/src/lint/post-synth/yaml-helpers.ts +65 -3
- package/src/plugin.test.ts +39 -13
- package/src/plugin.ts +636 -40
- package/src/serializer.test.ts +140 -0
- package/src/serializer.ts +63 -5
- package/src/validate.ts +1 -0
- package/src/variables.ts +4 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { expandComposite, isCompositeInstance } from "@intentius/chant";
|
|
3
|
+
import { DockerBuild } from "./docker-build";
|
|
4
|
+
import { NodePipeline, BunPipeline, PnpmPipeline } from "./node-pipeline";
|
|
5
|
+
import { PythonPipeline } from "./python-pipeline";
|
|
6
|
+
import { ReviewApp } from "./review-app";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// DockerBuild
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
describe("DockerBuild", () => {
|
|
12
|
+
test("returns a build member", () => {
|
|
13
|
+
const instance = DockerBuild({});
|
|
14
|
+
expect(instance.build).toBeDefined();
|
|
15
|
+
expect(Object.keys(instance.members)).toEqual(["build"]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("is a CompositeInstance", () => {
|
|
19
|
+
expect(isCompositeInstance(DockerBuild({}))).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("build job has docker image and dind service", () => {
|
|
23
|
+
const instance = DockerBuild({});
|
|
24
|
+
const props = (instance.build as any).props;
|
|
25
|
+
expect(props.image).toBeDefined();
|
|
26
|
+
const imgProps = (props.image as any).props;
|
|
27
|
+
expect(imgProps.name).toBe("docker:27-cli");
|
|
28
|
+
expect(props.services).toHaveLength(1);
|
|
29
|
+
const svcProps = (props.services[0] as any).props;
|
|
30
|
+
expect(svcProps.name).toBe("docker:27-dind");
|
|
31
|
+
expect(svcProps.alias).toBe("docker");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("sets TLS cert dir variable", () => {
|
|
35
|
+
const instance = DockerBuild({});
|
|
36
|
+
const props = (instance.build as any).props;
|
|
37
|
+
expect(props.variables.DOCKER_TLS_CERTDIR).toBe("/certs");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("default stage is build", () => {
|
|
41
|
+
const instance = DockerBuild({});
|
|
42
|
+
const props = (instance.build as any).props;
|
|
43
|
+
expect(props.stage).toBe("build");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("custom docker version", () => {
|
|
47
|
+
const instance = DockerBuild({ dockerVersion: "24" });
|
|
48
|
+
const props = (instance.build as any).props;
|
|
49
|
+
const imgProps = (props.image as any).props;
|
|
50
|
+
expect(imgProps.name).toBe("docker:24-cli");
|
|
51
|
+
const svcProps = (props.services[0] as any).props;
|
|
52
|
+
expect(svcProps.name).toBe("docker:24-dind");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("before_script contains docker login", () => {
|
|
56
|
+
const instance = DockerBuild({});
|
|
57
|
+
const props = (instance.build as any).props;
|
|
58
|
+
expect(props.before_script[0]).toContain("docker login");
|
|
59
|
+
expect(props.before_script[0]).toContain("$CI_REGISTRY_USER");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("script contains docker build and push", () => {
|
|
63
|
+
const instance = DockerBuild({});
|
|
64
|
+
const props = (instance.build as any).props;
|
|
65
|
+
expect(props.script[0]).toContain("docker build");
|
|
66
|
+
expect(props.script[1]).toContain("docker push");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("build args are passed as flags", () => {
|
|
70
|
+
const instance = DockerBuild({ buildArgs: { NODE_ENV: "production" } });
|
|
71
|
+
const props = (instance.build as any).props;
|
|
72
|
+
expect(props.script[0]).toContain("--build-arg NODE_ENV=production");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("tagLatest adds conditional tag script", () => {
|
|
76
|
+
const instance = DockerBuild({ tagLatest: true });
|
|
77
|
+
const props = (instance.build as any).props;
|
|
78
|
+
expect(props.script.length).toBe(3);
|
|
79
|
+
expect(props.script[2]).toContain(":latest");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("tagLatest: false omits latest tagging", () => {
|
|
83
|
+
const instance = DockerBuild({ tagLatest: false });
|
|
84
|
+
const props = (instance.build as any).props;
|
|
85
|
+
expect(props.script.length).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("custom rules are applied", () => {
|
|
89
|
+
const { Rule } = require("../generated");
|
|
90
|
+
const rule = new Rule({ if: "$CI_COMMIT_TAG" });
|
|
91
|
+
const instance = DockerBuild({ rules: [rule] });
|
|
92
|
+
const props = (instance.build as any).props;
|
|
93
|
+
expect(props.rules).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("expandComposite produces correct name", () => {
|
|
97
|
+
const expanded = expandComposite("docker", DockerBuild({}));
|
|
98
|
+
expect(expanded.has("dockerBuild")).toBe(true);
|
|
99
|
+
expect(expanded.size).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// NodePipeline
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
describe("NodePipeline", () => {
|
|
107
|
+
test("returns defaults, build, test members", () => {
|
|
108
|
+
const instance = NodePipeline({});
|
|
109
|
+
expect(Object.keys(instance.members)).toEqual(["defaults", "build", "test"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("default image is node:22-alpine", () => {
|
|
113
|
+
const instance = NodePipeline({});
|
|
114
|
+
const defaultProps = (instance.defaults as any).props;
|
|
115
|
+
const imgProps = (defaultProps.image as any).props;
|
|
116
|
+
expect(imgProps.name).toBe("node:22-alpine");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("custom node version", () => {
|
|
120
|
+
const instance = NodePipeline({ nodeVersion: "20" });
|
|
121
|
+
const defaultProps = (instance.defaults as any).props;
|
|
122
|
+
const imgProps = (defaultProps.image as any).props;
|
|
123
|
+
expect(imgProps.name).toBe("node:20-alpine");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("npm cache config (default)", () => {
|
|
127
|
+
const instance = NodePipeline({});
|
|
128
|
+
const defaultProps = (instance.defaults as any).props;
|
|
129
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
130
|
+
expect(cacheProps.paths).toEqual([".npm/"]);
|
|
131
|
+
expect(cacheProps.key).toEqual({ files: ["package-lock.json"] });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("pnpm cache config", () => {
|
|
135
|
+
const instance = NodePipeline({ packageManager: "pnpm" });
|
|
136
|
+
const defaultProps = (instance.defaults as any).props;
|
|
137
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
138
|
+
expect(cacheProps.paths).toEqual([".pnpm-store/"]);
|
|
139
|
+
expect(cacheProps.key).toEqual({ files: ["pnpm-lock.yaml"] });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("bun cache config", () => {
|
|
143
|
+
const instance = NodePipeline({ packageManager: "bun" });
|
|
144
|
+
const defaultProps = (instance.defaults as any).props;
|
|
145
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
146
|
+
expect(cacheProps.paths).toEqual([".bun/install/cache"]);
|
|
147
|
+
expect(cacheProps.key).toEqual({ files: ["bun.lock"] });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("build job has install + build script", () => {
|
|
151
|
+
const instance = NodePipeline({});
|
|
152
|
+
const props = (instance.build as any).props;
|
|
153
|
+
expect(props.script).toEqual(["npm ci", "npm run build"]);
|
|
154
|
+
expect(props.stage).toBe("build");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("test job has install + test script", () => {
|
|
158
|
+
const instance = NodePipeline({});
|
|
159
|
+
const props = (instance.test as any).props;
|
|
160
|
+
expect(props.script).toEqual(["npm ci", "npm run test"]);
|
|
161
|
+
expect(props.stage).toBe("test");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("build artifacts default to dist/", () => {
|
|
165
|
+
const instance = NodePipeline({});
|
|
166
|
+
const props = (instance.build as any).props;
|
|
167
|
+
const artProps = (props.artifacts as any).props;
|
|
168
|
+
expect(artProps.paths).toEqual(["dist/"]);
|
|
169
|
+
expect(artProps.expire_in).toBe("1 hour");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("custom build/test scripts", () => {
|
|
173
|
+
const instance = NodePipeline({
|
|
174
|
+
buildScript: "compile",
|
|
175
|
+
testScript: "check",
|
|
176
|
+
});
|
|
177
|
+
const buildProps = (instance.build as any).props;
|
|
178
|
+
const testProps = (instance.test as any).props;
|
|
179
|
+
expect(buildProps.script[1]).toBe("npm run compile");
|
|
180
|
+
expect(testProps.script[1]).toBe("npm run check");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("custom install command", () => {
|
|
184
|
+
const instance = NodePipeline({ installCommand: "yarn install" });
|
|
185
|
+
const buildProps = (instance.build as any).props;
|
|
186
|
+
expect(buildProps.script[0]).toBe("yarn install");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("test job has JUnit report artifact", () => {
|
|
190
|
+
const instance = NodePipeline({});
|
|
191
|
+
const props = (instance.test as any).props;
|
|
192
|
+
const artProps = (props.artifacts as any).props;
|
|
193
|
+
expect(artProps.reports).toEqual({ junit: "junit.xml" });
|
|
194
|
+
expect(artProps.when).toBe("always");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("expandComposite produces 3 entries", () => {
|
|
198
|
+
const expanded = expandComposite("app", NodePipeline({}));
|
|
199
|
+
expect(expanded.size).toBe(3);
|
|
200
|
+
expect(expanded.has("appDefaults")).toBe(true);
|
|
201
|
+
expect(expanded.has("appBuild")).toBe(true);
|
|
202
|
+
expect(expanded.has("appTest")).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// BunPipeline / PnpmPipeline presets
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
describe("BunPipeline preset", () => {
|
|
210
|
+
test("defaults to bun package manager", () => {
|
|
211
|
+
const instance = BunPipeline({});
|
|
212
|
+
const buildProps = (instance.build as any).props;
|
|
213
|
+
expect(buildProps.script[0]).toBe("bun install --frozen-lockfile");
|
|
214
|
+
expect(buildProps.script[1]).toBe("bun run build");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("preset defaults can be overridden", () => {
|
|
218
|
+
const instance = BunPipeline({ packageManager: "npm" });
|
|
219
|
+
const buildProps = (instance.build as any).props;
|
|
220
|
+
expect(buildProps.script[0]).toBe("npm ci");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("PnpmPipeline preset", () => {
|
|
225
|
+
test("defaults to pnpm package manager", () => {
|
|
226
|
+
const instance = PnpmPipeline({});
|
|
227
|
+
const buildProps = (instance.build as any).props;
|
|
228
|
+
expect(buildProps.script[0]).toBe("pnpm install --frozen-lockfile");
|
|
229
|
+
expect(buildProps.script[1]).toBe("pnpm run build");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// PythonPipeline
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
describe("PythonPipeline", () => {
|
|
237
|
+
test("returns defaults, test, lint members by default", () => {
|
|
238
|
+
const instance = PythonPipeline({});
|
|
239
|
+
expect(Object.keys(instance.members)).toContain("defaults");
|
|
240
|
+
expect(Object.keys(instance.members)).toContain("test");
|
|
241
|
+
expect(Object.keys(instance.members)).toContain("lint");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("default image is python:3.12-slim", () => {
|
|
245
|
+
const instance = PythonPipeline({});
|
|
246
|
+
const defaultProps = (instance.defaults as any).props;
|
|
247
|
+
const imgProps = (defaultProps.image as any).props;
|
|
248
|
+
expect(imgProps.name).toBe("python:3.12-slim");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("custom python version", () => {
|
|
252
|
+
const instance = PythonPipeline({ pythonVersion: "3.11" });
|
|
253
|
+
const defaultProps = (instance.defaults as any).props;
|
|
254
|
+
const imgProps = (defaultProps.image as any).props;
|
|
255
|
+
expect(imgProps.name).toBe("python:3.11-slim");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("defaults has venv setup in before_script", () => {
|
|
259
|
+
const instance = PythonPipeline({});
|
|
260
|
+
const defaultProps = (instance.defaults as any).props;
|
|
261
|
+
expect(defaultProps.before_script).toContain("python -m venv .venv");
|
|
262
|
+
expect(defaultProps.before_script).toContain("source .venv/bin/activate");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("poetry mode uses poetry install", () => {
|
|
266
|
+
const instance = PythonPipeline({ usePoetry: true });
|
|
267
|
+
const defaultProps = (instance.defaults as any).props;
|
|
268
|
+
expect(defaultProps.before_script).toContain("pip install poetry");
|
|
269
|
+
expect(defaultProps.before_script).toContain("poetry install");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("cache keyed on requirements file", () => {
|
|
273
|
+
const instance = PythonPipeline({});
|
|
274
|
+
const defaultProps = (instance.defaults as any).props;
|
|
275
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
276
|
+
expect(cacheProps.key).toEqual({ files: ["requirements.txt"] });
|
|
277
|
+
expect(cacheProps.paths).toEqual([".pip-cache/", ".venv/"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("poetry cache keyed on poetry.lock", () => {
|
|
281
|
+
const instance = PythonPipeline({ usePoetry: true });
|
|
282
|
+
const defaultProps = (instance.defaults as any).props;
|
|
283
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
284
|
+
expect(cacheProps.key).toEqual({ files: ["poetry.lock"] });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("test job runs pytest by default", () => {
|
|
288
|
+
const instance = PythonPipeline({});
|
|
289
|
+
const props = (instance.test as any).props;
|
|
290
|
+
expect(props.script).toContain("pytest --junitxml=report.xml --cov");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("test job has JUnit report", () => {
|
|
294
|
+
const instance = PythonPipeline({});
|
|
295
|
+
const props = (instance.test as any).props;
|
|
296
|
+
const artProps = (props.artifacts as any).props;
|
|
297
|
+
expect(artProps.reports).toEqual({ junit: "report.xml" });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("lint job runs ruff by default", () => {
|
|
301
|
+
const instance = PythonPipeline({});
|
|
302
|
+
const members = instance.members as any;
|
|
303
|
+
expect(members.lint).toBeDefined();
|
|
304
|
+
const props = (members.lint as any).props;
|
|
305
|
+
expect(props.script).toContain("ruff check .");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("lintCommand: null omits lint job", () => {
|
|
309
|
+
const instance = PythonPipeline({ lintCommand: null });
|
|
310
|
+
expect(Object.keys(instance.members)).not.toContain("lint");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("custom test command", () => {
|
|
314
|
+
const instance = PythonPipeline({ testCommand: "python -m unittest" });
|
|
315
|
+
const props = (instance.test as any).props;
|
|
316
|
+
expect(props.script).toContain("python -m unittest");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("expandComposite produces correct entries", () => {
|
|
320
|
+
const expanded = expandComposite("py", PythonPipeline({}));
|
|
321
|
+
expect(expanded.has("pyDefaults")).toBe(true);
|
|
322
|
+
expect(expanded.has("pyTest")).toBe(true);
|
|
323
|
+
expect(expanded.has("pyLint")).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("expandComposite without lint", () => {
|
|
327
|
+
const expanded = expandComposite("py", PythonPipeline({ lintCommand: null }));
|
|
328
|
+
expect(expanded.has("pyDefaults")).toBe(true);
|
|
329
|
+
expect(expanded.has("pyTest")).toBe(true);
|
|
330
|
+
expect(expanded.has("pyLint")).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// ReviewApp
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
describe("ReviewApp", () => {
|
|
338
|
+
const baseProps = {
|
|
339
|
+
name: "review",
|
|
340
|
+
deployScript: "kubectl apply -f k8s/",
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
test("returns deploy and stop members", () => {
|
|
344
|
+
const instance = ReviewApp(baseProps);
|
|
345
|
+
expect(Object.keys(instance.members)).toEqual(["deploy", "stop"]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("deploy job has environment with on_stop", () => {
|
|
349
|
+
const instance = ReviewApp(baseProps);
|
|
350
|
+
const props = (instance.deploy as any).props;
|
|
351
|
+
const envProps = (props.environment as any).props;
|
|
352
|
+
expect(envProps.name).toBe("review/$CI_COMMIT_REF_SLUG");
|
|
353
|
+
expect(envProps.on_stop).toBe("review-stop");
|
|
354
|
+
expect(envProps.auto_stop_in).toBe("1 week");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("stop job has environment with action stop", () => {
|
|
358
|
+
const instance = ReviewApp(baseProps);
|
|
359
|
+
const props = (instance.stop as any).props;
|
|
360
|
+
const envProps = (props.environment as any).props;
|
|
361
|
+
expect(envProps.name).toBe("review/$CI_COMMIT_REF_SLUG");
|
|
362
|
+
expect(envProps.action).toBe("stop");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("deploy has MR rule", () => {
|
|
366
|
+
const instance = ReviewApp(baseProps);
|
|
367
|
+
const props = (instance.deploy as any).props;
|
|
368
|
+
expect(props.rules).toHaveLength(1);
|
|
369
|
+
const ruleProps = (props.rules[0] as any).props;
|
|
370
|
+
expect(ruleProps.if).toBe("$CI_MERGE_REQUEST_IID");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("stop has manual MR rule", () => {
|
|
374
|
+
const instance = ReviewApp(baseProps);
|
|
375
|
+
const props = (instance.stop as any).props;
|
|
376
|
+
const ruleProps = (props.rules[0] as any).props;
|
|
377
|
+
expect(ruleProps.if).toBe("$CI_MERGE_REQUEST_IID");
|
|
378
|
+
expect(ruleProps.when).toBe("manual");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("deploy script from string", () => {
|
|
382
|
+
const instance = ReviewApp(baseProps);
|
|
383
|
+
const props = (instance.deploy as any).props;
|
|
384
|
+
expect(props.script).toEqual(["kubectl apply -f k8s/"]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("deploy script from array", () => {
|
|
388
|
+
const instance = ReviewApp({
|
|
389
|
+
...baseProps,
|
|
390
|
+
deployScript: ["helm upgrade --install", "kubectl rollout status"],
|
|
391
|
+
});
|
|
392
|
+
const props = (instance.deploy as any).props;
|
|
393
|
+
expect(props.script).toEqual(["helm upgrade --install", "kubectl rollout status"]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("default stop script", () => {
|
|
397
|
+
const instance = ReviewApp(baseProps);
|
|
398
|
+
const props = (instance.stop as any).props;
|
|
399
|
+
expect(props.script).toEqual(['echo "Stopping review app..."']);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("custom stop script", () => {
|
|
403
|
+
const instance = ReviewApp({ ...baseProps, stopScript: "kubectl delete -f k8s/" });
|
|
404
|
+
const props = (instance.stop as any).props;
|
|
405
|
+
expect(props.script).toEqual(["kubectl delete -f k8s/"]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("custom stage", () => {
|
|
409
|
+
const instance = ReviewApp({ ...baseProps, stage: "review" });
|
|
410
|
+
const deployProps = (instance.deploy as any).props;
|
|
411
|
+
const stopProps = (instance.stop as any).props;
|
|
412
|
+
expect(deployProps.stage).toBe("review");
|
|
413
|
+
expect(stopProps.stage).toBe("review");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("custom url pattern", () => {
|
|
417
|
+
const instance = ReviewApp({
|
|
418
|
+
...baseProps,
|
|
419
|
+
urlPattern: "https://$CI_ENVIRONMENT_SLUG.myapp.dev",
|
|
420
|
+
});
|
|
421
|
+
const props = (instance.deploy as any).props;
|
|
422
|
+
const envProps = (props.environment as any).props;
|
|
423
|
+
expect(envProps.url).toBe("https://$CI_ENVIRONMENT_SLUG.myapp.dev");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("custom image is applied to both jobs", () => {
|
|
427
|
+
const { Image } = require("../generated");
|
|
428
|
+
const img = new Image({ name: "alpine:latest" });
|
|
429
|
+
const instance = ReviewApp({ ...baseProps, image: img });
|
|
430
|
+
const deployProps = (instance.deploy as any).props;
|
|
431
|
+
const stopProps = (instance.stop as any).props;
|
|
432
|
+
expect(deployProps.image).toBe(img);
|
|
433
|
+
expect(stopProps.image).toBe(img);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("expandComposite produces correct names", () => {
|
|
437
|
+
const expanded = expandComposite("review", ReviewApp(baseProps));
|
|
438
|
+
expect(expanded.size).toBe(2);
|
|
439
|
+
expect(expanded.has("reviewDeploy")).toBe(true);
|
|
440
|
+
expect(expanded.has("reviewStop")).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("on_stop name matches kebab-case of expanded stop job", () => {
|
|
444
|
+
const instance = ReviewApp({ name: "staging", deployScript: "deploy.sh" });
|
|
445
|
+
const deployProps = (instance.deploy as any).props;
|
|
446
|
+
const envProps = (deployProps.environment as any).props;
|
|
447
|
+
// The on_stop value should be "staging-stop"
|
|
448
|
+
// When expanded with prefix "staging", the stop job becomes "stagingStop"
|
|
449
|
+
// which the serializer converts to "staging-stop" in YAML
|
|
450
|
+
expect(envProps.on_stop).toBe("staging-stop");
|
|
451
|
+
});
|
|
452
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Composite } from "@intentius/chant";
|
|
2
|
+
import { Job, Image, Service, Rule } from "../generated";
|
|
3
|
+
import { CI } from "../variables";
|
|
4
|
+
|
|
5
|
+
export interface DockerBuildProps {
|
|
6
|
+
/** Job stage. Default: "build" */
|
|
7
|
+
stage?: string;
|
|
8
|
+
/** Image tag. Default: $CI_COMMIT_REF_SLUG */
|
|
9
|
+
tag?: string;
|
|
10
|
+
/** Dockerfile path. Default: "Dockerfile" */
|
|
11
|
+
dockerfile?: string;
|
|
12
|
+
/** Build context. Default: "." */
|
|
13
|
+
context?: string;
|
|
14
|
+
/** Container registry. Default: $CI_REGISTRY */
|
|
15
|
+
registry?: string;
|
|
16
|
+
/** Full image name. Default: $CI_REGISTRY_IMAGE */
|
|
17
|
+
image?: string;
|
|
18
|
+
/** Tag as :latest on default branch. Default: true */
|
|
19
|
+
tagLatest?: boolean;
|
|
20
|
+
/** Extra docker build --build-arg flags */
|
|
21
|
+
buildArgs?: Record<string, string>;
|
|
22
|
+
/** Job rules. Default: none (always runs) */
|
|
23
|
+
rules?: InstanceType<typeof Rule>[];
|
|
24
|
+
/** Docker version. Default: "27" */
|
|
25
|
+
dockerVersion?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DockerBuild = Composite<DockerBuildProps>((props) => {
|
|
29
|
+
const {
|
|
30
|
+
stage = "build",
|
|
31
|
+
tag = CI.CommitRefSlug,
|
|
32
|
+
dockerfile = "Dockerfile",
|
|
33
|
+
context = ".",
|
|
34
|
+
registry = CI.Registry,
|
|
35
|
+
image = CI.RegistryImage,
|
|
36
|
+
tagLatest = true,
|
|
37
|
+
buildArgs,
|
|
38
|
+
rules,
|
|
39
|
+
dockerVersion = "27",
|
|
40
|
+
} = props;
|
|
41
|
+
|
|
42
|
+
const buildArgFlags = buildArgs
|
|
43
|
+
? Object.entries(buildArgs)
|
|
44
|
+
.map(([k, v]) => `--build-arg ${k}=${v}`)
|
|
45
|
+
.join(" ")
|
|
46
|
+
: "";
|
|
47
|
+
|
|
48
|
+
const buildCmd = [
|
|
49
|
+
"docker build",
|
|
50
|
+
buildArgFlags,
|
|
51
|
+
`-t ${image}:${tag}`,
|
|
52
|
+
`-f ${dockerfile}`,
|
|
53
|
+
context,
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(" ");
|
|
57
|
+
|
|
58
|
+
const script: string[] = [buildCmd, `docker push ${image}:${tag}`];
|
|
59
|
+
|
|
60
|
+
if (tagLatest) {
|
|
61
|
+
script.push(
|
|
62
|
+
`if [ "${CI.CommitBranch}" = "${CI.DefaultBranch}" ]; then docker tag ${image}:${tag} ${image}:latest && docker push ${image}:latest; fi`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const build = new Job({
|
|
67
|
+
stage,
|
|
68
|
+
image: new Image({ name: `docker:${dockerVersion}-cli` }),
|
|
69
|
+
services: [new Service({ name: `docker:${dockerVersion}-dind`, alias: "docker" })],
|
|
70
|
+
variables: {
|
|
71
|
+
DOCKER_TLS_CERTDIR: "/certs",
|
|
72
|
+
},
|
|
73
|
+
before_script: [
|
|
74
|
+
`docker login -u ${CI.RegistryUser} -p ${CI.RegistryPassword} ${registry}`,
|
|
75
|
+
],
|
|
76
|
+
script,
|
|
77
|
+
...(rules ? { rules } : {}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { build };
|
|
81
|
+
}, "DockerBuild");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { DockerBuild } from "./docker-build";
|
|
2
|
+
export type { DockerBuildProps } from "./docker-build";
|
|
3
|
+
export { NodePipeline, BunPipeline, PnpmPipeline } from "./node-pipeline";
|
|
4
|
+
export type { NodePipelineProps } from "./node-pipeline";
|
|
5
|
+
export { PythonPipeline } from "./python-pipeline";
|
|
6
|
+
export type { PythonPipelineProps } from "./python-pipeline";
|
|
7
|
+
export { ReviewApp } from "./review-app";
|
|
8
|
+
export type { ReviewAppProps } from "./review-app";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Composite, withDefaults } from "@intentius/chant";
|
|
2
|
+
import { Job, Default, Image, Cache, Artifacts } from "../generated";
|
|
3
|
+
import { CI } from "../variables";
|
|
4
|
+
|
|
5
|
+
export interface NodePipelineProps {
|
|
6
|
+
/** Node.js version. Default: "22" */
|
|
7
|
+
nodeVersion?: string;
|
|
8
|
+
/** Package manager. Default: "npm" */
|
|
9
|
+
packageManager?: "npm" | "pnpm" | "bun";
|
|
10
|
+
/** Build script name (runs via package manager). Default: "build" */
|
|
11
|
+
buildScript?: string;
|
|
12
|
+
/** Test script name. Default: "test" */
|
|
13
|
+
testScript?: string;
|
|
14
|
+
/** Artifact paths from build. Default: ["dist/"] */
|
|
15
|
+
buildArtifactPaths?: string[];
|
|
16
|
+
/** Artifact expiry. Default: "1 hour" */
|
|
17
|
+
artifactExpiry?: string;
|
|
18
|
+
/** Override auto-detected install command */
|
|
19
|
+
installCommand?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cacheConfig = {
|
|
23
|
+
npm: {
|
|
24
|
+
paths: [".npm/"],
|
|
25
|
+
keyFile: "package-lock.json",
|
|
26
|
+
envVars: { npm_config_cache: `${CI.ProjectDir}/.npm/` },
|
|
27
|
+
installCmd: "npm ci",
|
|
28
|
+
runPrefix: "npm run",
|
|
29
|
+
},
|
|
30
|
+
pnpm: {
|
|
31
|
+
paths: [".pnpm-store/"],
|
|
32
|
+
keyFile: "pnpm-lock.yaml",
|
|
33
|
+
envVars: {},
|
|
34
|
+
installCmd: "pnpm install --frozen-lockfile",
|
|
35
|
+
runPrefix: "pnpm run",
|
|
36
|
+
},
|
|
37
|
+
bun: {
|
|
38
|
+
paths: [".bun/install/cache"],
|
|
39
|
+
keyFile: "bun.lock",
|
|
40
|
+
envVars: {},
|
|
41
|
+
installCmd: "bun install --frozen-lockfile",
|
|
42
|
+
runPrefix: "bun run",
|
|
43
|
+
},
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export const NodePipeline = Composite<NodePipelineProps>((props) => {
|
|
47
|
+
const {
|
|
48
|
+
nodeVersion = "22",
|
|
49
|
+
packageManager = "npm",
|
|
50
|
+
buildScript = "build",
|
|
51
|
+
testScript = "test",
|
|
52
|
+
buildArtifactPaths = ["dist/"],
|
|
53
|
+
artifactExpiry = "1 hour",
|
|
54
|
+
installCommand,
|
|
55
|
+
} = props;
|
|
56
|
+
|
|
57
|
+
const pm = cacheConfig[packageManager];
|
|
58
|
+
const install = installCommand ?? pm.installCmd;
|
|
59
|
+
const run = pm.runPrefix;
|
|
60
|
+
|
|
61
|
+
const nodeImage = new Image({ name: `node:${nodeVersion}-alpine` });
|
|
62
|
+
|
|
63
|
+
const cache = new Cache({
|
|
64
|
+
key: { files: [pm.keyFile] },
|
|
65
|
+
paths: pm.paths as unknown as string[],
|
|
66
|
+
policy: "pull-push",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const variables = Object.keys(pm.envVars).length > 0 ? pm.envVars : undefined;
|
|
70
|
+
|
|
71
|
+
const defaults = new Default({
|
|
72
|
+
image: nodeImage,
|
|
73
|
+
cache: [cache],
|
|
74
|
+
...(variables ? {} : {}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const build = new Job({
|
|
78
|
+
stage: "build",
|
|
79
|
+
script: [install, `${run} ${buildScript}`],
|
|
80
|
+
artifacts: new Artifacts({
|
|
81
|
+
paths: buildArtifactPaths,
|
|
82
|
+
expire_in: artifactExpiry,
|
|
83
|
+
}),
|
|
84
|
+
...(variables ? { variables } : {}),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const test = new Job({
|
|
88
|
+
stage: "test",
|
|
89
|
+
script: [install, `${run} ${testScript}`],
|
|
90
|
+
artifacts: new Artifacts({
|
|
91
|
+
reports: { junit: "junit.xml" },
|
|
92
|
+
when: "always",
|
|
93
|
+
}),
|
|
94
|
+
...(variables ? { variables } : {}),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { defaults, build, test };
|
|
98
|
+
}, "NodePipeline");
|
|
99
|
+
|
|
100
|
+
/** NodePipeline preset for Bun projects. */
|
|
101
|
+
export const BunPipeline = withDefaults(NodePipeline, { packageManager: "bun" as const });
|
|
102
|
+
|
|
103
|
+
/** NodePipeline preset for pnpm projects. */
|
|
104
|
+
export const PnpmPipeline = withDefaults(NodePipeline, { packageManager: "pnpm" as const });
|