@intentius/chant-lexicon-gitlab 0.0.15 → 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 +18 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/wgl016.ts +82 -0
- package/dist/rules/wgl017.ts +54 -0
- package/dist/rules/wgl018.ts +39 -0
- package/dist/rules/wgl019.ts +44 -0
- package/dist/rules/wgl020.ts +56 -0
- package/dist/rules/wgl021.ts +62 -0
- package/dist/rules/wgl022.ts +44 -0
- package/dist/rules/wgl023.ts +51 -0
- package/dist/rules/wgl024.ts +46 -0
- package/dist/rules/wgl025.ts +49 -0
- package/dist/rules/wgl026.ts +67 -0
- package/dist/rules/wgl027.ts +54 -0
- package/dist/rules/wgl028.ts +67 -0
- package/dist/rules/yaml-helpers.ts +82 -0
- package/dist/skills/chant-gitlab.md +1 -1
- package/dist/skills/gitlab-ci-patterns.md +309 -0
- package/package.json +3 -3
- package/src/codegen/fetch.test.ts +30 -0
- package/src/codegen/generate.test.ts +65 -0
- package/src/codegen/idempotency.test.ts +28 -0
- package/src/codegen/naming.test.ts +93 -0
- package/src/codegen/snapshot.test.ts +28 -19
- package/src/composites/composites.test.ts +160 -0
- package/src/coverage.test.ts +15 -7
- package/src/import/roundtrip.test.ts +132 -0
- package/src/lint/post-synth/wgl016.test.ts +72 -0
- package/src/lint/post-synth/wgl016.ts +82 -0
- package/src/lint/post-synth/wgl017.test.ts +53 -0
- package/src/lint/post-synth/wgl017.ts +54 -0
- package/src/lint/post-synth/wgl018.test.ts +69 -0
- package/src/lint/post-synth/wgl018.ts +39 -0
- package/src/lint/post-synth/wgl019.test.ts +76 -0
- package/src/lint/post-synth/wgl019.ts +44 -0
- package/src/lint/post-synth/wgl020.test.ts +54 -0
- package/src/lint/post-synth/wgl020.ts +56 -0
- package/src/lint/post-synth/wgl021.test.ts +62 -0
- package/src/lint/post-synth/wgl021.ts +62 -0
- package/src/lint/post-synth/wgl022.test.ts +86 -0
- package/src/lint/post-synth/wgl022.ts +44 -0
- package/src/lint/post-synth/wgl023.test.ts +88 -0
- package/src/lint/post-synth/wgl023.ts +51 -0
- package/src/lint/post-synth/wgl024.test.ts +77 -0
- package/src/lint/post-synth/wgl024.ts +46 -0
- package/src/lint/post-synth/wgl025.test.ts +85 -0
- package/src/lint/post-synth/wgl025.ts +49 -0
- package/src/lint/post-synth/wgl026.test.ts +87 -0
- package/src/lint/post-synth/wgl026.ts +67 -0
- package/src/lint/post-synth/wgl027.test.ts +84 -0
- package/src/lint/post-synth/wgl027.ts +54 -0
- package/src/lint/post-synth/wgl028.test.ts +95 -0
- package/src/lint/post-synth/wgl028.ts +67 -0
- package/src/lint/post-synth/yaml-helpers.ts +82 -0
- package/src/lsp/completions.test.ts +16 -6
- package/src/lsp/hover.test.ts +18 -7
- package/src/plugin.test.ts +15 -2
- package/src/plugin.ts +66 -3
- package/src/skills/gitlab-ci-patterns.md +309 -0
- package/src/testdata/pipelines/deploy-envs.gitlab-ci.yml +60 -0
- package/src/testdata/pipelines/docker-build.gitlab-ci.yml +41 -0
- package/src/testdata/pipelines/includes-templates.gitlab-ci.yml +52 -0
- package/src/testdata/pipelines/monorepo.gitlab-ci.yml +51 -0
- package/src/testdata/pipelines/multi-stage.gitlab-ci.yml +56 -0
- package/src/testdata/pipelines/simple.gitlab-ci.yml +9 -0
- package/src/validate.test.ts +12 -6
- package/src/variables.test.ts +58 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { gitlabShortName, type GitLabParseResult } from "./parse";
|
|
3
|
+
import { NamingStrategy } from "./naming";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Chant's naming philosophy for GitLab: simple, flat names.
|
|
7
|
+
* Every priorityName should be the short name from the GitLab::CI:: namespace.
|
|
8
|
+
* No "CI" prefix should leak into class names.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Build a minimal GitLabParseResult for a given typeName. */
|
|
12
|
+
function stubResult(typeName: string, isProperty?: boolean): GitLabParseResult {
|
|
13
|
+
return {
|
|
14
|
+
resource: {
|
|
15
|
+
typeName,
|
|
16
|
+
properties: [],
|
|
17
|
+
attributes: [],
|
|
18
|
+
deprecatedProperties: [],
|
|
19
|
+
},
|
|
20
|
+
propertyTypes: [],
|
|
21
|
+
enums: [],
|
|
22
|
+
isProperty,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("naming spec fidelity", () => {
|
|
27
|
+
const typeNames = [
|
|
28
|
+
"GitLab::CI::Job",
|
|
29
|
+
"GitLab::CI::Default",
|
|
30
|
+
"GitLab::CI::Workflow",
|
|
31
|
+
"GitLab::CI::Artifacts",
|
|
32
|
+
"GitLab::CI::Cache",
|
|
33
|
+
"GitLab::CI::Image",
|
|
34
|
+
"GitLab::CI::Rule",
|
|
35
|
+
"GitLab::CI::Retry",
|
|
36
|
+
"GitLab::CI::AllowFailure",
|
|
37
|
+
"GitLab::CI::Parallel",
|
|
38
|
+
"GitLab::CI::Include",
|
|
39
|
+
"GitLab::CI::Release",
|
|
40
|
+
"GitLab::CI::Environment",
|
|
41
|
+
"GitLab::CI::Trigger",
|
|
42
|
+
"GitLab::CI::AutoCancel",
|
|
43
|
+
"GitLab::CI::WorkflowRule",
|
|
44
|
+
"GitLab::CI::Need",
|
|
45
|
+
"GitLab::CI::Inherit",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const results = typeNames.map((t) => stubResult(t));
|
|
49
|
+
const strategy = new NamingStrategy(results);
|
|
50
|
+
|
|
51
|
+
test("all priority names resolve to short name", () => {
|
|
52
|
+
for (const typeName of typeNames) {
|
|
53
|
+
const resolved = strategy.resolve(typeName);
|
|
54
|
+
const shortName = gitlabShortName(typeName);
|
|
55
|
+
expect(resolved).toBe(shortName);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("no priority name has CI prefix", () => {
|
|
60
|
+
for (const typeName of typeNames) {
|
|
61
|
+
const resolved = strategy.resolve(typeName)!;
|
|
62
|
+
// "CI" as a prefix (e.g. "CIJob") would be wrong; "Cache" starting with "C" is fine
|
|
63
|
+
expect(resolved.startsWith("CI")).toBe(false);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("no priority name has GitLab prefix", () => {
|
|
68
|
+
for (const typeName of typeNames) {
|
|
69
|
+
const resolved = strategy.resolve(typeName)!;
|
|
70
|
+
expect(resolved.startsWith("GitLab")).toBe(false);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("all 18 priority names are mapped", () => {
|
|
75
|
+
expect(typeNames).toHaveLength(18);
|
|
76
|
+
for (const typeName of typeNames) {
|
|
77
|
+
expect(strategy.resolve(typeName)).toBeDefined();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("property type naming", () => {
|
|
83
|
+
test("resolves property entities correctly", () => {
|
|
84
|
+
const results = [
|
|
85
|
+
stubResult("GitLab::CI::Artifacts", true),
|
|
86
|
+
stubResult("GitLab::CI::Cache", true),
|
|
87
|
+
];
|
|
88
|
+
const strategy = new NamingStrategy(results);
|
|
89
|
+
|
|
90
|
+
expect(strategy.resolve("GitLab::CI::Artifacts")).toBe("Artifacts");
|
|
91
|
+
expect(strategy.resolve("GitLab::CI::Cache")).toBe("Cache");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { readFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
|
|
6
6
|
const generatedDir = join(dirname(dirname(fileURLToPath(import.meta.url))), "generated");
|
|
7
|
+
const hasGenerated = existsSync(join(generatedDir, "lexicon-gitlab.json"));
|
|
7
8
|
|
|
8
9
|
describe("generated lexicon-gitlab.json", () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
test("is valid JSON with expected entries", () => {
|
|
10
|
+
test.skipIf(!hasGenerated)("is valid JSON with expected entries", () => {
|
|
11
|
+
const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
|
|
12
|
+
const registry = JSON.parse(content);
|
|
13
13
|
expect(Object.keys(registry)).toHaveLength(19);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
test("contains all resource entities", () => {
|
|
16
|
+
test.skipIf(!hasGenerated)("contains all resource entities", () => {
|
|
17
|
+
const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
|
|
18
|
+
const registry = JSON.parse(content);
|
|
17
19
|
expect(registry.Job).toBeDefined();
|
|
18
20
|
expect(registry.Job.kind).toBe("resource");
|
|
19
21
|
expect(registry.Job.resourceType).toBe("GitLab::CI::Job");
|
|
@@ -26,7 +28,9 @@ describe("generated lexicon-gitlab.json", () => {
|
|
|
26
28
|
expect(registry.Workflow.kind).toBe("resource");
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
test("contains all property entities", () => {
|
|
31
|
+
test.skipIf(!hasGenerated)("contains all property entities", () => {
|
|
32
|
+
const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
|
|
33
|
+
const registry = JSON.parse(content);
|
|
30
34
|
const propertyNames = [
|
|
31
35
|
"AllowFailure", "Artifacts", "AutoCancel", "Cache",
|
|
32
36
|
"Environment", "Image", "Include", "Inherit", "Parallel",
|
|
@@ -40,7 +44,9 @@ describe("generated lexicon-gitlab.json", () => {
|
|
|
40
44
|
}
|
|
41
45
|
});
|
|
42
46
|
|
|
43
|
-
test("entries match snapshot", () => {
|
|
47
|
+
test.skipIf(!hasGenerated)("entries match snapshot", () => {
|
|
48
|
+
const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
|
|
49
|
+
const registry = JSON.parse(content);
|
|
44
50
|
expect(registry.Job).toMatchSnapshot();
|
|
45
51
|
expect(registry.Artifacts).toMatchSnapshot();
|
|
46
52
|
expect(registry.Cache).toMatchSnapshot();
|
|
@@ -49,9 +55,8 @@ describe("generated lexicon-gitlab.json", () => {
|
|
|
49
55
|
});
|
|
50
56
|
|
|
51
57
|
describe("generated index.d.ts", () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
test("contains all class declarations", () => {
|
|
58
|
+
test.skipIf(!hasGenerated)("contains all class declarations", () => {
|
|
59
|
+
const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
|
|
55
60
|
const expectedClasses = [
|
|
56
61
|
"Job", "Default", "Workflow",
|
|
57
62
|
"AllowFailure", "Artifacts", "AutoCancel", "Cache",
|
|
@@ -64,14 +69,16 @@ describe("generated index.d.ts", () => {
|
|
|
64
69
|
}
|
|
65
70
|
});
|
|
66
71
|
|
|
67
|
-
test("contains CI variables declaration", () => {
|
|
72
|
+
test.skipIf(!hasGenerated)("contains CI variables declaration", () => {
|
|
73
|
+
const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
|
|
68
74
|
expect(content).toContain("export declare const CI");
|
|
69
75
|
expect(content).toContain("readonly CommitBranch: string");
|
|
70
76
|
expect(content).toContain("readonly CommitSha: string");
|
|
71
77
|
expect(content).toContain("readonly PipelineId: string");
|
|
72
78
|
});
|
|
73
79
|
|
|
74
|
-
test("Job class has key properties in constructor", () => {
|
|
80
|
+
test.skipIf(!hasGenerated)("Job class has key properties in constructor", () => {
|
|
81
|
+
const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
|
|
75
82
|
// Extract the Job class declaration
|
|
76
83
|
const jobMatch = content.match(/export declare class Job \{[\s\S]*?\n\}/);
|
|
77
84
|
expect(jobMatch).toBeDefined();
|
|
@@ -85,27 +92,29 @@ describe("generated index.d.ts", () => {
|
|
|
85
92
|
});
|
|
86
93
|
|
|
87
94
|
describe("generated index.ts", () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
test("has correct resource createResource calls", () => {
|
|
95
|
+
test.skipIf(!hasGenerated)("has correct resource createResource calls", () => {
|
|
96
|
+
const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
|
|
91
97
|
expect(content).toContain('createResource("GitLab::CI::Default"');
|
|
92
98
|
expect(content).toContain('createResource("GitLab::CI::Job"');
|
|
93
99
|
expect(content).toContain('createResource("GitLab::CI::Workflow"');
|
|
94
100
|
});
|
|
95
101
|
|
|
96
|
-
test("has correct property createProperty calls", () => {
|
|
102
|
+
test.skipIf(!hasGenerated)("has correct property createProperty calls", () => {
|
|
103
|
+
const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
|
|
97
104
|
expect(content).toContain('createProperty("GitLab::CI::Artifacts"');
|
|
98
105
|
expect(content).toContain('createProperty("GitLab::CI::Cache"');
|
|
99
106
|
expect(content).toContain('createProperty("GitLab::CI::Image"');
|
|
100
107
|
expect(content).toContain('createProperty("GitLab::CI::Rule"');
|
|
101
108
|
});
|
|
102
109
|
|
|
103
|
-
test("re-exports reference and CI", () => {
|
|
110
|
+
test.skipIf(!hasGenerated)("re-exports reference and CI", () => {
|
|
111
|
+
const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
|
|
104
112
|
expect(content).toContain('export { reference } from "../intrinsics"');
|
|
105
113
|
expect(content).toContain('export { CI } from "../variables"');
|
|
106
114
|
});
|
|
107
115
|
|
|
108
|
-
test("imports from runtime", () => {
|
|
116
|
+
test.skipIf(!hasGenerated)("imports from runtime", () => {
|
|
117
|
+
const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
|
|
109
118
|
expect(content).toContain('from "./runtime"');
|
|
110
119
|
});
|
|
111
120
|
});
|
|
@@ -98,6 +98,46 @@ describe("DockerBuild", () => {
|
|
|
98
98
|
expect(expanded.has("dockerBuild")).toBe(true);
|
|
99
99
|
expect(expanded.size).toBe(1);
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
test("default dockerfile and context", () => {
|
|
103
|
+
const instance = DockerBuild({});
|
|
104
|
+
const props = (instance.build as any).props;
|
|
105
|
+
expect(props.script[0]).toContain("-f Dockerfile");
|
|
106
|
+
expect(props.script[0]).toMatch(/\.\s*$/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("custom dockerfile path", () => {
|
|
110
|
+
const instance = DockerBuild({ dockerfile: "docker/app.Dockerfile" });
|
|
111
|
+
const props = (instance.build as any).props;
|
|
112
|
+
expect(props.script[0]).toContain("-f docker/app.Dockerfile");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("custom context directory", () => {
|
|
116
|
+
const instance = DockerBuild({ context: "./app" });
|
|
117
|
+
const props = (instance.build as any).props;
|
|
118
|
+
expect(props.script[0]).toContain("./app");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("empty buildArgs produces no --build-arg flags", () => {
|
|
122
|
+
const instance = DockerBuild({ buildArgs: {} });
|
|
123
|
+
const props = (instance.build as any).props;
|
|
124
|
+
expect(props.script[0]).not.toContain("--build-arg");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("multiple buildArgs are all present", () => {
|
|
128
|
+
const instance = DockerBuild({
|
|
129
|
+
buildArgs: { NODE_ENV: "production", VERSION: "1.0" },
|
|
130
|
+
});
|
|
131
|
+
const props = (instance.build as any).props;
|
|
132
|
+
expect(props.script[0]).toContain("--build-arg NODE_ENV=production");
|
|
133
|
+
expect(props.script[0]).toContain("--build-arg VERSION=1.0");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("no rules by default", () => {
|
|
137
|
+
const instance = DockerBuild({});
|
|
138
|
+
const props = (instance.build as any).props;
|
|
139
|
+
expect(props.rules).toBeUndefined();
|
|
140
|
+
});
|
|
101
141
|
});
|
|
102
142
|
|
|
103
143
|
// ---------------------------------------------------------------------------
|
|
@@ -201,6 +241,52 @@ describe("NodePipeline", () => {
|
|
|
201
241
|
expect(expanded.has("appBuild")).toBe(true);
|
|
202
242
|
expect(expanded.has("appTest")).toBe(true);
|
|
203
243
|
});
|
|
244
|
+
|
|
245
|
+
test("custom artifact paths", () => {
|
|
246
|
+
const instance = NodePipeline({ buildArtifactPaths: ["build/", "out/"] });
|
|
247
|
+
const props = (instance.build as any).props;
|
|
248
|
+
const artProps = (props.artifacts as any).props;
|
|
249
|
+
expect(artProps.paths).toEqual(["build/", "out/"]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("custom artifact expiry", () => {
|
|
253
|
+
const instance = NodePipeline({ artifactExpiry: "30 minutes" });
|
|
254
|
+
const props = (instance.build as any).props;
|
|
255
|
+
const artProps = (props.artifacts as any).props;
|
|
256
|
+
expect(artProps.expire_in).toBe("30 minutes");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("cache has pull-push policy", () => {
|
|
260
|
+
const instance = NodePipeline({});
|
|
261
|
+
const defaultProps = (instance.defaults as any).props;
|
|
262
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
263
|
+
expect(cacheProps.policy).toBe("pull-push");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("pnpm install command", () => {
|
|
267
|
+
const instance = NodePipeline({ packageManager: "pnpm" });
|
|
268
|
+
const buildProps = (instance.build as any).props;
|
|
269
|
+
expect(buildProps.script[0]).toBe("pnpm install --frozen-lockfile");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("bun install command", () => {
|
|
273
|
+
const instance = NodePipeline({ packageManager: "bun" });
|
|
274
|
+
const buildProps = (instance.build as any).props;
|
|
275
|
+
expect(buildProps.script[0]).toBe("bun install --frozen-lockfile");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("test job stage is test", () => {
|
|
279
|
+
const instance = NodePipeline({});
|
|
280
|
+
const props = (instance.test as any).props;
|
|
281
|
+
expect(props.stage).toBe("test");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("test artifacts always reported", () => {
|
|
285
|
+
const instance = NodePipeline({});
|
|
286
|
+
const props = (instance.test as any).props;
|
|
287
|
+
const artProps = (props.artifacts as any).props;
|
|
288
|
+
expect(artProps.when).toBe("always");
|
|
289
|
+
});
|
|
204
290
|
});
|
|
205
291
|
|
|
206
292
|
// ---------------------------------------------------------------------------
|
|
@@ -329,6 +415,42 @@ describe("PythonPipeline", () => {
|
|
|
329
415
|
expect(expanded.has("pyTest")).toBe(true);
|
|
330
416
|
expect(expanded.has("pyLint")).toBe(false);
|
|
331
417
|
});
|
|
418
|
+
|
|
419
|
+
test("custom requirements file", () => {
|
|
420
|
+
const instance = PythonPipeline({ requirementsFile: "requirements-dev.txt" });
|
|
421
|
+
const defaultProps = (instance.defaults as any).props;
|
|
422
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
423
|
+
expect(cacheProps.key).toEqual({ files: ["requirements-dev.txt"] });
|
|
424
|
+
expect(defaultProps.before_script).toContain("pip install -r requirements-dev.txt");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("poetry mode sets virtualenvs.in-project config", () => {
|
|
428
|
+
const instance = PythonPipeline({ usePoetry: true });
|
|
429
|
+
const defaultProps = (instance.defaults as any).props;
|
|
430
|
+
expect(defaultProps.before_script).toContain("poetry config virtualenvs.in-project true");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("custom lint command", () => {
|
|
434
|
+
const instance = PythonPipeline({ lintCommand: "flake8 src/" });
|
|
435
|
+
const members = instance.members as any;
|
|
436
|
+
const props = (members.lint as any).props;
|
|
437
|
+
expect(props.script).toContain("flake8 src/");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("test and lint jobs share the test stage", () => {
|
|
441
|
+
const instance = PythonPipeline({});
|
|
442
|
+
const testProps = (instance.test as any).props;
|
|
443
|
+
const lintProps = ((instance.members as any).lint as any).props;
|
|
444
|
+
expect(testProps.stage).toBe("test");
|
|
445
|
+
expect(lintProps.stage).toBe("test");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("cache has pull-push policy", () => {
|
|
449
|
+
const instance = PythonPipeline({});
|
|
450
|
+
const defaultProps = (instance.defaults as any).props;
|
|
451
|
+
const cacheProps = (defaultProps.cache[0] as any).props;
|
|
452
|
+
expect(cacheProps.policy).toBe("pull-push");
|
|
453
|
+
});
|
|
332
454
|
});
|
|
333
455
|
|
|
334
456
|
// ---------------------------------------------------------------------------
|
|
@@ -449,4 +571,42 @@ describe("ReviewApp", () => {
|
|
|
449
571
|
// which the serializer converts to "staging-stop" in YAML
|
|
450
572
|
expect(envProps.on_stop).toBe("staging-stop");
|
|
451
573
|
});
|
|
574
|
+
|
|
575
|
+
test("default auto_stop_in is 1 week", () => {
|
|
576
|
+
const instance = ReviewApp(baseProps);
|
|
577
|
+
const props = (instance.deploy as any).props;
|
|
578
|
+
const envProps = (props.environment as any).props;
|
|
579
|
+
expect(envProps.auto_stop_in).toBe("1 week");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("custom auto_stop_in", () => {
|
|
583
|
+
const instance = ReviewApp({ ...baseProps, autoStopIn: "2 days" });
|
|
584
|
+
const props = (instance.deploy as any).props;
|
|
585
|
+
const envProps = (props.environment as any).props;
|
|
586
|
+
expect(envProps.auto_stop_in).toBe("2 days");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("default stage is deploy for both jobs", () => {
|
|
590
|
+
const instance = ReviewApp(baseProps);
|
|
591
|
+
const deployProps = (instance.deploy as any).props;
|
|
592
|
+
const stopProps = (instance.stop as any).props;
|
|
593
|
+
expect(deployProps.stage).toBe("deploy");
|
|
594
|
+
expect(stopProps.stage).toBe("deploy");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("stop script as array", () => {
|
|
598
|
+
const instance = ReviewApp({
|
|
599
|
+
...baseProps,
|
|
600
|
+
stopScript: ["helm uninstall app", "kubectl delete ns review"],
|
|
601
|
+
});
|
|
602
|
+
const props = (instance.stop as any).props;
|
|
603
|
+
expect(props.script).toEqual(["helm uninstall app", "kubectl delete ns review"]);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("deploy environment url defaults to CI_ENVIRONMENT_SLUG pattern", () => {
|
|
607
|
+
const instance = ReviewApp(baseProps);
|
|
608
|
+
const props = (instance.deploy as any).props;
|
|
609
|
+
const envProps = (props.environment as any).props;
|
|
610
|
+
expect(envProps.url).toContain("$CI_ENVIRONMENT_SLUG");
|
|
611
|
+
});
|
|
452
612
|
});
|
package/src/coverage.test.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import { readFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
3
|
import { join, dirname } from "path";
|
|
5
4
|
import { fileURLToPath } from "url";
|
|
6
5
|
|
|
7
6
|
const basePath = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
-
const
|
|
7
|
+
const lexiconPath = join(basePath, "src", "generated", "lexicon-gitlab.json");
|
|
8
|
+
const hasGenerated = existsSync(lexiconPath);
|
|
9
9
|
|
|
10
10
|
describe("coverage analysis", () => {
|
|
11
|
-
test("computes coverage for GitLab lexicon", () => {
|
|
11
|
+
test.skipIf(!hasGenerated)("computes coverage for GitLab lexicon", async () => {
|
|
12
|
+
const { computeCoverage } = await import("./coverage");
|
|
13
|
+
const lexiconJSON = readFileSync(lexiconPath, "utf-8");
|
|
12
14
|
const report = computeCoverage(lexiconJSON);
|
|
13
15
|
expect(report.resourceCount).toBe(3); // Job, Default, Workflow
|
|
14
16
|
expect(report.resources).toHaveLength(3);
|
|
15
17
|
});
|
|
16
18
|
|
|
17
|
-
test("reports resource names correctly", () => {
|
|
19
|
+
test.skipIf(!hasGenerated)("reports resource names correctly", async () => {
|
|
20
|
+
const { computeCoverage } = await import("./coverage");
|
|
21
|
+
const lexiconJSON = readFileSync(lexiconPath, "utf-8");
|
|
18
22
|
const report = computeCoverage(lexiconJSON);
|
|
19
23
|
const names = report.resources.map((r) => r.name);
|
|
20
24
|
expect(names).toContain("Job");
|
|
@@ -22,7 +26,9 @@ describe("coverage analysis", () => {
|
|
|
22
26
|
expect(names).toContain("Workflow");
|
|
23
27
|
});
|
|
24
28
|
|
|
25
|
-
test("overallPct returns a number", () => {
|
|
29
|
+
test.skipIf(!hasGenerated)("overallPct returns a number", async () => {
|
|
30
|
+
const { computeCoverage, overallPct } = await import("./coverage");
|
|
31
|
+
const lexiconJSON = readFileSync(lexiconPath, "utf-8");
|
|
26
32
|
const report = computeCoverage(lexiconJSON);
|
|
27
33
|
const pct = overallPct(report);
|
|
28
34
|
expect(typeof pct).toBe("number");
|
|
@@ -30,7 +36,9 @@ describe("coverage analysis", () => {
|
|
|
30
36
|
expect(pct).toBeLessThanOrEqual(100);
|
|
31
37
|
});
|
|
32
38
|
|
|
33
|
-
test("formatSummary returns readable string", () => {
|
|
39
|
+
test.skipIf(!hasGenerated)("formatSummary returns readable string", async () => {
|
|
40
|
+
const { computeCoverage, formatSummary } = await import("./coverage");
|
|
41
|
+
const lexiconJSON = readFileSync(lexiconPath, "utf-8");
|
|
34
42
|
const report = computeCoverage(lexiconJSON);
|
|
35
43
|
const summary = formatSummary(report);
|
|
36
44
|
expect(summary).toContain("Coverage Report");
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { readFileSync, readdirSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
2
5
|
import { GitLabParser } from "./parser";
|
|
3
6
|
import { GitLabGenerator } from "./generator";
|
|
4
7
|
|
|
5
8
|
const parser = new GitLabParser();
|
|
6
9
|
const generator = new GitLabGenerator();
|
|
7
10
|
|
|
11
|
+
const pipelinesDir = join(
|
|
12
|
+
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
13
|
+
"testdata",
|
|
14
|
+
"pipelines",
|
|
15
|
+
);
|
|
16
|
+
|
|
8
17
|
describe("roundtrip: parse → generate", () => {
|
|
18
|
+
// ---- inline tests (existing smoke tests) ----
|
|
19
|
+
|
|
9
20
|
test("simple pipeline roundtrip", () => {
|
|
10
21
|
const yaml = `
|
|
11
22
|
stages:
|
|
@@ -86,4 +97,125 @@ test-job:
|
|
|
86
97
|
expect(content).toContain("new Workflow(");
|
|
87
98
|
expect(content).toContain("new Job(");
|
|
88
99
|
});
|
|
100
|
+
|
|
101
|
+
// ---- fixture-based tests ----
|
|
102
|
+
|
|
103
|
+
test("simple.gitlab-ci.yml fixture roundtrip", () => {
|
|
104
|
+
const yaml = readFileSync(join(pipelinesDir, "simple.gitlab-ci.yml"), "utf-8");
|
|
105
|
+
const ir = parser.parse(yaml);
|
|
106
|
+
const files = generator.generate(ir);
|
|
107
|
+
|
|
108
|
+
expect(files).toHaveLength(1);
|
|
109
|
+
const content = files[0].content;
|
|
110
|
+
|
|
111
|
+
expect(ir.resources).toHaveLength(1);
|
|
112
|
+
expect(content).toContain("new Job(");
|
|
113
|
+
expect(content).toContain("npm ci");
|
|
114
|
+
expect(content).toContain("npm test");
|
|
115
|
+
expect(content).toContain("node:22-alpine");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("multi-stage.gitlab-ci.yml fixture roundtrip", () => {
|
|
119
|
+
const yaml = readFileSync(join(pipelinesDir, "multi-stage.gitlab-ci.yml"), "utf-8");
|
|
120
|
+
const ir = parser.parse(yaml);
|
|
121
|
+
const files = generator.generate(ir);
|
|
122
|
+
|
|
123
|
+
const content = files[0].content;
|
|
124
|
+
|
|
125
|
+
// 4 jobs: build-app, lint, unit-tests, deploy-staging
|
|
126
|
+
expect(ir.resources).toHaveLength(4);
|
|
127
|
+
expect(content).toContain("buildApp");
|
|
128
|
+
expect(content).toContain("lint");
|
|
129
|
+
expect(content).toContain("unitTests");
|
|
130
|
+
expect(content).toContain("deployStaging");
|
|
131
|
+
expect(content).toContain("Pipeline stages: build, test, deploy");
|
|
132
|
+
// Artifacts and cache references
|
|
133
|
+
expect(content).toContain("dist/");
|
|
134
|
+
expect(content).toContain("package-lock.json");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("docker-build.gitlab-ci.yml fixture roundtrip", () => {
|
|
138
|
+
const yaml = readFileSync(join(pipelinesDir, "docker-build.gitlab-ci.yml"), "utf-8");
|
|
139
|
+
const ir = parser.parse(yaml);
|
|
140
|
+
const files = generator.generate(ir);
|
|
141
|
+
|
|
142
|
+
const content = files[0].content;
|
|
143
|
+
|
|
144
|
+
expect(ir.resources).toHaveLength(2);
|
|
145
|
+
expect(content).toContain("buildImage");
|
|
146
|
+
expect(content).toContain("pushLatest");
|
|
147
|
+
expect(content).toContain("docker:27-cli");
|
|
148
|
+
expect(content).toContain("docker:27-dind");
|
|
149
|
+
expect(content).toContain("docker build");
|
|
150
|
+
expect(content).toContain("docker push");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("deploy-envs.gitlab-ci.yml fixture roundtrip", () => {
|
|
154
|
+
const yaml = readFileSync(join(pipelinesDir, "deploy-envs.gitlab-ci.yml"), "utf-8");
|
|
155
|
+
const ir = parser.parse(yaml);
|
|
156
|
+
const files = generator.generate(ir);
|
|
157
|
+
|
|
158
|
+
const content = files[0].content;
|
|
159
|
+
|
|
160
|
+
// 5 jobs: build, deploy-review, stop-review, deploy-staging, deploy-production
|
|
161
|
+
expect(ir.resources).toHaveLength(5);
|
|
162
|
+
expect(content).toContain("deployReview");
|
|
163
|
+
expect(content).toContain("stopReview");
|
|
164
|
+
expect(content).toContain("deployStaging");
|
|
165
|
+
expect(content).toContain("deployProduction");
|
|
166
|
+
// Environment constructs
|
|
167
|
+
expect(content).toContain("review/$CI_COMMIT_REF_SLUG");
|
|
168
|
+
expect(content).toContain("staging");
|
|
169
|
+
expect(content).toContain("production");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("includes-templates.gitlab-ci.yml fixture roundtrip", () => {
|
|
173
|
+
const yaml = readFileSync(join(pipelinesDir, "includes-templates.gitlab-ci.yml"), "utf-8");
|
|
174
|
+
const ir = parser.parse(yaml);
|
|
175
|
+
const files = generator.generate(ir);
|
|
176
|
+
|
|
177
|
+
const content = files[0].content;
|
|
178
|
+
|
|
179
|
+
// default + build + unit-test + integration-test + deploy
|
|
180
|
+
expect(ir.resources.length).toBeGreaterThanOrEqual(3);
|
|
181
|
+
expect(content).toContain("new Default(");
|
|
182
|
+
// Include references should be captured as comments
|
|
183
|
+
expect(content).toContain("Auto-DevOps.gitlab-ci.yml");
|
|
184
|
+
expect(content).toContain("interruptible");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("monorepo.gitlab-ci.yml fixture roundtrip", () => {
|
|
188
|
+
const yaml = readFileSync(join(pipelinesDir, "monorepo.gitlab-ci.yml"), "utf-8");
|
|
189
|
+
const ir = parser.parse(yaml);
|
|
190
|
+
const files = generator.generate(ir);
|
|
191
|
+
|
|
192
|
+
const content = files[0].content;
|
|
193
|
+
|
|
194
|
+
// frontend, backend, e2e-matrix, deploy-all
|
|
195
|
+
expect(ir.resources).toHaveLength(4);
|
|
196
|
+
expect(content).toContain("frontend");
|
|
197
|
+
expect(content).toContain("backend");
|
|
198
|
+
expect(content).toContain("e2eMatrix");
|
|
199
|
+
expect(content).toContain("deployAll");
|
|
200
|
+
// Monorepo-specific constructs
|
|
201
|
+
expect(content).toContain("cypress");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---- fixture directory sweep ----
|
|
205
|
+
|
|
206
|
+
test("all pipeline fixtures parse and generate without error", () => {
|
|
207
|
+
const fixtures = readdirSync(pipelinesDir).filter((f) => f.endsWith(".yml"));
|
|
208
|
+
expect(fixtures.length).toBeGreaterThanOrEqual(6);
|
|
209
|
+
|
|
210
|
+
for (const fixture of fixtures) {
|
|
211
|
+
const yaml = readFileSync(join(pipelinesDir, fixture), "utf-8");
|
|
212
|
+
const ir = parser.parse(yaml);
|
|
213
|
+
expect(ir.resources.length).toBeGreaterThan(0);
|
|
214
|
+
|
|
215
|
+
const files = generator.generate(ir);
|
|
216
|
+
expect(files.length).toBeGreaterThan(0);
|
|
217
|
+
expect(files[0].content).toContain("import");
|
|
218
|
+
expect(files[0].content).toContain("export const");
|
|
219
|
+
}
|
|
220
|
+
});
|
|
89
221
|
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { wgl016, checkSecretsInVariables } from "./wgl016";
|
|
3
|
+
|
|
4
|
+
describe("WGL016: Secrets in Variables", () => {
|
|
5
|
+
test("check metadata", () => {
|
|
6
|
+
expect(wgl016.id).toBe("WGL016");
|
|
7
|
+
expect(wgl016.description).toContain("Secrets");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("flags hardcoded password variable", () => {
|
|
11
|
+
const yaml = `variables:
|
|
12
|
+
DB_PASSWORD: my-secret-pass123
|
|
13
|
+
DB_HOST: postgres
|
|
14
|
+
`;
|
|
15
|
+
const diags = checkSecretsInVariables(yaml);
|
|
16
|
+
expect(diags).toHaveLength(1);
|
|
17
|
+
expect(diags[0].checkId).toBe("WGL016");
|
|
18
|
+
expect(diags[0].severity).toBe("error");
|
|
19
|
+
expect(diags[0].message).toContain("DB_PASSWORD");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("flags hardcoded token variable", () => {
|
|
23
|
+
const yaml = `variables:
|
|
24
|
+
API_TOKEN: abc123xyz
|
|
25
|
+
`;
|
|
26
|
+
const diags = checkSecretsInVariables(yaml);
|
|
27
|
+
expect(diags).toHaveLength(1);
|
|
28
|
+
expect(diags[0].message).toContain("API_TOKEN");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("flags hardcoded secret variable", () => {
|
|
32
|
+
const yaml = `variables:
|
|
33
|
+
APP_SECRET: supersecretvalue
|
|
34
|
+
`;
|
|
35
|
+
const diags = checkSecretsInVariables(yaml);
|
|
36
|
+
expect(diags).toHaveLength(1);
|
|
37
|
+
expect(diags[0].message).toContain("APP_SECRET");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("does not flag variable references", () => {
|
|
41
|
+
const yaml = `variables:
|
|
42
|
+
DB_PASSWORD: $CI_DB_PASSWORD
|
|
43
|
+
API_TOKEN: \${SECRET_TOKEN}
|
|
44
|
+
`;
|
|
45
|
+
const diags = checkSecretsInVariables(yaml);
|
|
46
|
+
expect(diags).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("does not flag non-secret variables", () => {
|
|
50
|
+
const yaml = `variables:
|
|
51
|
+
NODE_ENV: production
|
|
52
|
+
DB_HOST: postgres
|
|
53
|
+
STAGE: deploy
|
|
54
|
+
`;
|
|
55
|
+
const diags = checkSecretsInVariables(yaml);
|
|
56
|
+
expect(diags).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("flags multiple secret variables", () => {
|
|
60
|
+
const yaml = `variables:
|
|
61
|
+
DB_PASSWORD: pass123
|
|
62
|
+
API_SECRET: secret456
|
|
63
|
+
`;
|
|
64
|
+
const diags = checkSecretsInVariables(yaml);
|
|
65
|
+
expect(diags).toHaveLength(2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("no diagnostics on empty yaml", () => {
|
|
69
|
+
const diags = checkSecretsInVariables("");
|
|
70
|
+
expect(diags).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
});
|