@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.
Files changed (45) hide show
  1. package/dist/integrity.json +10 -6
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +186 -8
  4. package/dist/rules/wgl012.ts +86 -0
  5. package/dist/rules/wgl013.ts +62 -0
  6. package/dist/rules/wgl014.ts +51 -0
  7. package/dist/rules/wgl015.ts +85 -0
  8. package/dist/rules/yaml-helpers.ts +65 -3
  9. package/dist/skills/chant-gitlab.md +467 -24
  10. package/dist/types/index.d.ts +55 -16
  11. package/package.json +2 -2
  12. package/src/codegen/__snapshots__/snapshot.test.ts.snap +58 -0
  13. package/src/codegen/docs.ts +32 -9
  14. package/src/codegen/generate-lexicon.ts +6 -1
  15. package/src/codegen/generate.ts +45 -50
  16. package/src/codegen/naming.ts +3 -0
  17. package/src/codegen/parse.test.ts +154 -4
  18. package/src/codegen/parse.ts +161 -49
  19. package/src/codegen/snapshot.test.ts +7 -5
  20. package/src/composites/composites.test.ts +452 -0
  21. package/src/composites/docker-build.ts +81 -0
  22. package/src/composites/index.ts +8 -0
  23. package/src/composites/node-pipeline.ts +104 -0
  24. package/src/composites/python-pipeline.ts +75 -0
  25. package/src/composites/review-app.ts +63 -0
  26. package/src/generated/index.d.ts +55 -16
  27. package/src/generated/index.ts +3 -0
  28. package/src/generated/lexicon-gitlab.json +186 -8
  29. package/src/import/generator.ts +3 -2
  30. package/src/index.ts +4 -0
  31. package/src/lint/post-synth/wgl012.test.ts +131 -0
  32. package/src/lint/post-synth/wgl012.ts +86 -0
  33. package/src/lint/post-synth/wgl013.test.ts +164 -0
  34. package/src/lint/post-synth/wgl013.ts +62 -0
  35. package/src/lint/post-synth/wgl014.test.ts +97 -0
  36. package/src/lint/post-synth/wgl014.ts +51 -0
  37. package/src/lint/post-synth/wgl015.test.ts +139 -0
  38. package/src/lint/post-synth/wgl015.ts +85 -0
  39. package/src/lint/post-synth/yaml-helpers.ts +65 -3
  40. package/src/plugin.test.ts +39 -13
  41. package/src/plugin.ts +636 -40
  42. package/src/serializer.test.ts +140 -0
  43. package/src/serializer.ts +63 -5
  44. package/src/validate.ts +1 -0
  45. package/src/variables.ts +4 -0
@@ -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
- return walkValue(value, entityNames, gitlabVisitor(entityNames));
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
- lines.push(`${prefix}- ${firstKey}: ${emitYAML(firstVal, indent + 2).trimStart()}`);
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
- lines.push(`${prefix} ${key}: ${emitYAML(val, indent + 2).trimStart()}`);
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 entries = Object.entries(value as Record<string, unknown>);
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
@@ -16,6 +16,7 @@ const REQUIRED_NAMES = [
16
16
  "Artifacts", "Cache", "Image", "Service", "Rule", "Retry",
17
17
  "AllowFailure", "Parallel", "Include", "Release",
18
18
  "Environment", "Trigger", "AutoCancel",
19
+ "WorkflowRule", "Need", "Inherit",
19
20
  ];
20
21
 
21
22
  /**
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;