@intentius/chant-lexicon-gitlab 0.0.1

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 (56) hide show
  1. package/package.json +27 -0
  2. package/src/codegen/__snapshots__/snapshot.test.ts.snap +33 -0
  3. package/src/codegen/docs-cli.ts +3 -0
  4. package/src/codegen/docs.ts +962 -0
  5. package/src/codegen/fetch.ts +73 -0
  6. package/src/codegen/generate-cli.ts +41 -0
  7. package/src/codegen/generate-lexicon.ts +53 -0
  8. package/src/codegen/generate-typescript.ts +144 -0
  9. package/src/codegen/generate.ts +166 -0
  10. package/src/codegen/naming.ts +52 -0
  11. package/src/codegen/package.ts +64 -0
  12. package/src/codegen/parse.test.ts +195 -0
  13. package/src/codegen/parse.ts +531 -0
  14. package/src/codegen/patches.test.ts +99 -0
  15. package/src/codegen/patches.ts +100 -0
  16. package/src/codegen/rollback.ts +26 -0
  17. package/src/codegen/snapshot.test.ts +109 -0
  18. package/src/coverage.test.ts +39 -0
  19. package/src/coverage.ts +52 -0
  20. package/src/generated/index.d.ts +248 -0
  21. package/src/generated/index.ts +23 -0
  22. package/src/generated/lexicon-gitlab.json +77 -0
  23. package/src/generated/runtime.ts +4 -0
  24. package/src/import/generator.test.ts +151 -0
  25. package/src/import/generator.ts +173 -0
  26. package/src/import/parser.test.ts +160 -0
  27. package/src/import/parser.ts +282 -0
  28. package/src/import/roundtrip.test.ts +89 -0
  29. package/src/index.ts +25 -0
  30. package/src/intrinsics.test.ts +42 -0
  31. package/src/intrinsics.ts +40 -0
  32. package/src/lint/post-synth/post-synth.test.ts +155 -0
  33. package/src/lint/post-synth/wgl010.ts +41 -0
  34. package/src/lint/post-synth/wgl011.ts +54 -0
  35. package/src/lint/post-synth/yaml-helpers.ts +88 -0
  36. package/src/lint/rules/artifact-no-expiry.ts +62 -0
  37. package/src/lint/rules/deprecated-only-except.ts +53 -0
  38. package/src/lint/rules/index.ts +8 -0
  39. package/src/lint/rules/missing-script.ts +65 -0
  40. package/src/lint/rules/missing-stage.ts +62 -0
  41. package/src/lint/rules/rules.test.ts +146 -0
  42. package/src/lsp/completions.test.ts +85 -0
  43. package/src/lsp/completions.ts +18 -0
  44. package/src/lsp/hover.test.ts +60 -0
  45. package/src/lsp/hover.ts +36 -0
  46. package/src/plugin.test.ts +228 -0
  47. package/src/plugin.ts +380 -0
  48. package/src/serializer.test.ts +309 -0
  49. package/src/serializer.ts +226 -0
  50. package/src/testdata/ci-schema-fixture.json +2184 -0
  51. package/src/testdata/create-fixture.ts +46 -0
  52. package/src/testdata/load-fixtures.ts +23 -0
  53. package/src/validate-cli.ts +19 -0
  54. package/src/validate.test.ts +43 -0
  55. package/src/validate.ts +125 -0
  56. package/src/variables.ts +27 -0
@@ -0,0 +1,962 @@
1
+ /**
2
+ * Documentation generation for GitLab CI lexicon.
3
+ *
4
+ * Generates Starlight MDX pages for CI entities using the core docs pipeline.
5
+ */
6
+
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
10
+
11
+ /**
12
+ * Extract service name from GitLab CI type: "GitLab::CI::Job" → "CI"
13
+ */
14
+ function serviceFromType(resourceType: string): string {
15
+ const parts = resourceType.split("::");
16
+ return parts.length >= 2 ? parts[1] : "CI";
17
+ }
18
+
19
+ const overview = `The **GitLab CI/CD** lexicon provides typed constructors for GitLab CI pipeline
20
+ configuration. It covers jobs, workflow settings, artifacts, caching, images,
21
+ rules, environments, triggers, and more.
22
+
23
+ Install it with:
24
+
25
+ \`\`\`bash
26
+ npm install --save-dev @intentius/chant-lexicon-gitlab
27
+ \`\`\`
28
+
29
+ ## Quick Start
30
+
31
+ \`\`\`typescript
32
+ import { Job, Image, Cache, Artifacts, CI } from "@intentius/chant-lexicon-gitlab";
33
+
34
+ export const test = new Job({
35
+ stage: "test",
36
+ image: new Image({ name: "node:20" }),
37
+ cache: new Cache({ key: CI.CommitRef, paths: ["node_modules/"] }),
38
+ script: ["npm ci", "npm test"],
39
+ artifacts: new Artifacts({
40
+ paths: ["coverage/"],
41
+ expireIn: "1 week",
42
+ }),
43
+ });
44
+ \`\`\`
45
+
46
+ The lexicon provides **3 resources** (Job, Workflow, Default), **13 property types** (Image, Cache, Artifacts, Rule, Environment, Trigger, and more), the \`CI\` pseudo-parameter object for predefined variables, and the \`reference()\` intrinsic for YAML \`!reference\` tags.
47
+ `;
48
+
49
+ const outputFormat = `The GitLab lexicon serializes resources into **\`.gitlab-ci.yml\` YAML**. Keys are
50
+ converted to \`snake_case\` and jobs use kebab-case names. Stages are automatically
51
+ collected from all job definitions.
52
+
53
+ ## Building
54
+
55
+ Run \`chant build\` to produce a \`.gitlab-ci.yml\` from your declarations:
56
+
57
+ \`\`\`bash
58
+ chant build
59
+ # Writes dist/.gitlab-ci.yml
60
+ \`\`\`
61
+
62
+ The generated file includes:
63
+
64
+ - \`stages:\` list — automatically collected from all job \`stage\` properties
65
+ - \`default:\` section — if a \`Default\` resource is exported
66
+ - \`workflow:\` section — if a \`Workflow\` resource is exported
67
+ - Job definitions with \`snake_case\` keys and \`kebab-case\` job names
68
+
69
+ ## Key conversions
70
+
71
+ | Chant (TypeScript) | YAML output | Rule |
72
+ |--------------------|-------------|------|
73
+ | \`export const buildApp = new Job({...})\` | \`build-app:\` | Export name → kebab-case job key |
74
+ | \`expireIn: "1 week"\` | \`expire_in: 1 week\` | camelCase → snake_case |
75
+ | \`ifCondition: ...\` | \`if: ...\` | Reserved word properties use suffixed names |
76
+ | \`new Image({ name: "node:20" })\` | \`image: node:20\` | Single-property objects are collapsed |
77
+
78
+ ## Validating locally
79
+
80
+ The output is standard GitLab CI YAML. Validate with the GitLab CI Lint API or locally:
81
+
82
+ \`\`\`bash
83
+ # Using the GitLab API
84
+ curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
85
+ --data-urlencode "content=$(cat dist/.gitlab-ci.yml)" \\
86
+ "https://gitlab.com/api/v4/ci/lint"
87
+
88
+ # Using the glab CLI
89
+ glab ci lint dist/.gitlab-ci.yml
90
+ \`\`\`
91
+
92
+ ## Compatibility
93
+
94
+ The output is compatible with:
95
+ - GitLab CI/CD (any recent GitLab version)
96
+ - GitLab CI Lint API
97
+ - \`glab\` CLI tool
98
+ - Any tool that processes \`.gitlab-ci.yml\` files`;
99
+
100
+ /**
101
+ * Generate documentation for the GitLab CI lexicon.
102
+ */
103
+ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void> {
104
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
105
+
106
+ const config: DocsConfig = {
107
+ name: "gitlab",
108
+ displayName: "GitLab CI/CD",
109
+ description: "Typed constructors for GitLab CI/CD pipeline configuration",
110
+ distDir: join(pkgDir, "dist"),
111
+ outDir: join(pkgDir, "docs"),
112
+ overview,
113
+ outputFormat,
114
+ serviceFromType,
115
+ suppressPages: ["intrinsics", "rules"],
116
+ extraPages: [
117
+ {
118
+ slug: "pipeline-concepts",
119
+ title: "Pipeline Concepts",
120
+ description: "Jobs, stages, artifacts, caching, images, rules, environments, and triggers in the GitLab CI/CD lexicon",
121
+ content: `Every exported \`Job\` declaration becomes a job entry in the generated \`.gitlab-ci.yml\`. The serializer handles the translation automatically:
122
+
123
+ - Converts camelCase property names to snake_case (\`expireIn\` → \`expire_in\`)
124
+ - Converts export names to kebab-case job keys (\`buildApp\` → \`build-app\`)
125
+ - Collects stages from all jobs into a \`stages:\` list
126
+ - Collapses single-property objects (\`new Image({ name: "node:20" })\` → \`image: node:20\`)
127
+
128
+ \`\`\`typescript
129
+ // This chant declaration...
130
+ export const buildApp = new Job({
131
+ stage: "build",
132
+ image: new Image({ name: "node:20" }),
133
+ script: ["npm ci", "npm run build"],
134
+ });
135
+ \`\`\`
136
+
137
+ Produces this YAML:
138
+
139
+ \`\`\`yaml
140
+ stages:
141
+ - build
142
+
143
+ build-app:
144
+ stage: build
145
+ image: node:20
146
+ script:
147
+ - npm ci
148
+ - npm run build
149
+ \`\`\`
150
+
151
+ ## Resource types
152
+
153
+ The lexicon provides 3 resource types and 13 property types:
154
+
155
+ ### Resources
156
+
157
+ | Type | Description |
158
+ |------|-------------|
159
+ | \`Job\` | A CI/CD job — the fundamental unit of a pipeline |
160
+ | \`Workflow\` | Top-level \`workflow:\` configuration (pipeline-level rules, name, auto_cancel) |
161
+ | \`Default\` | Top-level \`default:\` block (shared defaults inherited by all jobs) |
162
+
163
+ ### Property types
164
+
165
+ | Type | Used in | Description |
166
+ |------|---------|-------------|
167
+ | \`Image\` | Job, Default | Docker image for the job runner |
168
+ | \`Cache\` | Job, Default | Files cached between pipeline runs |
169
+ | \`Artifacts\` | Job | Files passed between stages or stored after completion |
170
+ | \`Rule\` | Job, Workflow | Conditional execution rules (\`rules:\` entries) |
171
+ | \`Environment\` | Job | Deployment target environment |
172
+ | \`Trigger\` | Job | Downstream pipeline trigger |
173
+ | \`Include\` | Workflow | External YAML file inclusion |
174
+ | \`AllowFailure\` | Job | Failure tolerance configuration |
175
+ | \`Retry\` | Job | Automatic retry on failure |
176
+ | \`Parallel\` | Job | Job parallelization (matrix builds) |
177
+ | \`Release\` | Job | GitLab Release creation |
178
+ | \`AutoCancel\` | Workflow | Pipeline auto-cancellation settings |
179
+
180
+ ## The barrel file
181
+
182
+ Every chant project has a barrel file (conventionally \`_.ts\`) that re-exports the lexicon:
183
+
184
+ \`\`\`typescript
185
+ // _.ts — the barrel file
186
+ export * from "@intentius/chant-lexicon-gitlab";
187
+ export * from "./config";
188
+ \`\`\`
189
+
190
+ Other files import the barrel and use its exports:
191
+
192
+ \`\`\`typescript
193
+ // pipeline.ts
194
+ import * as _ from "./_";
195
+
196
+ export const build = new _.Job({
197
+ stage: "build",
198
+ image: _.nodeImage, // from config.ts via barrel
199
+ cache: _.npmCache, // from config.ts via barrel
200
+ script: ["npm ci", "npm run build"],
201
+ artifacts: _.buildArtifacts,
202
+ });
203
+ \`\`\`
204
+
205
+ ## Jobs
206
+
207
+ A \`Job\` is the fundamental unit. Every exported \`Job\` becomes a job entry in the YAML:
208
+
209
+ \`\`\`typescript
210
+ export const test = new Job({
211
+ stage: "test",
212
+ image: new Image({ name: "node:20-alpine" }),
213
+ script: ["npm ci", "npm test"],
214
+ artifacts: new Artifacts({
215
+ paths: ["coverage/"],
216
+ expireIn: "1 week",
217
+ reports: { junit: "coverage/junit.xml" },
218
+ }),
219
+ });
220
+ \`\`\`
221
+
222
+ Key properties:
223
+ - \`script\` — **required** (or \`trigger\`/\`run\`). Array of shell commands to execute.
224
+ - \`stage\` — which pipeline stage this job belongs to. Defaults to \`test\` if omitted.
225
+ - \`image\` — Docker image. Use \`new Image({ name: "..." })\` or pass a string to the YAML.
226
+ - \`needs\` — job dependencies for DAG-mode execution (run before stage ordering).
227
+
228
+ ## Stages
229
+
230
+ Stages define the execution order of a pipeline. The serializer automatically collects unique stage values from all jobs:
231
+
232
+ \`\`\`typescript
233
+ export const lint = new Job({ stage: "test", script: ["npm run lint"] });
234
+ export const test = new Job({ stage: "test", script: ["npm test"] });
235
+ export const build = new Job({ stage: "build", script: ["npm run build"] });
236
+ export const deploy = new Job({ stage: "deploy", script: ["npm run deploy"] });
237
+ \`\`\`
238
+
239
+ Produces:
240
+
241
+ \`\`\`yaml
242
+ stages:
243
+ - test
244
+ - build
245
+ - deploy
246
+ \`\`\`
247
+
248
+ Jobs in the same stage run in parallel. Stages run sequentially in declaration order.
249
+
250
+ ## Artifacts and caching
251
+
252
+ **Artifacts** are files produced by a job and passed to later stages or stored for download:
253
+
254
+ \`\`\`typescript
255
+ export const buildArtifacts = new Artifacts({
256
+ paths: ["dist/"],
257
+ expireIn: "1 hour", // always set expiry (WGL004 warns if missing)
258
+ });
259
+
260
+ export const testArtifacts = new Artifacts({
261
+ paths: ["coverage/"],
262
+ expireIn: "1 week",
263
+ reports: { junit: "coverage/junit.xml" }, // parsed by GitLab for MR display
264
+ });
265
+ \`\`\`
266
+
267
+ **Caches** persist files between pipeline runs to speed up builds:
268
+
269
+ \`\`\`typescript
270
+ export const npmCache = new Cache({
271
+ key: "$CI_COMMIT_REF_SLUG", // cache per branch
272
+ paths: ["node_modules/"],
273
+ policy: "pull-push", // "pull" for read-only, "push" for write-only
274
+ });
275
+ \`\`\`
276
+
277
+ The key difference: artifacts are for passing files between **stages in the same pipeline**; caches are for speeding up **repeated pipeline runs**.
278
+
279
+ ## Conditional execution with rules
280
+
281
+ \`Rule\` objects control when a job runs. They map to \`rules:\` entries in the YAML:
282
+
283
+ \`\`\`typescript
284
+ export const onMergeRequest = new Rule({
285
+ ifCondition: CI.MergeRequestIid, // → if: $CI_MERGE_REQUEST_IID
286
+ });
287
+
288
+ export const onDefaultBranch = new Rule({
289
+ ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\`,
290
+ when: "manual", // require manual trigger
291
+ });
292
+
293
+ export const deploy = new Job({
294
+ stage: "deploy",
295
+ script: ["npm run deploy"],
296
+ rules: [onDefaultBranch],
297
+ });
298
+ \`\`\`
299
+
300
+ Produces:
301
+
302
+ \`\`\`yaml
303
+ deploy:
304
+ stage: deploy
305
+ script:
306
+ - npm run deploy
307
+ rules:
308
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
309
+ when: manual
310
+ \`\`\`
311
+
312
+ The \`ifCondition\` property maps to \`if:\` in the YAML (since \`if\` is a reserved word in TypeScript).
313
+
314
+ ## Environments
315
+
316
+ \`Environment\` defines a deployment target:
317
+
318
+ \`\`\`typescript
319
+ export const productionEnv = new Environment({
320
+ name: "production",
321
+ url: "https://example.com",
322
+ });
323
+
324
+ export const deploy = new Job({
325
+ stage: "deploy",
326
+ script: ["npm run deploy"],
327
+ environment: productionEnv,
328
+ rules: [onDefaultBranch],
329
+ });
330
+ \`\`\`
331
+
332
+ GitLab tracks deployments to environments and provides rollback capabilities in the UI.
333
+
334
+ ## Images and services
335
+
336
+ \`Image\` specifies the Docker image for a job:
337
+
338
+ \`\`\`typescript
339
+ export const nodeImage = new Image({ name: "node:20-alpine" });
340
+
341
+ // With entrypoint override
342
+ export const customImage = new Image({
343
+ name: "registry.example.com/my-image:latest",
344
+ entrypoint: ["/bin/sh", "-c"],
345
+ });
346
+ \`\`\`
347
+
348
+ ## Workflow
349
+
350
+ \`Workflow\` controls pipeline-level settings — when pipelines run, auto-cancellation, and global includes:
351
+
352
+ \`\`\`typescript
353
+ export const workflow = new Workflow({
354
+ name: "CI Pipeline for $CI_COMMIT_REF_NAME",
355
+ rules: [
356
+ new Rule({ ifCondition: CI.MergeRequestIid }),
357
+ new Rule({ ifCondition: CI.CommitBranch }),
358
+ ],
359
+ autoCancel: new AutoCancel({
360
+ onNewCommit: "interruptible",
361
+ }),
362
+ });
363
+ \`\`\`
364
+
365
+ ## Default
366
+
367
+ \`Default\` sets shared configuration inherited by all jobs:
368
+
369
+ \`\`\`typescript
370
+ export const defaults = new Default({
371
+ image: new Image({ name: "node:20-alpine" }),
372
+ cache: new Cache({ key: CI.CommitRef, paths: ["node_modules/"] }),
373
+ retry: new Retry({ max: 2, when: ["runner_system_failure"] }),
374
+ });
375
+ \`\`\`
376
+
377
+ Jobs can override any default property individually.
378
+
379
+ ## Triggers
380
+
381
+ \`Trigger\` creates downstream pipeline jobs:
382
+
383
+ \`\`\`typescript
384
+ export const deployInfra = new Job({
385
+ stage: "deploy",
386
+ trigger: new Trigger({
387
+ project: "my-group/infra-repo",
388
+ branch: "main",
389
+ strategy: "depend",
390
+ }),
391
+ });
392
+ \`\`\``,
393
+ },
394
+ {
395
+ slug: "variables",
396
+ title: "Predefined Variables",
397
+ description: "GitLab CI/CD predefined variable references",
398
+ content: `The \`CI\` object provides type-safe access to GitLab CI/CD predefined variables. These map to \`$CI_*\` environment variables at runtime.
399
+
400
+ \`\`\`typescript
401
+ import { CI, Job, Rule } from "@intentius/chant-lexicon-gitlab";
402
+
403
+ // Use in rule conditions
404
+ const onDefault = new Rule({
405
+ ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\`,
406
+ });
407
+
408
+ // Use in cache keys
409
+ const cache = new Cache({
410
+ key: CI.CommitRef, // → $CI_COMMIT_REF_NAME
411
+ paths: ["node_modules/"],
412
+ });
413
+
414
+ // Use in workflow names
415
+ const workflow = new Workflow({
416
+ name: \`Pipeline for \${CI.CommitRef}\`,
417
+ });
418
+ \`\`\`
419
+
420
+ ## Variable reference
421
+
422
+ | Property | Variable | Description |
423
+ |----------|----------|-------------|
424
+ | \`CI.CommitBranch\` | \`$CI_COMMIT_BRANCH\` | Current branch name (not set for tag pipelines) |
425
+ | \`CI.CommitRef\` | \`$CI_COMMIT_REF_NAME\` | Branch or tag name |
426
+ | \`CI.CommitSha\` | \`$CI_COMMIT_SHA\` | Full commit SHA |
427
+ | \`CI.CommitTag\` | \`$CI_COMMIT_TAG\` | Tag name (only set for tag pipelines) |
428
+ | \`CI.DefaultBranch\` | \`$CI_DEFAULT_BRANCH\` | Default branch (usually \`main\`) |
429
+ | \`CI.Environment\` | \`$CI_ENVIRONMENT_NAME\` | Environment name (set during deploy jobs) |
430
+ | \`CI.JobId\` | \`$CI_JOB_ID\` | Unique job ID |
431
+ | \`CI.JobName\` | \`$CI_JOB_NAME\` | Job name |
432
+ | \`CI.JobStage\` | \`$CI_JOB_STAGE\` | Job stage name |
433
+ | \`CI.MergeRequestIid\` | \`$CI_MERGE_REQUEST_IID\` | MR internal ID (merge request pipelines only) |
434
+ | \`CI.PipelineId\` | \`$CI_PIPELINE_ID\` | Unique pipeline ID |
435
+ | \`CI.PipelineSource\` | \`$CI_PIPELINE_SOURCE\` | How the pipeline was triggered (\`push\`, \`merge_request_event\`, \`schedule\`, etc.) |
436
+ | \`CI.ProjectDir\` | \`$CI_PROJECT_DIR\` | Full path of the repository clone |
437
+ | \`CI.ProjectId\` | \`$CI_PROJECT_ID\` | Unique project ID |
438
+ | \`CI.ProjectName\` | \`$CI_PROJECT_NAME\` | Project name (URL-safe) |
439
+ | \`CI.ProjectPath\` | \`$CI_PROJECT_PATH\` | Project namespace with project name |
440
+ | \`CI.Registry\` | \`$CI_REGISTRY\` | Container registry URL |
441
+ | \`CI.RegistryImage\` | \`$CI_REGISTRY_IMAGE\` | Registry image path for the project |
442
+
443
+ ## Common patterns
444
+
445
+ **Conditional on branch type:**
446
+
447
+ \`\`\`typescript
448
+ // Only on merge requests
449
+ new Rule({ ifCondition: CI.MergeRequestIid })
450
+
451
+ // Only on default branch
452
+ new Rule({ ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\` })
453
+
454
+ // Only on tags
455
+ new Rule({ ifCondition: CI.CommitTag })
456
+ \`\`\`
457
+
458
+ **Dynamic naming:**
459
+
460
+ \`\`\`typescript
461
+ export const deploy = new Job({
462
+ stage: "deploy",
463
+ environment: new Environment({
464
+ name: \`review/\${CI.CommitRef}\`,
465
+ url: \`https://\${CI.CommitRef}.preview.example.com\`,
466
+ }),
467
+ script: ["deploy-preview"],
468
+ });
469
+ \`\`\`
470
+
471
+ **Container registry:**
472
+
473
+ \`\`\`typescript
474
+ export const buildImage = new Job({
475
+ stage: "build",
476
+ image: new Image({ name: "docker:24" }),
477
+ script: [
478
+ \`docker build -t \${CI.RegistryImage}:\${CI.CommitSha} .\`,
479
+ \`docker push \${CI.RegistryImage}:\${CI.CommitSha}\`,
480
+ ],
481
+ });
482
+ \`\`\`
483
+ `,
484
+ },
485
+ {
486
+ slug: "intrinsics",
487
+ title: "Intrinsic Functions",
488
+ description: "GitLab CI/CD intrinsic functions and their chant syntax",
489
+ content: `The GitLab lexicon provides one intrinsic function: \`reference()\`, which maps to GitLab's \`!reference\` YAML tag.
490
+
491
+ \`\`\`typescript
492
+ import { reference } from "@intentius/chant-lexicon-gitlab";
493
+ \`\`\`
494
+
495
+ ## \`reference()\` — reuse job properties
496
+
497
+ The \`reference()\` intrinsic lets you reuse properties from other jobs or hidden keys. It produces the \`!reference\` YAML tag:
498
+
499
+ \`\`\`typescript
500
+ import { reference, Job } from "@intentius/chant-lexicon-gitlab";
501
+
502
+ export const deploy = new Job({
503
+ script: reference(".setup", "script"),
504
+ });
505
+ \`\`\`
506
+
507
+ Serializes to:
508
+
509
+ \`\`\`yaml
510
+ deploy:
511
+ script: !reference [.setup, script]
512
+ \`\`\`
513
+
514
+ ### Syntax
515
+
516
+ \`\`\`typescript
517
+ reference(jobName: string, property: string): ReferenceTag
518
+ \`\`\`
519
+
520
+ - \`jobName\` — the job or hidden key to reference (e.g. \`".setup"\`, \`"build"\`)
521
+ - \`property\` — the property to extract (e.g. \`"script"\`, \`"before_script"\`, \`"rules"\`)
522
+
523
+ ### Use cases
524
+
525
+ **Shared setup scripts:**
526
+
527
+ \`\`\`typescript
528
+ // Hidden key with shared setup (defined in .gitlab-ci.yml or included)
529
+ // Reference its script from multiple jobs:
530
+
531
+ export const test = new Job({
532
+ stage: "test",
533
+ beforeScript: reference(".node-setup", "before_script"),
534
+ script: ["npm test"],
535
+ });
536
+
537
+ export const lint = new Job({
538
+ stage: "test",
539
+ beforeScript: reference(".node-setup", "before_script"),
540
+ script: ["npm run lint"],
541
+ });
542
+ \`\`\`
543
+
544
+ Produces:
545
+
546
+ \`\`\`yaml
547
+ test:
548
+ stage: test
549
+ before_script: !reference [.node-setup, before_script]
550
+ script:
551
+ - npm test
552
+
553
+ lint:
554
+ stage: test
555
+ before_script: !reference [.node-setup, before_script]
556
+ script:
557
+ - npm run lint
558
+ \`\`\`
559
+
560
+ **Shared rules:**
561
+
562
+ \`\`\`typescript
563
+ export const build = new Job({
564
+ stage: "build",
565
+ rules: reference(".default-rules", "rules"),
566
+ script: ["npm run build"],
567
+ });
568
+ \`\`\`
569
+
570
+ **Nested references (multi-level):**
571
+
572
+ \`\`\`typescript
573
+ // Reference a specific nested element
574
+ export const deploy = new Job({
575
+ script: reference(".setup", "script"),
576
+ environment: reference(".deploy-defaults", "environment"),
577
+ });
578
+ \`\`\`
579
+
580
+ ### When to use \`reference()\` vs barrel imports
581
+
582
+ Use **barrel imports** (\`_.$\`) when referencing chant-managed objects — the serializer resolves them at build time:
583
+
584
+ \`\`\`typescript
585
+ // Preferred for chant-managed config
586
+ export const test = new Job({
587
+ cache: _.npmCache, // resolved at build time
588
+ artifacts: _.testArtifacts, // resolved at build time
589
+ });
590
+ \`\`\`
591
+
592
+ Use **\`reference()\`** when referencing jobs or hidden keys defined outside chant (e.g. in included YAML files or templates):
593
+
594
+ \`\`\`typescript
595
+ // For external/included YAML definitions
596
+ export const test = new Job({
597
+ beforeScript: reference(".ci-setup", "before_script"),
598
+ });
599
+ \`\`\`
600
+ `,
601
+ },
602
+ {
603
+ slug: "lint-rules",
604
+ title: "Lint Rules",
605
+ description: "Built-in lint rules and post-synth checks for GitLab CI/CD",
606
+ content: `The GitLab lexicon ships lint rules that run during \`chant lint\` and post-synth checks that validate the serialized YAML after \`chant build\`.
607
+
608
+ ## Lint rules
609
+
610
+ Lint rules analyze your TypeScript source code before build.
611
+
612
+ ### WGL001 — Deprecated only/except
613
+
614
+ **Severity:** warning | **Category:** style
615
+
616
+ Flags usage of \`only:\` and \`except:\` keywords, which are deprecated in favor of \`rules:\`. The \`rules:\` syntax is more flexible and is the recommended approach.
617
+
618
+ \`\`\`typescript
619
+ // Triggers WGL001
620
+ export const deploy = new Job({
621
+ stage: "deploy",
622
+ script: ["npm run deploy"],
623
+ only: ["main"], // deprecated
624
+ });
625
+
626
+ // Fixed — use rules instead
627
+ export const deploy = new Job({
628
+ stage: "deploy",
629
+ script: ["npm run deploy"],
630
+ rules: [new Rule({
631
+ ifCondition: \`\${CI.CommitBranch} == \${CI.DefaultBranch}\`,
632
+ })],
633
+ });
634
+ \`\`\`
635
+
636
+ ### WGL002 — Missing script
637
+
638
+ **Severity:** error | **Category:** correctness
639
+
640
+ A GitLab CI job must have \`script\`, \`trigger\`, or \`run\` defined. Jobs without any of these will fail pipeline validation.
641
+
642
+ \`\`\`typescript
643
+ // Triggers WGL002
644
+ export const build = new Job({
645
+ stage: "build",
646
+ image: new Image({ name: "node:20" }),
647
+ // Missing script!
648
+ });
649
+
650
+ // Fixed — add script
651
+ export const build = new Job({
652
+ stage: "build",
653
+ image: new Image({ name: "node:20" }),
654
+ script: ["npm run build"],
655
+ });
656
+
657
+ // Also valid — trigger job (no script needed)
658
+ export const downstream = new Job({
659
+ trigger: new Trigger({ project: "my-group/other-repo" }),
660
+ });
661
+ \`\`\`
662
+
663
+ ### WGL003 — Missing stage
664
+
665
+ **Severity:** info | **Category:** style
666
+
667
+ Jobs should declare a \`stage\` property. Without it, the job defaults to the \`test\` stage, which may not be the intended behavior.
668
+
669
+ \`\`\`typescript
670
+ // Triggers WGL003
671
+ export const build = new Job({
672
+ script: ["npm run build"],
673
+ // No stage — defaults to "test"
674
+ });
675
+
676
+ // Fixed — declare the stage
677
+ export const build = new Job({
678
+ stage: "build",
679
+ script: ["npm run build"],
680
+ });
681
+ \`\`\`
682
+
683
+ ### WGL004 — Artifacts without expiry
684
+
685
+ **Severity:** warning | **Category:** performance
686
+
687
+ Flags \`Artifacts\` without \`expireIn\`. Artifacts without expiry are kept indefinitely, consuming storage. Always set an expiration.
688
+
689
+ \`\`\`typescript
690
+ // Triggers WGL004
691
+ export const build = new Job({
692
+ script: ["npm run build"],
693
+ artifacts: new Artifacts({
694
+ paths: ["dist/"],
695
+ // Missing expireIn!
696
+ }),
697
+ });
698
+
699
+ // Fixed — set expiry
700
+ export const build = new Job({
701
+ script: ["npm run build"],
702
+ artifacts: new Artifacts({
703
+ paths: ["dist/"],
704
+ expireIn: "1 hour",
705
+ }),
706
+ });
707
+ \`\`\`
708
+
709
+ ## Post-synth checks
710
+
711
+ Post-synth checks run against the serialized YAML after build. They catch issues only visible in the final output.
712
+
713
+ ### WGL010 — Undefined stage
714
+
715
+ **Severity:** error
716
+
717
+ Flags jobs that reference a stage not present in the collected stages list. This causes a pipeline validation error in GitLab.
718
+
719
+ ### WGL011 — Unreachable job
720
+
721
+ **Severity:** warning
722
+
723
+ Flags jobs where all \`rules:\` entries have \`when: "never"\`, making the job unreachable. This usually indicates a configuration error.
724
+
725
+ \`\`\`typescript
726
+ // Triggers WGL011 — job can never run
727
+ export const noop = new Job({
728
+ script: ["echo unreachable"],
729
+ rules: [
730
+ new Rule({ ifCondition: CI.CommitBranch, when: "never" }),
731
+ new Rule({ ifCondition: CI.CommitTag, when: "never" }),
732
+ ],
733
+ });
734
+ \`\`\`
735
+
736
+ ## Running lint
737
+
738
+ \`\`\`bash
739
+ # Lint your chant project
740
+ chant lint
741
+
742
+ # Lint with auto-fix where supported
743
+ chant lint --fix
744
+ \`\`\`
745
+
746
+ To suppress a rule on a specific line:
747
+
748
+ \`\`\`typescript
749
+ // chant-disable-next-line WGL001
750
+ export const deploy = new Job({ only: ["main"], script: ["deploy"] });
751
+ \`\`\`
752
+
753
+ To suppress globally in \`chant.config.ts\`:
754
+
755
+ \`\`\`typescript
756
+ export default {
757
+ lint: {
758
+ rules: {
759
+ WGL003: "off", // don't require stage on every job
760
+ },
761
+ },
762
+ };
763
+ \`\`\`
764
+ `,
765
+ },
766
+ {
767
+ slug: "examples",
768
+ title: "Examples",
769
+ description: "Walkthrough of the getting-started GitLab CI/CD example",
770
+ content: `A runnable example lives in the lexicon's \`examples/\` directory. Clone the repo and try it:
771
+
772
+ \`\`\`bash
773
+ cd examples/getting-started
774
+ bun install
775
+ chant build # produces .gitlab-ci.yml
776
+ chant lint # runs lint rules
777
+ bun test # runs the example's tests
778
+ \`\`\`
779
+
780
+ ## Getting Started
781
+
782
+ \`examples/getting-started/\` — a 3-stage Node.js pipeline with build, test, and deploy jobs.
783
+
784
+ \`\`\`
785
+ src/
786
+ ├── _.ts # Barrel — re-exports lexicon + shared config
787
+ ├── config.ts # Shared config: images, caches, artifacts, rules, environments
788
+ └── pipeline.ts # Job definitions: build, test, deploy
789
+ \`\`\`
790
+
791
+ ### Barrel file
792
+
793
+ The barrel re-exports both the lexicon and shared config, so pipeline files only need one import:
794
+
795
+ \`\`\`typescript
796
+ // _.ts
797
+ export * from "@intentius/chant-lexicon-gitlab";
798
+ export * from "./config";
799
+ \`\`\`
800
+
801
+ ### Shared configuration
802
+
803
+ \`config.ts\` extracts reusable objects — images, caches, artifacts, rules, and environments — so jobs stay concise:
804
+
805
+ \`\`\`typescript
806
+ // config.ts
807
+ import * as _ from "./_";
808
+
809
+ export const nodeImage = new _.Image({ name: "node:20-alpine" });
810
+
811
+ export const npmCache = new _.Cache({
812
+ key: "$CI_COMMIT_REF_SLUG",
813
+ paths: ["node_modules/"],
814
+ policy: "pull-push",
815
+ });
816
+
817
+ export const buildArtifacts = new _.Artifacts({
818
+ paths: ["dist/"],
819
+ expireIn: "1 hour",
820
+ });
821
+
822
+ export const testArtifacts = new _.Artifacts({
823
+ paths: ["coverage/"],
824
+ expireIn: "1 week",
825
+ reports: { junit: "coverage/junit.xml" },
826
+ });
827
+
828
+ export const onMergeRequest = new _.Rule({
829
+ ifCondition: _.CI.MergeRequestIid,
830
+ });
831
+
832
+ export const onCommit = new _.Rule({
833
+ ifCondition: _.CI.CommitBranch,
834
+ });
835
+
836
+ export const onDefaultBranch = new _.Rule({
837
+ ifCondition: \`\${_.CI.CommitBranch} == \${_.CI.DefaultBranch}\`,
838
+ when: "manual",
839
+ });
840
+
841
+ export const productionEnv = new _.Environment({
842
+ name: "production",
843
+ url: "https://example.com",
844
+ });
845
+ \`\`\`
846
+
847
+ ### Pipeline jobs
848
+
849
+ \`pipeline.ts\` defines three jobs that reference shared config via the barrel:
850
+
851
+ \`\`\`typescript
852
+ // pipeline.ts
853
+ import * as _ from "./_";
854
+
855
+ export const build = new _.Job({
856
+ stage: "build",
857
+ image: _.nodeImage,
858
+ cache: _.npmCache,
859
+ script: ["npm ci", "npm run build"],
860
+ artifacts: _.buildArtifacts,
861
+ });
862
+
863
+ export const test = new _.Job({
864
+ stage: "test",
865
+ image: _.nodeImage,
866
+ cache: _.npmCache,
867
+ script: ["npm ci", "npm test"],
868
+ artifacts: _.testArtifacts,
869
+ rules: [_.onMergeRequest, _.onCommit],
870
+ });
871
+
872
+ export const deploy = new _.Job({
873
+ stage: "deploy",
874
+ image: _.nodeImage,
875
+ script: ["npm run deploy"],
876
+ environment: _.productionEnv,
877
+ rules: [_.onDefaultBranch],
878
+ });
879
+ \`\`\`
880
+
881
+ ### Generated output
882
+
883
+ \`chant build\` produces this \`.gitlab-ci.yml\`:
884
+
885
+ \`\`\`yaml
886
+ stages:
887
+ - build
888
+ - test
889
+ - deploy
890
+
891
+ build:
892
+ stage: build
893
+ image: node:20-alpine
894
+ cache:
895
+ key: $CI_COMMIT_REF_SLUG
896
+ paths:
897
+ - node_modules/
898
+ policy: pull-push
899
+ script:
900
+ - npm ci
901
+ - npm run build
902
+ artifacts:
903
+ paths:
904
+ - dist/
905
+ expire_in: 1 hour
906
+
907
+ test:
908
+ stage: test
909
+ image: node:20-alpine
910
+ cache:
911
+ key: $CI_COMMIT_REF_SLUG
912
+ paths:
913
+ - node_modules/
914
+ policy: pull-push
915
+ script:
916
+ - npm ci
917
+ - npm test
918
+ artifacts:
919
+ paths:
920
+ - coverage/
921
+ expire_in: 1 week
922
+ reports:
923
+ junit: coverage/junit.xml
924
+ rules:
925
+ - if: $CI_MERGE_REQUEST_IID
926
+ - if: $CI_COMMIT_BRANCH
927
+
928
+ deploy:
929
+ stage: deploy
930
+ image: node:20-alpine
931
+ script:
932
+ - npm run deploy
933
+ environment:
934
+ name: production
935
+ url: https://example.com
936
+ rules:
937
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
938
+ when: manual
939
+ \`\`\`
940
+
941
+ **Patterns demonstrated:**
942
+
943
+ 1. **Barrel file** — single import point for lexicon types and shared config
944
+ 2. **Shared config** — reusable images, caches, artifacts, and rules extracted into \`config.ts\`
945
+ 3. **Conditional execution** — merge request and branch rules control when jobs run
946
+ 4. **Manual deployment** — deploy requires manual trigger on the default branch
947
+ 5. **JUnit reports** — test artifacts include JUnit XML for GitLab MR display
948
+ `,
949
+ },
950
+ ],
951
+ basePath: "/lexicons/gitlab/",
952
+ };
953
+
954
+ const result = await docsPipeline(config);
955
+ writeDocsSite(config, result);
956
+
957
+ if (opts?.verbose) {
958
+ console.error(
959
+ `Generated docs: ${result.stats.resources} resources, ${result.stats.properties} properties, ${result.stats.services} services, ${result.stats.rules} rules`,
960
+ );
961
+ }
962
+ }