@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
|
@@ -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",
|
package/src/codegen/docs.ts
CHANGED
|
@@ -58,8 +58,7 @@ The generated file includes:
|
|
|
58
58
|
| Chant (TypeScript) | YAML output | Rule |
|
|
59
59
|
|--------------------|-------------|------|
|
|
60
60
|
| \`export const buildApp = new Job({...})\` | \`build-app:\` | Export name → kebab-case job key |
|
|
61
|
-
| \`
|
|
62
|
-
| \`ifCondition: ...\` | \`if: ...\` | Reserved word properties use suffixed names |
|
|
61
|
+
| \`expire_in: "1 week"\` | \`expire_in: 1 week\` | Property names use spec-native snake_case |
|
|
63
62
|
| \`new Image({ name: "node:20" })\` | \`image: node:20\` | Single-property objects are collapsed |
|
|
64
63
|
|
|
65
64
|
## Validating locally
|
|
@@ -108,7 +107,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
|
|
|
108
107
|
description: "Jobs, stages, artifacts, caching, images, rules, environments, and triggers in the GitLab CI/CD lexicon",
|
|
109
108
|
content: `Every exported \`Job\` declaration becomes a job entry in the generated \`.gitlab-ci.yml\`. The serializer handles the translation automatically:
|
|
110
109
|
|
|
111
|
-
-
|
|
110
|
+
- Property names use spec-native snake_case (\`expire_in\`, \`allow_failure\`)
|
|
112
111
|
- Converts export names to kebab-case job keys (\`buildApp\` → \`build-app\`)
|
|
113
112
|
- Collects stages from all jobs into a \`stages:\` list
|
|
114
113
|
- Collapses single-property objects (\`new Image({ name: "node:20" })\` → \`image: node:20\`)
|
|
@@ -162,7 +161,7 @@ The lexicon provides 3 resource types and 13 property types:
|
|
|
162
161
|
|
|
163
162
|
Extract reusable objects into a shared config file and import them across your pipeline files:
|
|
164
163
|
|
|
165
|
-
{{file:docs-snippets/src/pipeline-
|
|
164
|
+
{{file:docs-snippets/src/pipeline-shared-config.ts}}
|
|
166
165
|
|
|
167
166
|
## Jobs
|
|
168
167
|
|
|
@@ -339,9 +338,9 @@ lint:
|
|
|
339
338
|
- npm run lint
|
|
340
339
|
\`\`\`
|
|
341
340
|
|
|
342
|
-
### When to use \`reference()\` vs
|
|
341
|
+
### When to use \`reference()\` vs direct imports
|
|
343
342
|
|
|
344
|
-
{{file:docs-snippets/src/reference-vs-
|
|
343
|
+
{{file:docs-snippets/src/reference-vs-import.ts}}
|
|
345
344
|
`,
|
|
346
345
|
},
|
|
347
346
|
{
|
|
@@ -404,6 +403,30 @@ Flags jobs where all \`rules:\` entries have \`when: "never"\`, making the job u
|
|
|
404
403
|
|
|
405
404
|
{{file:docs-snippets/src/lint-wgl011.ts}}
|
|
406
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
|
+
|
|
407
430
|
## Running lint
|
|
408
431
|
|
|
409
432
|
\`\`\`bash
|
|
@@ -532,13 +555,67 @@ deploy:
|
|
|
532
555
|
|
|
533
556
|
**Patterns demonstrated:**
|
|
534
557
|
|
|
535
|
-
1. **
|
|
536
|
-
2. **
|
|
537
|
-
3. **
|
|
538
|
-
4. **
|
|
539
|
-
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
|
|
540
562
|
`,
|
|
541
563
|
},
|
|
564
|
+
{
|
|
565
|
+
slug: "skills",
|
|
566
|
+
title: "AI Skills",
|
|
567
|
+
description: "AI agent skills bundled with the GitLab CI/CD lexicon",
|
|
568
|
+
content: `The GitLab lexicon ships an AI skill called **chant-gitlab** that teaches AI coding agents (like Claude Code) how to build, validate, and deploy GitLab CI pipelines from a chant project.
|
|
569
|
+
|
|
570
|
+
## What are skills?
|
|
571
|
+
|
|
572
|
+
Skills are structured markdown documents bundled with a lexicon. When an AI agent works in a chant project, it discovers and loads relevant skills automatically — giving it operational knowledge about the deployment workflow without requiring the user to explain each step.
|
|
573
|
+
|
|
574
|
+
## Installation
|
|
575
|
+
|
|
576
|
+
When you scaffold a new project with \`chant init --lexicon gitlab\`, the skill is installed to \`.claude/skills/chant-gitlab/SKILL.md\` for automatic discovery by Claude Code.
|
|
577
|
+
|
|
578
|
+
For existing projects, create the file manually:
|
|
579
|
+
|
|
580
|
+
\`\`\`
|
|
581
|
+
.claude/
|
|
582
|
+
skills/
|
|
583
|
+
chant-gitlab/
|
|
584
|
+
SKILL.md # skill content (see below)
|
|
585
|
+
\`\`\`
|
|
586
|
+
|
|
587
|
+
## Skill: chant-gitlab
|
|
588
|
+
|
|
589
|
+
The \`chant-gitlab\` skill covers the full deployment lifecycle:
|
|
590
|
+
|
|
591
|
+
- **Build** — \`chant build src/ --output .gitlab-ci.yml\`
|
|
592
|
+
- **Validate** — \`chant lint src/\` + GitLab CI Lint API
|
|
593
|
+
- **Deploy** — commit and push the generated YAML
|
|
594
|
+
- **Status** — GitLab UI or pipelines API
|
|
595
|
+
- **Retry** — retry failed jobs via UI or API
|
|
596
|
+
- **Cancel** — cancel running pipelines via API
|
|
597
|
+
- **Troubleshooting** — job logs, lint rule codes (WGL001–WGL004), post-synth checks (WGL010–WGL015)
|
|
598
|
+
|
|
599
|
+
The skill is invocable as a slash command: \`/chant-gitlab\`
|
|
600
|
+
|
|
601
|
+
## MCP integration
|
|
602
|
+
|
|
603
|
+
The lexicon also provides MCP (Model Context Protocol) tools and resources that AI agents can use programmatically:
|
|
604
|
+
|
|
605
|
+
| MCP tool | Description |
|
|
606
|
+
|----------|-------------|
|
|
607
|
+
| \`build\` | Build the chant project |
|
|
608
|
+
| \`lint\` | Run lint rules |
|
|
609
|
+
| \`explain\` | Summarize project resources |
|
|
610
|
+
| \`scaffold\` | Generate starter files |
|
|
611
|
+
| \`search\` | Search available resource types |
|
|
612
|
+
| \`gitlab:diff\` | Compare current build output against previous |
|
|
613
|
+
|
|
614
|
+
| MCP resource | Description |
|
|
615
|
+
|--------------|-------------|
|
|
616
|
+
| \`resource-catalog\` | JSON list of all supported GitLab CI entity types |
|
|
617
|
+
| \`examples/basic-pipeline\` | Example pipeline with build, test, and deploy jobs |`,
|
|
618
|
+
},
|
|
542
619
|
],
|
|
543
620
|
basePath: "/chant/lexicons/gitlab/",
|
|
544
621
|
};
|
|
@@ -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,
|
|
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) => ({
|
package/src/codegen/generate.ts
CHANGED
|
@@ -30,61 +30,56 @@ export interface GitLabGenerateOptions extends GenerateOptions {
|
|
|
30
30
|
schemaVersion?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
40
|
+
const config: GeneratePipelineConfig<GitLabParseResult> = {
|
|
41
|
+
fetchSchemas: async (fetchOpts) => {
|
|
42
|
+
return fetchSchemas(fetchOpts.force, opts.schemaVersion);
|
|
43
|
+
},
|
|
68
44
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
return generateRuntimeIndex(results, naming as NamingStrategy);
|
|
75
|
-
},
|
|
76
|
-
};
|
|
56
|
+
createNaming: (results) => new NamingStrategy(results),
|
|
77
57
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
/**
|
package/src/codegen/naming.ts
CHANGED
|
@@ -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: {},
|
package/src/codegen/package.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* with GitLab-specific manifest building and skill collection.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createRequire } from "module";
|
|
6
7
|
import { readFileSync } from "fs";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
7
9
|
import { join, dirname } from "path";
|
|
8
10
|
import { fileURLToPath } from "url";
|
|
9
11
|
import type { IntrinsicDef } from "@intentius/chant/lexicon";
|
|
@@ -5,9 +5,9 @@ import { loadSchemaFixture } from "../testdata/load-fixtures";
|
|
|
5
5
|
const fixture = loadSchemaFixture();
|
|
6
6
|
|
|
7
7
|
describe("parseCISchema", () => {
|
|
8
|
-
test("returns
|
|
8
|
+
test("returns 19 entities", () => {
|
|
9
9
|
const results = parseCISchema(fixture);
|
|
10
|
-
expect(results).toHaveLength(
|
|
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
|
|
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(
|
|
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: {} });
|