@intentius/chant-lexicon-gitlab 0.0.6 → 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 +502 -0
- 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 +88 -11
- 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/package.ts +2 -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/import/parser.test.ts +3 -3
- package/src/import/parser.ts +12 -26
- 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/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +2 -0
- package/src/plugin.test.ts +44 -19
- package/src/plugin.ts +671 -76
- package/src/serializer.test.ts +146 -6
- package/src/serializer.ts +64 -14
- package/src/validate.ts +1 -0
- package/src/variables.ts +4 -0
- package/dist/skills/gitlab-ci.md +0 -37
- package/src/codegen/rollback.ts +0 -26
package/src/codegen/parse.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface ParsedResource {
|
|
|
46
46
|
description?: string;
|
|
47
47
|
properties: ParsedProperty[];
|
|
48
48
|
attributes: Array<{ name: string; tsType: string }>;
|
|
49
|
+
deprecatedProperties: string[];
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface GitLabParseResult {
|
|
@@ -142,17 +143,9 @@ const PROPERTY_ENTITIES: Array<{
|
|
|
142
143
|
{ typeName: "GitLab::CI::Environment", source: "job_template:environment", description: "Deployment environment" },
|
|
143
144
|
{ typeName: "GitLab::CI::Trigger", source: "job_template:trigger", description: "Trigger downstream pipeline" },
|
|
144
145
|
{ typeName: "GitLab::CI::AutoCancel", source: "#/definitions/workflowAutoCancel", description: "Auto-cancel configuration" },
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* Enum types to extract.
|
|
149
|
-
*/
|
|
150
|
-
const ENUM_ENTITIES: Array<{
|
|
151
|
-
name: string;
|
|
152
|
-
source: string;
|
|
153
|
-
}> = [
|
|
154
|
-
{ name: "When", source: "#/definitions/when" },
|
|
155
|
-
{ name: "RetryError", source: "#/definitions/retry_errors" },
|
|
146
|
+
{ typeName: "GitLab::CI::WorkflowRule", source: "root:workflow:rules:item", description: "Workflow rule with restricted when values" },
|
|
147
|
+
{ typeName: "GitLab::CI::Need", source: "job_template:needs:item", description: "Job dependency specification" },
|
|
148
|
+
{ typeName: "GitLab::CI::Inherit", source: "job_template:inherit", description: "Control default/variable inheritance" },
|
|
156
149
|
];
|
|
157
150
|
|
|
158
151
|
// ── Parser ─────────────────────────────────────────────────────────
|
|
@@ -194,22 +187,35 @@ function extractResourceEntity(
|
|
|
194
187
|
if (!def) return null;
|
|
195
188
|
|
|
196
189
|
// Find the object variant if it's a oneOf/anyOf
|
|
197
|
-
const objectDef = findObjectVariant(def);
|
|
190
|
+
const objectDef = findObjectVariant(def, schema);
|
|
198
191
|
if (!objectDef?.properties) return null;
|
|
199
192
|
|
|
200
193
|
const requiredSet = new Set<string>(objectDef.required ?? []);
|
|
201
194
|
const properties = parseProperties(objectDef.properties, requiredSet, schema);
|
|
202
195
|
const shortName = gitlabShortName(entity.typeName);
|
|
203
196
|
|
|
197
|
+
// Apply property type overrides for known entity references
|
|
198
|
+
const overrides = RESOURCE_PROPERTY_OVERRIDES[entity.typeName];
|
|
199
|
+
if (overrides) {
|
|
200
|
+
for (const prop of properties) {
|
|
201
|
+
if (overrides[prop.name]) {
|
|
202
|
+
prop.tsType = overrides[prop.name];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
204
207
|
// Extract nested property types from definition properties
|
|
205
208
|
const { propertyTypes, enums } = extractNestedTypes(objectDef, shortName, schema);
|
|
206
209
|
|
|
210
|
+
const deprecatedProperties = mineDeprecatedProperties(properties);
|
|
211
|
+
|
|
207
212
|
return {
|
|
208
213
|
resource: {
|
|
209
214
|
typeName: entity.typeName,
|
|
210
215
|
description: entity.description ?? objectDef.description,
|
|
211
216
|
properties,
|
|
212
217
|
attributes: [], // CI entities have no read-only attributes
|
|
218
|
+
deprecatedProperties,
|
|
213
219
|
},
|
|
214
220
|
propertyTypes,
|
|
215
221
|
enums,
|
|
@@ -227,7 +233,7 @@ function extractPropertyEntity(
|
|
|
227
233
|
const def = resolveSource(schema, entity.source);
|
|
228
234
|
if (!def) return null;
|
|
229
235
|
|
|
230
|
-
const objectDef = findObjectVariant(def);
|
|
236
|
+
const objectDef = findObjectVariant(def, schema);
|
|
231
237
|
if (!objectDef?.properties) {
|
|
232
238
|
// Some entities like Parallel might be simple types
|
|
233
239
|
// Create a minimal entry with no properties
|
|
@@ -237,6 +243,7 @@ function extractPropertyEntity(
|
|
|
237
243
|
description: entity.description ?? def.description,
|
|
238
244
|
properties: [],
|
|
239
245
|
attributes: [],
|
|
246
|
+
deprecatedProperties: [],
|
|
240
247
|
},
|
|
241
248
|
propertyTypes: [],
|
|
242
249
|
enums: [],
|
|
@@ -248,6 +255,7 @@ function extractPropertyEntity(
|
|
|
248
255
|
const shortName = gitlabShortName(entity.typeName);
|
|
249
256
|
|
|
250
257
|
const { propertyTypes, enums } = extractNestedTypes(objectDef, shortName, schema);
|
|
258
|
+
const deprecatedProperties = mineDeprecatedProperties(properties);
|
|
251
259
|
|
|
252
260
|
return {
|
|
253
261
|
resource: {
|
|
@@ -255,6 +263,7 @@ function extractPropertyEntity(
|
|
|
255
263
|
description: entity.description ?? objectDef.description,
|
|
256
264
|
properties,
|
|
257
265
|
attributes: [],
|
|
266
|
+
deprecatedProperties,
|
|
258
267
|
},
|
|
259
268
|
propertyTypes,
|
|
260
269
|
enums,
|
|
@@ -273,32 +282,51 @@ function resolveSource(schema: CISchema, source: string): CISchemaDefinition | n
|
|
|
273
282
|
const defName = source.slice("#/definitions/".length).replace(":item", "");
|
|
274
283
|
const arrayDef = schema.definitions?.[defName];
|
|
275
284
|
if (!arrayDef?.items) return null;
|
|
276
|
-
return findObjectVariant(arrayDef.items);
|
|
285
|
+
return findObjectVariant(arrayDef.items, schema);
|
|
277
286
|
}
|
|
278
287
|
const defName = source.slice("#/definitions/".length);
|
|
279
288
|
return schema.definitions?.[defName] ?? null;
|
|
280
289
|
}
|
|
281
290
|
|
|
291
|
+
// Multi-segment path under root: root:prop:subprop:...:item
|
|
282
292
|
if (source.startsWith("root:")) {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
293
|
+
const segments = source.slice("root:".length).split(":");
|
|
294
|
+
let current: CISchemaDefinition | null = { properties: schema.properties } as CISchemaDefinition;
|
|
295
|
+
|
|
296
|
+
for (const seg of segments) {
|
|
297
|
+
if (!current) return null;
|
|
298
|
+
if (seg === "item") {
|
|
299
|
+
if (!current.items) return null;
|
|
300
|
+
return findObjectVariant(current.items, schema);
|
|
301
|
+
}
|
|
302
|
+
if (!current.properties) return null;
|
|
303
|
+
const prop = current.properties[seg];
|
|
304
|
+
if (!prop) return null;
|
|
305
|
+
current = prop.$ref ? resolveRef(prop.$ref, schema) : prop;
|
|
288
306
|
}
|
|
289
|
-
|
|
307
|
+
|
|
308
|
+
return current;
|
|
290
309
|
}
|
|
291
310
|
|
|
311
|
+
// Multi-segment path under job_template: job_template:prop:...:item
|
|
292
312
|
if (source.startsWith("job_template:")) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
if (!
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
313
|
+
const segments = source.slice("job_template:".length).split(":");
|
|
314
|
+
let current: CISchemaDefinition | null = schema.definitions?.job_template ?? null;
|
|
315
|
+
if (!current) return null;
|
|
316
|
+
|
|
317
|
+
for (const seg of segments) {
|
|
318
|
+
if (!current) return null;
|
|
319
|
+
if (seg === "item") {
|
|
320
|
+
if (!current.items) return null;
|
|
321
|
+
return findObjectVariant(current.items, schema);
|
|
322
|
+
}
|
|
323
|
+
if (!current.properties) return null;
|
|
324
|
+
const prop = current.properties[seg];
|
|
325
|
+
if (!prop) return null;
|
|
326
|
+
current = prop.$ref ? resolveRef(prop.$ref, schema) : prop;
|
|
300
327
|
}
|
|
301
|
-
|
|
328
|
+
|
|
329
|
+
return current;
|
|
302
330
|
}
|
|
303
331
|
|
|
304
332
|
return null;
|
|
@@ -317,35 +345,82 @@ function resolveRef(ref: string, schema: CISchema): CISchemaDefinition | null {
|
|
|
317
345
|
/**
|
|
318
346
|
* Find the object variant from a oneOf/anyOf union, or return the
|
|
319
347
|
* definition itself if it already has properties.
|
|
348
|
+
*
|
|
349
|
+
* When multiple object variants exist, merges their properties:
|
|
350
|
+
* - Picks the variant with the most properties as the base
|
|
351
|
+
* - Adds unique properties from other variants
|
|
352
|
+
* - Union-merges overlapping property types (e.g. exit_codes: integer | integer[])
|
|
353
|
+
* - Required = intersection of all variants' required sets
|
|
320
354
|
*/
|
|
321
|
-
function findObjectVariant(def: CISchemaDefinition): CISchemaDefinition | null {
|
|
355
|
+
function findObjectVariant(def: CISchemaDefinition, schema?: CISchema): CISchemaDefinition | null {
|
|
322
356
|
if (def.properties) return def;
|
|
323
357
|
|
|
324
358
|
const variants = def.oneOf ?? def.anyOf;
|
|
325
|
-
if (variants)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
359
|
+
if (!variants) return null;
|
|
360
|
+
|
|
361
|
+
// Collect all object variants, resolving $refs
|
|
362
|
+
const objectVariants: CISchemaDefinition[] = [];
|
|
363
|
+
for (const v of variants) {
|
|
364
|
+
let resolved: CISchemaDefinition = v;
|
|
365
|
+
if (v.$ref && schema) {
|
|
366
|
+
const r = resolveRef(v.$ref, schema);
|
|
367
|
+
if (r) resolved = r;
|
|
368
|
+
else continue;
|
|
369
|
+
} else if (v.$ref) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (resolved.properties) {
|
|
373
|
+
objectVariants.push(resolved);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (objectVariants.length === 0) return null;
|
|
378
|
+
if (objectVariants.length === 1) return objectVariants[0];
|
|
379
|
+
|
|
380
|
+
// Find the variant with the most properties as the base
|
|
381
|
+
let best = objectVariants[0];
|
|
382
|
+
let bestCount = Object.keys(best.properties!).length;
|
|
383
|
+
for (let i = 1; i < objectVariants.length; i++) {
|
|
384
|
+
const count = Object.keys(objectVariants[i].properties!).length;
|
|
385
|
+
if (count > bestCount) {
|
|
386
|
+
best = objectVariants[i];
|
|
387
|
+
bestCount = count;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Merge properties from other object variants into the base
|
|
392
|
+
const mergedProperties: Record<string, CISchemaProperty> = { ...best.properties };
|
|
393
|
+
const allRequiredSets = objectVariants.map((v) => new Set(v.required ?? []));
|
|
394
|
+
|
|
395
|
+
for (const variant of objectVariants) {
|
|
396
|
+
if (variant === best) continue;
|
|
397
|
+
for (const [propName, propDef] of Object.entries(variant.properties!)) {
|
|
398
|
+
if (propName in mergedProperties) {
|
|
399
|
+
// Property exists in base — if types differ, create a merged oneOf
|
|
400
|
+
const existing = mergedProperties[propName];
|
|
401
|
+
if (JSON.stringify(existing) !== JSON.stringify(propDef)) {
|
|
402
|
+
mergedProperties[propName] = {
|
|
403
|
+
oneOf: [existing, propDef],
|
|
404
|
+
description: existing.description ?? propDef.description,
|
|
405
|
+
};
|
|
341
406
|
}
|
|
407
|
+
} else {
|
|
408
|
+
// New property from another variant
|
|
409
|
+
mergedProperties[propName] = propDef;
|
|
342
410
|
}
|
|
343
411
|
}
|
|
344
|
-
return best;
|
|
345
412
|
}
|
|
346
413
|
|
|
347
|
-
//
|
|
348
|
-
|
|
414
|
+
// Required = intersection across all object variants
|
|
415
|
+
const requiredIntersection = [...allRequiredSets[0]].filter((r) =>
|
|
416
|
+
allRequiredSets.every((s) => s.has(r)),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
...best,
|
|
421
|
+
properties: mergedProperties,
|
|
422
|
+
required: requiredIntersection.length > 0 ? requiredIntersection : undefined,
|
|
423
|
+
};
|
|
349
424
|
}
|
|
350
425
|
|
|
351
426
|
/**
|
|
@@ -442,6 +517,10 @@ function resolvePropertyType(prop: CISchemaProperty, schema: CISchema): string {
|
|
|
442
517
|
case "array":
|
|
443
518
|
if (prop.items) {
|
|
444
519
|
const itemType = resolvePropertyType(prop.items, schema);
|
|
520
|
+
// Wrap union types in parens so [] applies to the whole union
|
|
521
|
+
if (itemType.includes(" | ")) {
|
|
522
|
+
return `(${itemType})[]`;
|
|
523
|
+
}
|
|
445
524
|
return `${itemType}[]`;
|
|
446
525
|
}
|
|
447
526
|
return "any[]";
|
|
@@ -452,6 +531,24 @@ function resolvePropertyType(prop: CISchemaProperty, schema: CISchema): string {
|
|
|
452
531
|
}
|
|
453
532
|
}
|
|
454
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Property type overrides for resource entities.
|
|
536
|
+
* Maps entity typeName → property name → desired TS type.
|
|
537
|
+
* Applied after parseProperties to replace generic types with entity references.
|
|
538
|
+
*/
|
|
539
|
+
const RESOURCE_PROPERTY_OVERRIDES: Record<string, Record<string, string>> = {
|
|
540
|
+
"GitLab::CI::Job": {
|
|
541
|
+
environment: "Environment | string",
|
|
542
|
+
trigger: "Trigger | string",
|
|
543
|
+
release: "Release",
|
|
544
|
+
needs: "Need[]",
|
|
545
|
+
inherit: "Inherit",
|
|
546
|
+
},
|
|
547
|
+
"GitLab::CI::Workflow": {
|
|
548
|
+
rules: "WorkflowRule[]",
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
455
552
|
/**
|
|
456
553
|
* Map well-known definition names to their TypeScript entity types.
|
|
457
554
|
*/
|
|
@@ -476,7 +573,7 @@ function definitionToTsType(defName: string): string | null {
|
|
|
476
573
|
rulesVariables: "Record<string, any>",
|
|
477
574
|
when: '"on_success" | "on_failure" | "always" | "never" | "manual" | "delayed"',
|
|
478
575
|
workflowAutoCancel: "AutoCancel",
|
|
479
|
-
id_tokens: "Record<string,
|
|
576
|
+
id_tokens: "Record<string, { aud: string | string[] }>",
|
|
480
577
|
secrets: "Record<string, any>",
|
|
481
578
|
timeout: "string",
|
|
482
579
|
start_in: "string",
|
|
@@ -512,6 +609,21 @@ function extractNestedTypes(
|
|
|
512
609
|
return { propertyTypes: [], enums: [] };
|
|
513
610
|
}
|
|
514
611
|
|
|
612
|
+
const DEPRECATION_RE = /\bDeprecated\b|\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended/i;
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Mine property descriptions for deprecation signals.
|
|
616
|
+
*/
|
|
617
|
+
function mineDeprecatedProperties(properties: ParsedProperty[]): string[] {
|
|
618
|
+
const deprecated: string[] = [];
|
|
619
|
+
for (const prop of properties) {
|
|
620
|
+
if (prop.description && DEPRECATION_RE.test(prop.description)) {
|
|
621
|
+
deprecated.push(prop.name);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return deprecated;
|
|
625
|
+
}
|
|
626
|
+
|
|
515
627
|
/**
|
|
516
628
|
* Extract short name: "GitLab::CI::Job" → "Job"
|
|
517
629
|
*/
|
|
@@ -10,7 +10,7 @@ describe("generated lexicon-gitlab.json", () => {
|
|
|
10
10
|
const registry = JSON.parse(content);
|
|
11
11
|
|
|
12
12
|
test("is valid JSON with expected entries", () => {
|
|
13
|
-
expect(Object.keys(registry)).toHaveLength(
|
|
13
|
+
expect(Object.keys(registry)).toHaveLength(19);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test("contains all resource entities", () => {
|
|
@@ -29,8 +29,9 @@ describe("generated lexicon-gitlab.json", () => {
|
|
|
29
29
|
test("contains all property entities", () => {
|
|
30
30
|
const propertyNames = [
|
|
31
31
|
"AllowFailure", "Artifacts", "AutoCancel", "Cache",
|
|
32
|
-
"Environment", "Image", "Include", "Parallel",
|
|
33
|
-
"Release", "Retry", "Rule", "Service", "Trigger",
|
|
32
|
+
"Environment", "Image", "Include", "Inherit", "Parallel",
|
|
33
|
+
"Need", "Release", "Retry", "Rule", "Service", "Trigger",
|
|
34
|
+
"WorkflowRule",
|
|
34
35
|
];
|
|
35
36
|
for (const name of propertyNames) {
|
|
36
37
|
expect(registry[name]).toBeDefined();
|
|
@@ -54,8 +55,9 @@ describe("generated index.d.ts", () => {
|
|
|
54
55
|
const expectedClasses = [
|
|
55
56
|
"Job", "Default", "Workflow",
|
|
56
57
|
"AllowFailure", "Artifacts", "AutoCancel", "Cache",
|
|
57
|
-
"Environment", "Image", "Include", "Parallel",
|
|
58
|
-
"Release", "Retry", "Rule", "Service", "Trigger",
|
|
58
|
+
"Environment", "Image", "Include", "Inherit", "Parallel",
|
|
59
|
+
"Need", "Release", "Retry", "Rule", "Service", "Trigger",
|
|
60
|
+
"WorkflowRule",
|
|
59
61
|
];
|
|
60
62
|
for (const cls of expectedClasses) {
|
|
61
63
|
expect(content).toContain(`export declare class ${cls}`);
|