@intentius/chant-lexicon-gitlab 0.0.8 → 0.0.10
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
package/src/serializer.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { gitlabSerializer } from "./serializer";
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { createProperty, createResource } from "@intentius/chant/runtime";
|
|
4
5
|
|
|
5
6
|
// ── Mock entities ──────────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -258,6 +259,145 @@ describe("string quoting", () => {
|
|
|
258
259
|
});
|
|
259
260
|
});
|
|
260
261
|
|
|
262
|
+
describe("runtime-style entities (non-enumerable props)", () => {
|
|
263
|
+
// Real generated entities use createResource/createProperty which define
|
|
264
|
+
// DECLARABLE_MARKER, entityType, kind, and props as non-enumerable.
|
|
265
|
+
// Object.entries() skips non-enumerable properties — the serializer must
|
|
266
|
+
// handle these correctly.
|
|
267
|
+
|
|
268
|
+
const RuntimeImage = createProperty("GitLab::CI::Image", "gitlab");
|
|
269
|
+
const RuntimeCache = createProperty("GitLab::CI::Cache", "gitlab");
|
|
270
|
+
const RuntimeArtifacts = createProperty("GitLab::CI::Artifacts", "gitlab");
|
|
271
|
+
const RuntimeRule = createProperty("GitLab::CI::Rule", "gitlab");
|
|
272
|
+
const RuntimeEnvironment = createProperty("GitLab::CI::Environment", "gitlab");
|
|
273
|
+
const RuntimeJob = createResource("GitLab::CI::Job", "gitlab", {});
|
|
274
|
+
|
|
275
|
+
test("serializes runtime-style property entities in jobs", () => {
|
|
276
|
+
const image = new RuntimeImage({ name: "node:20-alpine" });
|
|
277
|
+
const cache = new RuntimeCache({
|
|
278
|
+
key: "$CI_COMMIT_REF_SLUG",
|
|
279
|
+
paths: ["node_modules/"],
|
|
280
|
+
policy: "pull-push",
|
|
281
|
+
});
|
|
282
|
+
const artifacts = new RuntimeArtifacts({
|
|
283
|
+
paths: ["dist/"],
|
|
284
|
+
expire_in: "1 hour",
|
|
285
|
+
});
|
|
286
|
+
const job = new RuntimeJob({
|
|
287
|
+
stage: "build",
|
|
288
|
+
image,
|
|
289
|
+
cache,
|
|
290
|
+
script: ["npm ci", "npm run build"],
|
|
291
|
+
artifacts,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const entities = new Map<string, Declarable>();
|
|
295
|
+
entities.set("build", job as unknown as Declarable);
|
|
296
|
+
|
|
297
|
+
const output = gitlabSerializer.serialize(entities);
|
|
298
|
+
|
|
299
|
+
// Image must expand to its properties, not {}
|
|
300
|
+
expect(output).toContain("image:");
|
|
301
|
+
expect(output).toContain("name: node:20-alpine");
|
|
302
|
+
expect(output).not.toContain("image: {}");
|
|
303
|
+
|
|
304
|
+
// Cache must expand
|
|
305
|
+
expect(output).toContain("cache:");
|
|
306
|
+
expect(output).toContain("paths:");
|
|
307
|
+
expect(output).toContain("- node_modules/");
|
|
308
|
+
expect(output).not.toContain("cache: {}");
|
|
309
|
+
|
|
310
|
+
// Artifacts must expand
|
|
311
|
+
expect(output).toContain("artifacts:");
|
|
312
|
+
expect(output).toContain("- dist/");
|
|
313
|
+
expect(output).not.toContain("artifacts: {}");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("serializes runtime-style rules array", () => {
|
|
317
|
+
const rule1 = new RuntimeRule({ if: "$CI_MERGE_REQUEST_IID" });
|
|
318
|
+
const rule2 = new RuntimeRule({ if: "$CI_COMMIT_BRANCH" });
|
|
319
|
+
const job = new RuntimeJob({
|
|
320
|
+
stage: "test",
|
|
321
|
+
script: ["npm test"],
|
|
322
|
+
rules: [rule1, rule2],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const entities = new Map<string, Declarable>();
|
|
326
|
+
entities.set("test", job as unknown as Declarable);
|
|
327
|
+
|
|
328
|
+
const output = gitlabSerializer.serialize(entities);
|
|
329
|
+
expect(output).toContain("rules:");
|
|
330
|
+
expect(output).toContain("if: '$CI_MERGE_REQUEST_IID'");
|
|
331
|
+
expect(output).toContain("if: '$CI_COMMIT_BRANCH'");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("serializes runtime-style environment", () => {
|
|
335
|
+
const env = new RuntimeEnvironment({
|
|
336
|
+
name: "production",
|
|
337
|
+
url: "https://example.com",
|
|
338
|
+
});
|
|
339
|
+
const job = new RuntimeJob({
|
|
340
|
+
stage: "deploy",
|
|
341
|
+
script: ["deploy.sh"],
|
|
342
|
+
environment: env,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const entities = new Map<string, Declarable>();
|
|
346
|
+
entities.set("deploy", job as unknown as Declarable);
|
|
347
|
+
|
|
348
|
+
const output = gitlabSerializer.serialize(entities);
|
|
349
|
+
expect(output).toContain("environment:");
|
|
350
|
+
expect(output).toContain("name: production");
|
|
351
|
+
expect(output).toContain("url: https://example.com");
|
|
352
|
+
expect(output).not.toContain("environment: {}");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("array-of-objects YAML formatting", () => {
|
|
357
|
+
test("serializes cache array with nested key object correctly", () => {
|
|
358
|
+
const entities = new Map<string, Declarable>();
|
|
359
|
+
entities.set("job", new MockJob({
|
|
360
|
+
stage: "build",
|
|
361
|
+
script: ["npm ci"],
|
|
362
|
+
cache: [
|
|
363
|
+
{
|
|
364
|
+
key: { files: ["package-lock.json"] },
|
|
365
|
+
paths: [".npm/"],
|
|
366
|
+
policy: "pull-push",
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
}));
|
|
370
|
+
|
|
371
|
+
const output = gitlabSerializer.serialize(entities);
|
|
372
|
+
// The key object should be on a new line, properly indented
|
|
373
|
+
expect(output).toContain("cache:");
|
|
374
|
+
expect(output).toContain("- key:");
|
|
375
|
+
expect(output).toContain("files:");
|
|
376
|
+
expect(output).toContain("- package-lock.json");
|
|
377
|
+
expect(output).toContain("paths:");
|
|
378
|
+
expect(output).toContain("- .npm/");
|
|
379
|
+
expect(output).toContain("policy: pull-push");
|
|
380
|
+
// The key value should NOT be inlined as "key: files:"
|
|
381
|
+
expect(output).not.toMatch(/key: files:/);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("serializes services array with nested objects", () => {
|
|
385
|
+
const entities = new Map<string, Declarable>();
|
|
386
|
+
entities.set("job", new MockJob({
|
|
387
|
+
stage: "build",
|
|
388
|
+
script: ["docker build ."],
|
|
389
|
+
services: [
|
|
390
|
+
{ name: "docker:27-dind", alias: "docker" },
|
|
391
|
+
],
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
const output = gitlabSerializer.serialize(entities);
|
|
395
|
+
expect(output).toContain("services:");
|
|
396
|
+
expect(output).toContain("- name: docker:27-dind");
|
|
397
|
+
expect(output).toContain("alias: docker");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
261
401
|
describe("nested objects and arrays", () => {
|
|
262
402
|
test("serializes nested objects", () => {
|
|
263
403
|
const entities = new Map<string, Declarable>();
|
package/src/serializer.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
|
20
20
|
function gitlabVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
21
21
|
return {
|
|
22
22
|
attrRef: (name, _attr) => name,
|
|
23
|
-
resourceRef: (name) => name,
|
|
23
|
+
resourceRef: (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(),
|
|
24
24
|
propertyDeclarable: (entity, walk) => {
|
|
25
25
|
if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
26
26
|
return undefined;
|
|
@@ -37,11 +37,49 @@ function gitlabVisitor(entityNames: Map<Declarable, string>): SerializerVisitor
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Pre-process values to convert intrinsics to their YAML representation
|
|
42
|
+
* before the walker (which would call toJSON instead of toYAML).
|
|
43
|
+
*
|
|
44
|
+
* IMPORTANT: Must not touch Declarable objects — their identity markers
|
|
45
|
+
* (DECLARABLE_MARKER, entityType, kind, props) are non-enumerable and
|
|
46
|
+
* would be stripped by Object.entries(), producing empty `{}` output.
|
|
47
|
+
*/
|
|
48
|
+
function preprocessIntrinsics(value: unknown): unknown {
|
|
49
|
+
if (value === null || value === undefined) return value;
|
|
50
|
+
|
|
51
|
+
if (typeof value === "object" && INTRINSIC_MARKER in value) {
|
|
52
|
+
if ("toYAML" in value && typeof value.toYAML === "function") {
|
|
53
|
+
return (value as { toYAML(): unknown }).toYAML();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Leave Declarables untouched — the walker handles them
|
|
58
|
+
if (typeof value === "object" && value !== null && "entityType" in value) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map(preprocessIntrinsics);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof value === "object") {
|
|
67
|
+
const result: Record<string, unknown> = {};
|
|
68
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
69
|
+
result[k] = preprocessIntrinsics(v);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
40
77
|
/**
|
|
41
78
|
* Convert a value to YAML-compatible form using the walker.
|
|
42
79
|
*/
|
|
43
80
|
function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
|
|
44
|
-
|
|
81
|
+
const preprocessed = preprocessIntrinsics(value);
|
|
82
|
+
return walkValue(preprocessed, entityNames, gitlabVisitor(entityNames));
|
|
45
83
|
}
|
|
46
84
|
|
|
47
85
|
/**
|
|
@@ -98,10 +136,21 @@ function emitYAML(value: unknown, indent: number): string {
|
|
|
98
136
|
const entries = Object.entries(item as Record<string, unknown>);
|
|
99
137
|
if (entries.length > 0) {
|
|
100
138
|
const [firstKey, firstVal] = entries[0];
|
|
101
|
-
|
|
139
|
+
const firstEmitted = emitYAML(firstVal, indent + 2);
|
|
140
|
+
if (firstEmitted.startsWith("\n")) {
|
|
141
|
+
// Multi-line value: put on next line, indented under the key
|
|
142
|
+
lines.push(`${prefix}- ${firstKey}:${firstEmitted}`);
|
|
143
|
+
} else {
|
|
144
|
+
lines.push(`${prefix}- ${firstKey}: ${firstEmitted}`);
|
|
145
|
+
}
|
|
102
146
|
for (let i = 1; i < entries.length; i++) {
|
|
103
147
|
const [key, val] = entries[i];
|
|
104
|
-
|
|
148
|
+
const emitted = emitYAML(val, indent + 2);
|
|
149
|
+
if (emitted.startsWith("\n")) {
|
|
150
|
+
lines.push(`${prefix} ${key}:${emitted}`);
|
|
151
|
+
} else {
|
|
152
|
+
lines.push(`${prefix} ${key}: ${emitted}`);
|
|
153
|
+
}
|
|
105
154
|
}
|
|
106
155
|
}
|
|
107
156
|
} else {
|
|
@@ -112,7 +161,16 @@ function emitYAML(value: unknown, indent: number): string {
|
|
|
112
161
|
}
|
|
113
162
|
|
|
114
163
|
if (typeof value === "object") {
|
|
115
|
-
const
|
|
164
|
+
const obj = value as Record<string, unknown>;
|
|
165
|
+
|
|
166
|
+
// Handle tagged values (intrinsics like !reference)
|
|
167
|
+
if ("tag" in obj && "value" in obj && typeof obj.tag === "string") {
|
|
168
|
+
if (obj.tag === "!reference" && Array.isArray(obj.value)) {
|
|
169
|
+
return `!reference [${(obj.value as string[]).join(", ")}]`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const entries = Object.entries(obj);
|
|
116
174
|
if (entries.length === 0) return "{}";
|
|
117
175
|
const lines: string[] = [];
|
|
118
176
|
for (const [key, val] of entries) {
|
package/src/validate.ts
CHANGED
package/src/variables.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
export const CI = {
|
|
9
9
|
CommitBranch: "$CI_COMMIT_BRANCH",
|
|
10
10
|
CommitRef: "$CI_COMMIT_REF_NAME",
|
|
11
|
+
CommitRefSlug: "$CI_COMMIT_REF_SLUG",
|
|
11
12
|
CommitSha: "$CI_COMMIT_SHA",
|
|
12
13
|
CommitTag: "$CI_COMMIT_TAG",
|
|
13
14
|
DefaultBranch: "$CI_DEFAULT_BRANCH",
|
|
14
15
|
Environment: "$CI_ENVIRONMENT_NAME",
|
|
16
|
+
EnvironmentSlug: "$CI_ENVIRONMENT_SLUG",
|
|
15
17
|
JobId: "$CI_JOB_ID",
|
|
16
18
|
JobName: "$CI_JOB_NAME",
|
|
17
19
|
JobStage: "$CI_JOB_STAGE",
|
|
@@ -24,4 +26,6 @@ export const CI = {
|
|
|
24
26
|
ProjectPath: "$CI_PROJECT_PATH",
|
|
25
27
|
Registry: "$CI_REGISTRY",
|
|
26
28
|
RegistryImage: "$CI_REGISTRY_IMAGE",
|
|
29
|
+
RegistryUser: "$CI_REGISTRY_USER",
|
|
30
|
+
RegistryPassword: "$CI_REGISTRY_PASSWORD",
|
|
27
31
|
} as const;
|