@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": ["src/", "dist/"],
@@ -21,7 +21,7 @@
21
21
  "prepack": "bun run bundle && bun run validate"
22
22
  },
23
23
  "dependencies": {
24
- "@intentius/chant": "0.0.5"
24
+ "@intentius/chant": "0.0.9"
25
25
  },
26
26
  "devDependencies": {
27
27
  "typescript": "^5.9.3"
@@ -2,6 +2,12 @@
2
2
 
3
3
  exports[`generated lexicon-gitlab.json entries match snapshot 1`] = `
4
4
  {
5
+ "constraints": {
6
+ "coverage": {
7
+ "format": "regex",
8
+ "pattern": "^/.+/$",
9
+ },
10
+ },
5
11
  "kind": "resource",
6
12
  "lexicon": "gitlab",
7
13
  "resourceType": "GitLab::CI::Job",
@@ -10,6 +16,30 @@ exports[`generated lexicon-gitlab.json entries match snapshot 1`] = `
10
16
 
11
17
  exports[`generated lexicon-gitlab.json entries match snapshot 2`] = `
12
18
  {
19
+ "constraints": {
20
+ "access": {
21
+ "default": "all",
22
+ "enum": [
23
+ "none",
24
+ "developer",
25
+ "all",
26
+ ],
27
+ },
28
+ "expire_in": {
29
+ "default": "30 days",
30
+ },
31
+ "untracked": {
32
+ "default": false,
33
+ },
34
+ "when": {
35
+ "default": "on_success",
36
+ "enum": [
37
+ "on_success",
38
+ "on_failure",
39
+ "always",
40
+ ],
41
+ },
42
+ },
13
43
  "kind": "property",
14
44
  "lexicon": "gitlab",
15
45
  "resourceType": "GitLab::CI::Artifacts",
@@ -18,6 +48,26 @@ exports[`generated lexicon-gitlab.json entries match snapshot 2`] = `
18
48
 
19
49
  exports[`generated lexicon-gitlab.json entries match snapshot 3`] = `
20
50
  {
51
+ "constraints": {
52
+ "policy": {
53
+ "default": "pull-push",
54
+ "pattern": "pull-push|pull|push|\\$\\w{1,255}",
55
+ },
56
+ "unprotect": {
57
+ "default": false,
58
+ },
59
+ "untracked": {
60
+ "default": false,
61
+ },
62
+ "when": {
63
+ "default": "on_success",
64
+ "enum": [
65
+ "on_success",
66
+ "on_failure",
67
+ "always",
68
+ ],
69
+ },
70
+ },
21
71
  "kind": "property",
22
72
  "lexicon": "gitlab",
23
73
  "resourceType": "GitLab::CI::Cache",
@@ -26,6 +76,14 @@ exports[`generated lexicon-gitlab.json entries match snapshot 3`] = `
26
76
 
27
77
  exports[`generated lexicon-gitlab.json entries match snapshot 4`] = `
28
78
  {
79
+ "constraints": {
80
+ "name": {
81
+ "minLength": 1,
82
+ },
83
+ "pull_policy": {
84
+ "default": "always",
85
+ },
86
+ },
29
87
  "kind": "property",
30
88
  "lexicon": "gitlab",
31
89
  "resourceType": "GitLab::CI::Image",
@@ -161,7 +161,7 @@ The lexicon provides 3 resource types and 13 property types:
161
161
 
162
162
  Extract reusable objects into a shared config file and import them across your pipeline files:
163
163
 
164
- {{file:docs-snippets/src/pipeline-barrel.ts}}
164
+ {{file:docs-snippets/src/pipeline-shared-config.ts}}
165
165
 
166
166
  ## Jobs
167
167
 
@@ -338,9 +338,9 @@ lint:
338
338
  - npm run lint
339
339
  \`\`\`
340
340
 
341
- ### When to use \`reference()\` vs barrel imports
341
+ ### When to use \`reference()\` vs direct imports
342
342
 
343
- {{file:docs-snippets/src/reference-vs-barrel.ts}}
343
+ {{file:docs-snippets/src/reference-vs-import.ts}}
344
344
  `,
345
345
  },
346
346
  {
@@ -403,6 +403,30 @@ Flags jobs where all \`rules:\` entries have \`when: "never"\`, making the job u
403
403
 
404
404
  {{file:docs-snippets/src/lint-wgl011.ts}}
405
405
 
406
+ ### WGL012 — Deprecated property usage
407
+
408
+ **Severity:** warning
409
+
410
+ Flags properties marked as deprecated in the GitLab CI schema. Deprecation signals are mined from property descriptions (keywords like "deprecated", "legacy", "no longer available"). Using deprecated properties may cause unexpected behavior in future GitLab versions.
411
+
412
+ ### WGL013 — Invalid \`needs:\` target
413
+
414
+ **Severity:** error
415
+
416
+ Flags jobs whose \`needs:\` entries reference a job not defined in the pipeline, or reference themselves. Both cause GitLab pipeline validation failures. When \`include:\` is present, the check is skipped since needed jobs may come from included files.
417
+
418
+ ### WGL014 — Invalid \`extends:\` target
419
+
420
+ **Severity:** error
421
+
422
+ Flags jobs whose \`extends:\` references a template or hidden job not defined in the pipeline. GitLab rejects pipelines with unresolved extends references. When \`include:\` is present, the check is skipped since templates may come from included files.
423
+
424
+ ### WGL015 — Circular \`needs:\` chain
425
+
426
+ **Severity:** error
427
+
428
+ Detects cycles in the \`needs:\` dependency graph. If job A needs B and B needs A (directly or transitively), GitLab rejects the pipeline. Reports the full cycle chain in the diagnostic message.
429
+
406
430
  ## Running lint
407
431
 
408
432
  \`\`\`bash
@@ -531,11 +555,10 @@ deploy:
531
555
 
532
556
  **Patterns demonstrated:**
533
557
 
534
- 1. **Barrel file** — single import point for lexicon types and shared config
535
- 2. **Shared config** — reusable images, caches, artifacts, and rules extracted into \`config.ts\`
536
- 3. **Conditional execution** — merge request and branch rules control when jobs run
537
- 4. **Manual deployment** — deploy requires manual trigger on the default branch
538
- 5. **JUnit reports** — test artifacts include JUnit XML for GitLab MR display
558
+ 1. **Shared config** — reusable images, caches, artifacts, and rules extracted into \`config.ts\`
559
+ 2. **Conditional execution** — merge request and branch rules control when jobs run
560
+ 3. **Manual deployment** — deploy requires manual trigger on the default branch
561
+ 4. **JUnit reports** — test artifacts include JUnit XML for GitLab MR display
539
562
  `,
540
563
  },
541
564
  {
@@ -571,7 +594,7 @@ The \`chant-gitlab\` skill covers the full deployment lifecycle:
571
594
  - **Status** — GitLab UI or pipelines API
572
595
  - **Retry** — retry failed jobs via UI or API
573
596
  - **Cancel** — cancel running pipelines via API
574
- - **Troubleshooting** — job logs, lint rule codes (WGL001–WGL004), post-synth checks (WGL010, WGL011)
597
+ - **Troubleshooting** — job logs, lint rule codes (WGL001–WGL004), post-synth checks (WGL010–WGL015)
575
598
 
576
599
  The skill is invocable as a slash command: \`/chant-gitlab\`
577
600
 
@@ -3,6 +3,7 @@
3
3
  * for all GitLab CI entities.
4
4
  */
5
5
 
6
+ import type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
6
7
  import type { GitLabParseResult } from "./parse";
7
8
  import { gitlabShortName } from "./parse";
8
9
  import type { NamingStrategy } from "./naming";
@@ -16,6 +17,8 @@ export interface LexiconEntry {
16
17
  resourceType: string;
17
18
  kind: "resource" | "property";
18
19
  lexicon: "gitlab";
20
+ deprecatedProperties?: string[];
21
+ constraints?: Record<string, PropertyConstraints>;
19
22
  }
20
23
 
21
24
  /**
@@ -34,12 +37,14 @@ export function generateLexiconJSON(
34
37
 
35
38
  const entries = buildRegistry<LexiconEntry>(registryResources, naming, {
36
39
  shortName: gitlabShortName,
37
- buildEntry: (resource, _tsName, _attrs, _propConstraints) => {
40
+ buildEntry: (resource, _tsName, _attrs, propConstraints) => {
38
41
  const r = results.find((res) => res.resource.typeName === resource.typeName);
39
42
  return {
40
43
  resourceType: resource.typeName,
41
44
  kind: (r?.isProperty ? "property" : "resource") as "resource" | "property",
42
45
  lexicon: "gitlab" as const,
46
+ ...(r?.resource.deprecatedProperties?.length && { deprecatedProperties: r.resource.deprecatedProperties }),
47
+ ...(propConstraints && Object.keys(propConstraints).length > 0 && { constraints: propConstraints }),
43
48
  };
44
49
  },
45
50
  buildPropertyEntry: (resourceType, propertyType) => ({
@@ -30,61 +30,56 @@ export interface GitLabGenerateOptions extends GenerateOptions {
30
30
  schemaVersion?: string;
31
31
  }
32
32
 
33
- // Captured from generate() call so the pipeline config can access it
34
- let activeSchemaVersion: string | undefined;
35
-
36
- const gitlabPipelineConfig: GeneratePipelineConfig<GitLabParseResult> = {
37
- fetchSchemas: async (opts) => {
38
- return fetchSchemas(opts.force, activeSchemaVersion);
39
- },
40
-
41
- parseSchema: (_typeName, data) => {
42
- // The CI schema is a single document — parseCISchema returns multiple results.
43
- // The pipeline calls this once per schema entry. We return the first result
44
- // and use augmentResults to inject the rest.
45
- const results = parseCISchema(data);
46
- if (results.length === 0) return null;
47
- // Return the first result; stash the rest for augmentResults
48
- pendingResults = results.slice(1);
49
- return results[0];
50
- },
51
-
52
- createNaming: (results) => new NamingStrategy(results),
53
-
54
- augmentResults: (results, _opts, log) => {
55
- // Add the remaining results from the single-schema parse
56
- if (pendingResults.length > 0) {
57
- results.push(...pendingResults);
58
- log(`Added ${pendingResults.length} additional CI entities from single schema`);
59
- pendingResults = [];
60
- }
61
- log(`Total: ${results.length} CI entity schemas`);
62
- return { results };
63
- },
33
+ /**
34
+ * Run the full GitLab generation pipeline.
35
+ */
36
+ export async function generate(opts: GitLabGenerateOptions = {}): Promise<GenerateResult> {
37
+ // Pipeline state captured in closure — no module-level mutation
38
+ let pendingResults: GitLabParseResult[] = [];
64
39
 
65
- generateRegistry: (results, naming) => {
66
- return generateLexiconJSON(results, naming as NamingStrategy);
67
- },
40
+ const config: GeneratePipelineConfig<GitLabParseResult> = {
41
+ fetchSchemas: async (fetchOpts) => {
42
+ return fetchSchemas(fetchOpts.force, opts.schemaVersion);
43
+ },
68
44
 
69
- generateTypes: (results, naming) => {
70
- return generateTypeScriptDeclarations(results, naming as NamingStrategy);
71
- },
45
+ parseSchema: (_typeName, data) => {
46
+ // The CI schema is a single document — parseCISchema returns multiple results.
47
+ // The pipeline calls this once per schema entry. We return the first result
48
+ // and use augmentResults to inject the rest.
49
+ const results = parseCISchema(data);
50
+ if (results.length === 0) return null;
51
+ // Return the first result; stash the rest for augmentResults
52
+ pendingResults = results.slice(1);
53
+ return results[0];
54
+ },
72
55
 
73
- generateRuntimeIndex: (results, naming) => {
74
- return generateRuntimeIndex(results, naming as NamingStrategy);
75
- },
76
- };
56
+ createNaming: (results) => new NamingStrategy(results),
77
57
 
78
- // Shared state between parseSchema and augmentResults
79
- let pendingResults: GitLabParseResult[] = [];
58
+ augmentResults: (results, _opts, log) => {
59
+ // Add the remaining results from the single-schema parse
60
+ if (pendingResults.length > 0) {
61
+ results.push(...pendingResults);
62
+ log(`Added ${pendingResults.length} additional CI entities from single schema`);
63
+ pendingResults = [];
64
+ }
65
+ log(`Total: ${results.length} CI entity schemas`);
66
+ return { results };
67
+ },
80
68
 
81
- /**
82
- * Run the full GitLab generation pipeline.
83
- */
84
- export async function generate(opts: GitLabGenerateOptions = {}): Promise<GenerateResult> {
85
- pendingResults = [];
86
- activeSchemaVersion = opts.schemaVersion;
87
- return generatePipeline(gitlabPipelineConfig, opts);
69
+ generateRegistry: (results, naming) => {
70
+ return generateLexiconJSON(results, naming as NamingStrategy);
71
+ },
72
+
73
+ generateTypes: (results, naming) => {
74
+ return generateTypeScriptDeclarations(results, naming as NamingStrategy);
75
+ },
76
+
77
+ generateRuntimeIndex: (results, naming) => {
78
+ return generateRuntimeIndex(results, naming as NamingStrategy);
79
+ },
80
+ };
81
+
82
+ return generatePipeline(config, opts);
88
83
  }
89
84
 
90
85
  /**
@@ -29,6 +29,9 @@ const gitlabNamingConfig: NamingConfig = {
29
29
  "GitLab::CI::Environment": "Environment",
30
30
  "GitLab::CI::Trigger": "Trigger",
31
31
  "GitLab::CI::AutoCancel": "AutoCancel",
32
+ "GitLab::CI::WorkflowRule": "WorkflowRule",
33
+ "GitLab::CI::Need": "Need",
34
+ "GitLab::CI::Inherit": "Inherit",
32
35
  },
33
36
  priorityAliases: {},
34
37
  priorityPropertyAliases: {},
@@ -5,9 +5,9 @@ import { loadSchemaFixture } from "../testdata/load-fixtures";
5
5
  const fixture = loadSchemaFixture();
6
6
 
7
7
  describe("parseCISchema", () => {
8
- test("returns 16 entities", () => {
8
+ test("returns 19 entities", () => {
9
9
  const results = parseCISchema(fixture);
10
- expect(results).toHaveLength(16);
10
+ expect(results).toHaveLength(19);
11
11
  });
12
12
 
13
13
  test("returns 3 resource entities", () => {
@@ -20,10 +20,10 @@ describe("parseCISchema", () => {
20
20
  expect(names).toContain("GitLab::CI::Workflow");
21
21
  });
22
22
 
23
- test("returns 13 property entities", () => {
23
+ test("returns 16 property entities", () => {
24
24
  const results = parseCISchema(fixture);
25
25
  const properties = results.filter((r) => r.isProperty);
26
- expect(properties).toHaveLength(13);
26
+ expect(properties).toHaveLength(16);
27
27
  const names = properties.map((r) => r.resource.typeName);
28
28
  expect(names).toContain("GitLab::CI::Artifacts");
29
29
  expect(names).toContain("GitLab::CI::Cache");
@@ -38,6 +38,9 @@ describe("parseCISchema", () => {
38
38
  expect(names).toContain("GitLab::CI::Environment");
39
39
  expect(names).toContain("GitLab::CI::Trigger");
40
40
  expect(names).toContain("GitLab::CI::AutoCancel");
41
+ expect(names).toContain("GitLab::CI::WorkflowRule");
42
+ expect(names).toContain("GitLab::CI::Need");
43
+ expect(names).toContain("GitLab::CI::Inherit");
41
44
  });
42
45
 
43
46
  test("property entities have isProperty set to true", () => {
@@ -125,6 +128,108 @@ describe("Cache entity", () => {
125
128
  });
126
129
  });
127
130
 
131
+ describe("spec conformance — type mappings", () => {
132
+ const results = parseCISchema(fixture);
133
+ const findEntity = (name: string) => results.find((r) => r.resource.typeName === `GitLab::CI::${name}`);
134
+ const findProp = (entityName: string, propName: string) =>
135
+ findEntity(entityName)?.resource.properties.find((p) => p.name === propName);
136
+
137
+ test("Job.environment → Environment | string", () => {
138
+ expect(findProp("Job", "environment")?.tsType).toBe("Environment | string");
139
+ });
140
+
141
+ test("Job.trigger → Trigger | string", () => {
142
+ expect(findProp("Job", "trigger")?.tsType).toBe("Trigger | string");
143
+ });
144
+
145
+ test("Job.release → Release", () => {
146
+ expect(findProp("Job", "release")?.tsType).toBe("Release");
147
+ });
148
+
149
+ test("Job.needs → Need[]", () => {
150
+ expect(findProp("Job", "needs")?.tsType).toBe("Need[]");
151
+ });
152
+
153
+ test("Job.inherit → Inherit", () => {
154
+ expect(findProp("Job", "inherit")?.tsType).toBe("Inherit");
155
+ });
156
+
157
+ test("Workflow.rules → WorkflowRule[]", () => {
158
+ expect(findProp("Workflow", "rules")?.tsType).toBe("WorkflowRule[]");
159
+ });
160
+
161
+ test("AllowFailure.exit_codes → number | number[]", () => {
162
+ expect(findProp("AllowFailure", "exit_codes")?.tsType).toBe("number | number[]");
163
+ });
164
+
165
+ test("Image.pull_policy has no duplicate enum values", () => {
166
+ const pp = findProp("Image", "pull_policy");
167
+ expect(pp).toBeDefined();
168
+ // Should be clean union with parens on array form
169
+ expect(pp!.tsType).toContain("always");
170
+ expect(pp!.tsType).toContain("never");
171
+ expect(pp!.tsType).toContain("if-not-present");
172
+ // Array variant should use parens
173
+ expect(pp!.tsType).toContain(")[]");
174
+ });
175
+
176
+ test("Include has local, remote, template, component fields", () => {
177
+ const include = findEntity("Include");
178
+ expect(include).toBeDefined();
179
+ const propNames = include!.resource.properties.map((p) => p.name);
180
+ expect(propNames).toContain("project");
181
+ expect(propNames).toContain("file");
182
+ expect(propNames).toContain("local");
183
+ expect(propNames).toContain("remote");
184
+ expect(propNames).toContain("template");
185
+ expect(propNames).toContain("component");
186
+ });
187
+
188
+ test("Trigger has include field for child pipelines", () => {
189
+ const trigger = findEntity("Trigger");
190
+ expect(trigger).toBeDefined();
191
+ const propNames = trigger!.resource.properties.map((p) => p.name);
192
+ expect(propNames).toContain("project");
193
+ expect(propNames).toContain("include");
194
+ expect(propNames).toContain("strategy");
195
+ expect(propNames).toContain("forward");
196
+ });
197
+
198
+ test("Need has merged properties from all object variants", () => {
199
+ const need = findEntity("Need");
200
+ expect(need).toBeDefined();
201
+ const propNames = need!.resource.properties.map((p) => p.name);
202
+ expect(propNames).toContain("job");
203
+ expect(propNames).toContain("artifacts");
204
+ expect(propNames).toContain("project");
205
+ expect(propNames).toContain("ref");
206
+ expect(propNames).toContain("pipeline");
207
+ expect(propNames).toContain("optional");
208
+ // job should be required (in all object variants)
209
+ const jobProp = need!.resource.properties.find((p) => p.name === "job");
210
+ expect(jobProp?.required).toBe(true);
211
+ });
212
+
213
+ test("WorkflowRule has restricted when enum", () => {
214
+ const wr = findEntity("WorkflowRule");
215
+ expect(wr).toBeDefined();
216
+ const propNames = wr!.resource.properties.map((p) => p.name);
217
+ expect(propNames).toContain("if");
218
+ expect(propNames).toContain("when");
219
+ expect(propNames).toContain("auto_cancel");
220
+ const whenProp = wr!.resource.properties.find((p) => p.name === "when");
221
+ expect(whenProp?.tsType).toBe('"always" | "never"');
222
+ });
223
+
224
+ test("Inherit has default and variables properties", () => {
225
+ const inherit = findEntity("Inherit");
226
+ expect(inherit).toBeDefined();
227
+ const propNames = inherit!.resource.properties.map((p) => p.name);
228
+ expect(propNames).toContain("default");
229
+ expect(propNames).toContain("variables");
230
+ });
231
+ });
232
+
128
233
  describe("property types and enums", () => {
129
234
  test("entities have empty propertyTypes (nested types extracted as top-level)", () => {
130
235
  const results = parseCISchema(fixture);
@@ -141,6 +246,51 @@ describe("property types and enums", () => {
141
246
  });
142
247
  });
143
248
 
249
+ describe("deprecatedProperties", () => {
250
+ test("all entities have a deprecatedProperties array", () => {
251
+ const results = parseCISchema(fixture);
252
+ for (const r of results) {
253
+ expect(Array.isArray(r.resource.deprecatedProperties)).toBe(true);
254
+ }
255
+ });
256
+
257
+ test("mines deprecation from property description", () => {
258
+ const schema = JSON.stringify({
259
+ definitions: {
260
+ job_template: {
261
+ properties: {
262
+ script: { type: "string" },
263
+ oldProp: { type: "string", description: "Deprecated in 12.0: use newProp instead." },
264
+ newProp: { type: "string", description: "The replacement property" },
265
+ },
266
+ },
267
+ },
268
+ });
269
+ const results = parseCISchema(schema);
270
+ const job = results.find((r) => r.resource.typeName === "GitLab::CI::Job");
271
+ expect(job).toBeDefined();
272
+ expect(job!.resource.deprecatedProperties).toContain("oldProp");
273
+ expect(job!.resource.deprecatedProperties).not.toContain("newProp");
274
+ expect(job!.resource.deprecatedProperties).not.toContain("script");
275
+ });
276
+
277
+ test("empty deprecatedProperties when no deprecation signals", () => {
278
+ const schema = JSON.stringify({
279
+ definitions: {
280
+ job_template: {
281
+ properties: {
282
+ script: { type: "string", description: "Commands to run" },
283
+ stage: { type: "string", description: "Pipeline stage" },
284
+ },
285
+ },
286
+ },
287
+ });
288
+ const results = parseCISchema(schema);
289
+ const job = results.find((r) => r.resource.typeName === "GitLab::CI::Job");
290
+ expect(job!.resource.deprecatedProperties).toEqual([]);
291
+ });
292
+ });
293
+
144
294
  describe("edge cases", () => {
145
295
  test("empty schema returns empty results", () => {
146
296
  const emptySchema = JSON.stringify({ definitions: {} });