@intentius/chant-lexicon-gitlab 0.0.4 → 0.0.6
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/README.md +9 -46
- package/dist/integrity.json +4 -4
- package/dist/manifest.json +1 -1
- package/dist/meta.json +5 -0
- package/dist/types/index.d.ts +13 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +30 -438
- package/src/codegen/generate-lexicon.ts +1 -1
- package/src/codegen/parse.test.ts +5 -4
- package/src/codegen/parse.ts +8 -10
- package/src/codegen/snapshot.test.ts +3 -3
- package/src/generated/index.d.ts +13 -0
- package/src/generated/index.ts +1 -0
- package/src/generated/lexicon-gitlab.json +5 -0
- package/src/plugin.test.ts +1 -2
- package/src/plugin.ts +13 -40
- package/src/validate.test.ts +13 -22
- package/src/validate.ts +17 -108
package/README.md
CHANGED
|
@@ -1,58 +1,21 @@
|
|
|
1
1
|
# @intentius/chant-lexicon-gitlab
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GitLab CI lexicon for [chant](https://intentius.io/chant/) — declare CI/CD pipelines as typed TypeScript that serializes to `.gitlab-ci.yml`.
|
|
4
4
|
|
|
5
|
-
GitLab CI
|
|
5
|
+
This package provides typed constructors for all GitLab CI keywords (Jobs, Workflows, Defaults, and property types like Artifacts, Cache, Image, Rule, Environment, and Trigger), the `CI` pseudo-parameter object for predefined variables, the `reference()` intrinsic for YAML `!reference` tags, and GitLab-specific lint rules. It also includes LSP and MCP server support for editor completions and hover.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
This package provides:
|
|
10
|
-
|
|
11
|
-
- **GitLab CI serializer** — converts chant declarables to GitLab CI YAML
|
|
12
|
-
- **Resource types** — typed constructors for `Job`, `Default`, `Workflow`, and all GitLab CI keywords
|
|
13
|
-
- **Property types** — `Artifacts`, `Cache`, `Image`, `Rule`, `Retry`, `Environment`, `Trigger`, and more
|
|
14
|
-
- **Lint rules** — GitLab-specific validation (e.g. missing script, deprecated only/except)
|
|
15
|
-
- **Code generation** — generates TypeScript types from the GitLab CI JSON schema
|
|
16
|
-
- **LSP/MCP support** — completions and hover for GitLab CI keywords
|
|
17
|
-
|
|
18
|
-
## Usage
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { Job, Artifacts, Image } from "@intentius/chant-lexicon-gitlab";
|
|
22
|
-
|
|
23
|
-
export const testJob = new Job({
|
|
24
|
-
stage: "test",
|
|
25
|
-
image: new Image({ name: "node:20" }),
|
|
26
|
-
script: ["npm ci", "npm test"],
|
|
27
|
-
artifacts: new Artifacts({
|
|
28
|
-
paths: ["coverage/"],
|
|
29
|
-
expireIn: "1 week",
|
|
30
|
-
}),
|
|
31
|
-
});
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @intentius/chant @intentius/chant-lexicon-gitlab
|
|
32
9
|
```
|
|
33
10
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
| Rule | Description |
|
|
37
|
-
|------|-------------|
|
|
38
|
-
| `missing-script` | Job must have a `script` keyword |
|
|
39
|
-
| `missing-stage` | Job should declare a `stage` |
|
|
40
|
-
| `deprecated-only-except` | Flags use of deprecated `only`/`except` keywords |
|
|
41
|
-
| `artifact-no-expiry` | Artifacts should have `expire_in` set |
|
|
42
|
-
|
|
43
|
-
## Code Generation
|
|
44
|
-
|
|
45
|
-
The GitLab lexicon generates types from the [GitLab CI JSON schema](https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json):
|
|
46
|
-
|
|
47
|
-
- `codegen/generate.ts` — calls core `generatePipeline<GitLabParseResult>` with GitLab callbacks
|
|
48
|
-
- `codegen/naming.ts` — extends core `NamingStrategy` for GitLab CI keywords
|
|
49
|
-
- `codegen/package.ts` — calls core `packagePipeline` with GitLab manifest
|
|
50
|
-
- `codegen/parse.ts` — parses the GitLab CI JSON schema into typed entities
|
|
11
|
+
**[Documentation →](https://intentius.io/chant/lexicons/gitlab/)**
|
|
51
12
|
|
|
52
13
|
## Related Packages
|
|
53
14
|
|
|
54
|
-
|
|
55
|
-
|
|
15
|
+
| Package | Role |
|
|
16
|
+
|---------|------|
|
|
17
|
+
| [@intentius/chant](https://www.npmjs.com/package/@intentius/chant) | Core type system, CLI, build pipeline |
|
|
18
|
+
| [@intentius/chant-lexicon-aws](https://www.npmjs.com/package/@intentius/chant-lexicon-aws) | AWS CloudFormation lexicon |
|
|
56
19
|
|
|
57
20
|
## License
|
|
58
21
|
|
package/dist/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "xxhash64",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
5
|
-
"meta.json": "
|
|
6
|
-
"types/index.d.ts": "
|
|
4
|
+
"manifest.json": "c72b64b58dec21e0",
|
|
5
|
+
"meta.json": "9ee0d2f2d1679f09",
|
|
6
|
+
"types/index.d.ts": "4e56a7de40d655c0",
|
|
7
7
|
"rules/missing-stage.ts": "6d5379e74209a735",
|
|
8
8
|
"rules/missing-script.ts": "923dde9acb46cc28",
|
|
9
9
|
"rules/deprecated-only-except.ts": "1f5a8c785777fb03",
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
"rules/wgl010.ts": "1548cad287cdf286",
|
|
14
14
|
"skills/gitlab-ci.md": "f860e40c2643c327"
|
|
15
15
|
},
|
|
16
|
-
"composite": "
|
|
16
|
+
"composite": "95b9f28f579d5862"
|
|
17
17
|
}
|
package/dist/manifest.json
CHANGED
package/dist/meta.json
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -203,6 +203,19 @@ export declare class Rule {
|
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export declare class Service {
|
|
207
|
+
constructor(props: {
|
|
208
|
+
/** Full name of the image that should be used. It should contain the Registry part if needed. */
|
|
209
|
+
name: string;
|
|
210
|
+
alias?: string;
|
|
211
|
+
command?: string[];
|
|
212
|
+
docker?: Record<string, unknown>;
|
|
213
|
+
entrypoint?: string[];
|
|
214
|
+
pull_policy?: "always" | "never" | "if-not-present" | "always" | "never" | "if-not-present"[];
|
|
215
|
+
variables?: Record<string, unknown>;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
206
219
|
export declare class Trigger {
|
|
207
220
|
constructor(props: {
|
|
208
221
|
/** Path to the project, e.g. `group/project`, or `group/sub-group/project`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gitlab",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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.
|
|
24
|
+
"@intentius/chant": "0.0.5"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"typescript": "^5.9.3"
|
package/src/codegen/docs.ts
CHANGED
|
@@ -28,20 +28,7 @@ npm install --save-dev @intentius/chant-lexicon-gitlab
|
|
|
28
28
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
\`\`\`
|
|
31
|
+
{{file:docs-snippets/src/quickstart.ts}}
|
|
45
32
|
|
|
46
33
|
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
34
|
`;
|
|
@@ -113,6 +100,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
|
|
|
113
100
|
outputFormat,
|
|
114
101
|
serviceFromType,
|
|
115
102
|
suppressPages: ["intrinsics", "rules"],
|
|
103
|
+
examplesDir: join(pkgDir, "examples"),
|
|
116
104
|
extraPages: [
|
|
117
105
|
{
|
|
118
106
|
slug: "pipeline-concepts",
|
|
@@ -125,14 +113,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
|
|
|
125
113
|
- Collects stages from all jobs into a \`stages:\` list
|
|
126
114
|
- Collapses single-property objects (\`new Image({ name: "node:20" })\` → \`image: node:20\`)
|
|
127
115
|
|
|
128
|
-
|
|
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
|
-
\`\`\`
|
|
116
|
+
{{file:docs-snippets/src/job-basic.ts}}
|
|
136
117
|
|
|
137
118
|
Produces this YAML:
|
|
138
119
|
|
|
@@ -177,47 +158,17 @@ The lexicon provides 3 resource types and 13 property types:
|
|
|
177
158
|
| \`Release\` | Job | GitLab Release creation |
|
|
178
159
|
| \`AutoCancel\` | Workflow | Pipeline auto-cancellation settings |
|
|
179
160
|
|
|
180
|
-
##
|
|
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
|
-
\`\`\`
|
|
161
|
+
## Shared config
|
|
189
162
|
|
|
190
|
-
|
|
163
|
+
Extract reusable objects into a shared config file and import them across your pipeline files:
|
|
191
164
|
|
|
192
|
-
|
|
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
|
-
\`\`\`
|
|
165
|
+
{{file:docs-snippets/src/pipeline-barrel.ts}}
|
|
204
166
|
|
|
205
167
|
## Jobs
|
|
206
168
|
|
|
207
169
|
A \`Job\` is the fundamental unit. Every exported \`Job\` becomes a job entry in the YAML:
|
|
208
170
|
|
|
209
|
-
|
|
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
|
-
\`\`\`
|
|
171
|
+
{{file:docs-snippets/src/job-test.ts}}
|
|
221
172
|
|
|
222
173
|
Key properties:
|
|
223
174
|
- \`script\` — **required** (or \`trigger\`/\`run\`). Array of shell commands to execute.
|
|
@@ -229,12 +180,7 @@ Key properties:
|
|
|
229
180
|
|
|
230
181
|
Stages define the execution order of a pipeline. The serializer automatically collects unique stage values from all jobs:
|
|
231
182
|
|
|
232
|
-
|
|
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
|
-
\`\`\`
|
|
183
|
+
{{file:docs-snippets/src/stages.ts}}
|
|
238
184
|
|
|
239
185
|
Produces:
|
|
240
186
|
|
|
@@ -249,30 +195,9 @@ Jobs in the same stage run in parallel. Stages run sequentially in declaration o
|
|
|
249
195
|
|
|
250
196
|
## Artifacts and caching
|
|
251
197
|
|
|
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
|
-
\`\`\`
|
|
198
|
+
**Artifacts** are files produced by a job and passed to later stages or stored for download. **Caches** persist files between pipeline runs to speed up builds. Both are shown in the shared config:
|
|
266
199
|
|
|
267
|
-
|
|
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
|
-
\`\`\`
|
|
200
|
+
{{file:docs-snippets/src/config.ts:4-22}}
|
|
276
201
|
|
|
277
202
|
The key difference: artifacts are for passing files between **stages in the same pipeline**; caches are for speeding up **repeated pipeline runs**.
|
|
278
203
|
|
|
@@ -280,22 +205,7 @@ The key difference: artifacts are for passing files between **stages in the same
|
|
|
280
205
|
|
|
281
206
|
\`Rule\` objects control when a job runs. They map to \`rules:\` entries in the YAML:
|
|
282
207
|
|
|
283
|
-
|
|
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
|
-
\`\`\`
|
|
208
|
+
{{file:docs-snippets/src/rules-conditions.ts}}
|
|
299
209
|
|
|
300
210
|
Produces:
|
|
301
211
|
|
|
@@ -315,19 +225,7 @@ The \`ifCondition\` property maps to \`if:\` in the YAML (since \`if\` is a rese
|
|
|
315
225
|
|
|
316
226
|
\`Environment\` defines a deployment target:
|
|
317
227
|
|
|
318
|
-
|
|
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
|
-
\`\`\`
|
|
228
|
+
{{file:docs-snippets/src/environment.ts}}
|
|
331
229
|
|
|
332
230
|
GitLab tracks deployments to environments and provides rollback capabilities in the UI.
|
|
333
231
|
|
|
@@ -335,44 +233,19 @@ GitLab tracks deployments to environments and provides rollback capabilities in
|
|
|
335
233
|
|
|
336
234
|
\`Image\` specifies the Docker image for a job:
|
|
337
235
|
|
|
338
|
-
|
|
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
|
-
\`\`\`
|
|
236
|
+
{{file:docs-snippets/src/images.ts}}
|
|
347
237
|
|
|
348
238
|
## Workflow
|
|
349
239
|
|
|
350
240
|
\`Workflow\` controls pipeline-level settings — when pipelines run, auto-cancellation, and global includes:
|
|
351
241
|
|
|
352
|
-
|
|
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
|
-
\`\`\`
|
|
242
|
+
{{file:docs-snippets/src/workflow.ts}}
|
|
364
243
|
|
|
365
244
|
## Default
|
|
366
245
|
|
|
367
246
|
\`Default\` sets shared configuration inherited by all jobs:
|
|
368
247
|
|
|
369
|
-
|
|
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
|
-
\`\`\`
|
|
248
|
+
{{file:docs-snippets/src/defaults.ts}}
|
|
376
249
|
|
|
377
250
|
Jobs can override any default property individually.
|
|
378
251
|
|
|
@@ -380,16 +253,7 @@ Jobs can override any default property individually.
|
|
|
380
253
|
|
|
381
254
|
\`Trigger\` creates downstream pipeline jobs:
|
|
382
255
|
|
|
383
|
-
|
|
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
|
-
\`\`\``,
|
|
256
|
+
{{file:docs-snippets/src/trigger.ts}}`,
|
|
393
257
|
},
|
|
394
258
|
{
|
|
395
259
|
slug: "variables",
|
|
@@ -397,25 +261,7 @@ export const deployInfra = new Job({
|
|
|
397
261
|
description: "GitLab CI/CD predefined variable references",
|
|
398
262
|
content: `The \`CI\` object provides type-safe access to GitLab CI/CD predefined variables. These map to \`$CI_*\` environment variables at runtime.
|
|
399
263
|
|
|
400
|
-
|
|
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
|
-
\`\`\`
|
|
264
|
+
{{file:docs-snippets/src/variables-usage.ts}}
|
|
419
265
|
|
|
420
266
|
## Variable reference
|
|
421
267
|
|
|
@@ -442,44 +288,7 @@ const workflow = new Workflow({
|
|
|
442
288
|
|
|
443
289
|
## Common patterns
|
|
444
290
|
|
|
445
|
-
|
|
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
|
-
\`\`\`
|
|
291
|
+
{{file:docs-snippets/src/variables-patterns.ts}}
|
|
483
292
|
`,
|
|
484
293
|
},
|
|
485
294
|
{
|
|
@@ -488,21 +297,11 @@ export const buildImage = new Job({
|
|
|
488
297
|
description: "GitLab CI/CD intrinsic functions and their chant syntax",
|
|
489
298
|
content: `The GitLab lexicon provides one intrinsic function: \`reference()\`, which maps to GitLab's \`!reference\` YAML tag.
|
|
490
299
|
|
|
491
|
-
\`\`\`typescript
|
|
492
|
-
import { reference } from "@intentius/chant-lexicon-gitlab";
|
|
493
|
-
\`\`\`
|
|
494
|
-
|
|
495
300
|
## \`reference()\` — reuse job properties
|
|
496
301
|
|
|
497
302
|
The \`reference()\` intrinsic lets you reuse properties from other jobs or hidden keys. It produces the \`!reference\` YAML tag:
|
|
498
303
|
|
|
499
|
-
|
|
500
|
-
import { reference, Job } from "@intentius/chant-lexicon-gitlab";
|
|
501
|
-
|
|
502
|
-
export const deploy = new Job({
|
|
503
|
-
script: reference(".setup", "script"),
|
|
504
|
-
});
|
|
505
|
-
\`\`\`
|
|
304
|
+
{{file:docs-snippets/src/reference-basic.ts}}
|
|
506
305
|
|
|
507
306
|
Serializes to:
|
|
508
307
|
|
|
@@ -522,24 +321,7 @@ reference(jobName: string, property: string): ReferenceTag
|
|
|
522
321
|
|
|
523
322
|
### Use cases
|
|
524
323
|
|
|
525
|
-
|
|
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
|
-
\`\`\`
|
|
324
|
+
{{file:docs-snippets/src/reference-shared.ts}}
|
|
543
325
|
|
|
544
326
|
Produces:
|
|
545
327
|
|
|
@@ -557,46 +339,9 @@ lint:
|
|
|
557
339
|
- npm run lint
|
|
558
340
|
\`\`\`
|
|
559
341
|
|
|
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
342
|
### When to use \`reference()\` vs barrel imports
|
|
581
343
|
|
|
582
|
-
|
|
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
|
-
\`\`\`
|
|
344
|
+
{{file:docs-snippets/src/reference-vs-barrel.ts}}
|
|
600
345
|
`,
|
|
601
346
|
},
|
|
602
347
|
{
|
|
@@ -615,23 +360,7 @@ Lint rules analyze your TypeScript source code before build.
|
|
|
615
360
|
|
|
616
361
|
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
362
|
|
|
618
|
-
|
|
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
|
-
\`\`\`
|
|
363
|
+
{{file:docs-snippets/src/lint-wgl001.ts}}
|
|
635
364
|
|
|
636
365
|
### WGL002 — Missing script
|
|
637
366
|
|
|
@@ -639,26 +368,7 @@ export const deploy = new Job({
|
|
|
639
368
|
|
|
640
369
|
A GitLab CI job must have \`script\`, \`trigger\`, or \`run\` defined. Jobs without any of these will fail pipeline validation.
|
|
641
370
|
|
|
642
|
-
|
|
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
|
-
\`\`\`
|
|
371
|
+
{{file:docs-snippets/src/lint-wgl002.ts}}
|
|
662
372
|
|
|
663
373
|
### WGL003 — Missing stage
|
|
664
374
|
|
|
@@ -666,19 +376,7 @@ export const downstream = new Job({
|
|
|
666
376
|
|
|
667
377
|
Jobs should declare a \`stage\` property. Without it, the job defaults to the \`test\` stage, which may not be the intended behavior.
|
|
668
378
|
|
|
669
|
-
|
|
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
|
-
\`\`\`
|
|
379
|
+
{{file:docs-snippets/src/lint-wgl003.ts}}
|
|
682
380
|
|
|
683
381
|
### WGL004 — Artifacts without expiry
|
|
684
382
|
|
|
@@ -686,25 +384,7 @@ export const build = new Job({
|
|
|
686
384
|
|
|
687
385
|
Flags \`Artifacts\` without \`expireIn\`. Artifacts without expiry are kept indefinitely, consuming storage. Always set an expiration.
|
|
688
386
|
|
|
689
|
-
|
|
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
|
-
\`\`\`
|
|
387
|
+
{{file:docs-snippets/src/lint-wgl004.ts}}
|
|
708
388
|
|
|
709
389
|
## Post-synth checks
|
|
710
390
|
|
|
@@ -722,16 +402,7 @@ Flags jobs that reference a stage not present in the collected stages list. This
|
|
|
722
402
|
|
|
723
403
|
Flags jobs where all \`rules:\` entries have \`when: "never"\`, making the job unreachable. This usually indicates a configuration error.
|
|
724
404
|
|
|
725
|
-
|
|
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
|
-
\`\`\`
|
|
405
|
+
{{file:docs-snippets/src/lint-wgl011.ts}}
|
|
735
406
|
|
|
736
407
|
## Running lint
|
|
737
408
|
|
|
@@ -756,7 +427,7 @@ To suppress globally in \`chant.config.ts\`:
|
|
|
756
427
|
export default {
|
|
757
428
|
lint: {
|
|
758
429
|
rules: {
|
|
759
|
-
WGL003: "off",
|
|
430
|
+
WGL003: "off",
|
|
760
431
|
},
|
|
761
432
|
},
|
|
762
433
|
};
|
|
@@ -783,100 +454,21 @@ bun test # runs the example's tests
|
|
|
783
454
|
|
|
784
455
|
\`\`\`
|
|
785
456
|
src/
|
|
786
|
-
├── _.ts # Barrel — re-exports lexicon + shared config
|
|
787
457
|
├── config.ts # Shared config: images, caches, artifacts, rules, environments
|
|
788
458
|
└── pipeline.ts # Job definitions: build, test, deploy
|
|
789
459
|
\`\`\`
|
|
790
460
|
|
|
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
461
|
### Shared configuration
|
|
802
462
|
|
|
803
463
|
\`config.ts\` extracts reusable objects — images, caches, artifacts, rules, and environments — so jobs stay concise:
|
|
804
464
|
|
|
805
|
-
|
|
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
|
-
\`\`\`
|
|
465
|
+
{{file:getting-started/src/config.ts}}
|
|
846
466
|
|
|
847
467
|
### Pipeline jobs
|
|
848
468
|
|
|
849
|
-
\`pipeline.ts\` defines three jobs that
|
|
469
|
+
\`pipeline.ts\` defines three jobs that import shared config directly:
|
|
850
470
|
|
|
851
|
-
|
|
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
|
-
\`\`\`
|
|
471
|
+
{{file:getting-started/src/pipeline.ts}}
|
|
880
472
|
|
|
881
473
|
### Generated output
|
|
882
474
|
|
|
@@ -29,7 +29,7 @@ export function generateLexiconJSON(
|
|
|
29
29
|
typeName: r.resource.typeName,
|
|
30
30
|
attributes: r.resource.attributes,
|
|
31
31
|
properties: r.resource.properties,
|
|
32
|
-
propertyTypes: r.propertyTypes.map((pt) => ({ name: pt.name,
|
|
32
|
+
propertyTypes: r.propertyTypes.map((pt) => ({ name: pt.name, specType: pt.defType })),
|
|
33
33
|
}));
|
|
34
34
|
|
|
35
35
|
const entries = buildRegistry<LexiconEntry>(registryResources, naming, {
|
|
@@ -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 16 entities", () => {
|
|
9
9
|
const results = parseCISchema(fixture);
|
|
10
|
-
expect(results).toHaveLength(
|
|
10
|
+
expect(results).toHaveLength(16);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
test("returns 3 resource entities", () => {
|
|
@@ -20,14 +20,15 @@ describe("parseCISchema", () => {
|
|
|
20
20
|
expect(names).toContain("GitLab::CI::Workflow");
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
test("returns
|
|
23
|
+
test("returns 13 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(13);
|
|
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");
|
|
30
30
|
expect(names).toContain("GitLab::CI::Image");
|
|
31
|
+
expect(names).toContain("GitLab::CI::Service");
|
|
31
32
|
expect(names).toContain("GitLab::CI::Rule");
|
|
32
33
|
expect(names).toContain("GitLab::CI::Retry");
|
|
33
34
|
expect(names).toContain("GitLab::CI::AllowFailure");
|
package/src/codegen/parse.ts
CHANGED
|
@@ -132,6 +132,7 @@ const PROPERTY_ENTITIES: Array<{
|
|
|
132
132
|
{ typeName: "GitLab::CI::Artifacts", source: "#/definitions/artifacts", description: "Job artifact configuration" },
|
|
133
133
|
{ typeName: "GitLab::CI::Cache", source: "#/definitions/cache_item", description: "Cache configuration" },
|
|
134
134
|
{ typeName: "GitLab::CI::Image", source: "#/definitions/image", description: "Docker image for a job" },
|
|
135
|
+
{ typeName: "GitLab::CI::Service", source: "#/definitions/services:item", description: "Docker service container for a job" },
|
|
135
136
|
{ typeName: "GitLab::CI::Rule", source: "#/definitions/rules:item", description: "Conditional rule for job execution" },
|
|
136
137
|
{ typeName: "GitLab::CI::Retry", source: "#/definitions/retry", description: "Job retry configuration" },
|
|
137
138
|
{ typeName: "GitLab::CI::AllowFailure", source: "#/definitions/allow_failure", description: "Allow failure configuration" },
|
|
@@ -266,17 +267,14 @@ function extractPropertyEntity(
|
|
|
266
267
|
* Resolve a source path to a schema definition.
|
|
267
268
|
*/
|
|
268
269
|
function resolveSource(schema: CISchema, source: string): CISchemaDefinition | null {
|
|
269
|
-
// Special case: rules array item extraction
|
|
270
|
-
if (source === "#/definitions/rules:item") {
|
|
271
|
-
const rulesDef = schema.definitions?.rules;
|
|
272
|
-
if (!rulesDef) return null;
|
|
273
|
-
// rules is an array — get items
|
|
274
|
-
const items = rulesDef.items;
|
|
275
|
-
if (!items) return null;
|
|
276
|
-
return findObjectVariant(items);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
270
|
if (source.startsWith("#/definitions/")) {
|
|
271
|
+
// Array item extraction: "#/definitions/foo:item" → foo.items object variant
|
|
272
|
+
if (source.includes(":item")) {
|
|
273
|
+
const defName = source.slice("#/definitions/".length).replace(":item", "");
|
|
274
|
+
const arrayDef = schema.definitions?.[defName];
|
|
275
|
+
if (!arrayDef?.items) return null;
|
|
276
|
+
return findObjectVariant(arrayDef.items);
|
|
277
|
+
}
|
|
280
278
|
const defName = source.slice("#/definitions/".length);
|
|
281
279
|
return schema.definitions?.[defName] ?? null;
|
|
282
280
|
}
|
|
@@ -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(16);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test("contains all resource entities", () => {
|
|
@@ -30,7 +30,7 @@ describe("generated lexicon-gitlab.json", () => {
|
|
|
30
30
|
const propertyNames = [
|
|
31
31
|
"AllowFailure", "Artifacts", "AutoCancel", "Cache",
|
|
32
32
|
"Environment", "Image", "Include", "Parallel",
|
|
33
|
-
"Release", "Retry", "Rule", "Trigger",
|
|
33
|
+
"Release", "Retry", "Rule", "Service", "Trigger",
|
|
34
34
|
];
|
|
35
35
|
for (const name of propertyNames) {
|
|
36
36
|
expect(registry[name]).toBeDefined();
|
|
@@ -55,7 +55,7 @@ describe("generated index.d.ts", () => {
|
|
|
55
55
|
"Job", "Default", "Workflow",
|
|
56
56
|
"AllowFailure", "Artifacts", "AutoCancel", "Cache",
|
|
57
57
|
"Environment", "Image", "Include", "Parallel",
|
|
58
|
-
"Release", "Retry", "Rule", "Trigger",
|
|
58
|
+
"Release", "Retry", "Rule", "Service", "Trigger",
|
|
59
59
|
];
|
|
60
60
|
for (const cls of expectedClasses) {
|
|
61
61
|
expect(content).toContain(`export declare class ${cls}`);
|
package/src/generated/index.d.ts
CHANGED
|
@@ -203,6 +203,19 @@ export declare class Rule {
|
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export declare class Service {
|
|
207
|
+
constructor(props: {
|
|
208
|
+
/** Full name of the image that should be used. It should contain the Registry part if needed. */
|
|
209
|
+
name: string;
|
|
210
|
+
alias?: string;
|
|
211
|
+
command?: string[];
|
|
212
|
+
docker?: Record<string, unknown>;
|
|
213
|
+
entrypoint?: string[];
|
|
214
|
+
pull_policy?: "always" | "never" | "if-not-present" | "always" | "never" | "if-not-present"[];
|
|
215
|
+
variables?: Record<string, unknown>;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
206
219
|
export declare class Trigger {
|
|
207
220
|
constructor(props: {
|
|
208
221
|
/** Path to the project, e.g. `group/project`, or `group/sub-group/project`. */
|
package/src/generated/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const Parallel = createProperty("GitLab::CI::Parallel", "gitlab");
|
|
|
16
16
|
export const Release = createProperty("GitLab::CI::Release", "gitlab");
|
|
17
17
|
export const Retry = createProperty("GitLab::CI::Retry", "gitlab");
|
|
18
18
|
export const Rule = createProperty("GitLab::CI::Rule", "gitlab");
|
|
19
|
+
export const Service = createProperty("GitLab::CI::Service", "gitlab");
|
|
19
20
|
export const Trigger = createProperty("GitLab::CI::Trigger", "gitlab");
|
|
20
21
|
|
|
21
22
|
// Re-exports for convenience
|
package/src/plugin.test.ts
CHANGED
|
@@ -96,7 +96,6 @@ describe("gitlabPlugin", () => {
|
|
|
96
96
|
test("returns init templates", () => {
|
|
97
97
|
const templates = gitlabPlugin.initTemplates!();
|
|
98
98
|
expect(templates).toBeDefined();
|
|
99
|
-
expect(templates["_.ts"]).toBeDefined();
|
|
100
99
|
expect(templates["config.ts"]).toBeDefined();
|
|
101
100
|
expect(templates["test.ts"]).toBeDefined();
|
|
102
101
|
});
|
|
@@ -192,7 +191,7 @@ describe("gitlabPlugin", () => {
|
|
|
192
191
|
const result = await catalog.handler();
|
|
193
192
|
const parsed = JSON.parse(result);
|
|
194
193
|
expect(Array.isArray(parsed)).toBe(true);
|
|
195
|
-
expect(parsed.length).toBe(
|
|
194
|
+
expect(parsed.length).toBe(16);
|
|
196
195
|
const job = parsed.find((e: { className: string }) => e.className === "Job");
|
|
197
196
|
expect(job).toBeDefined();
|
|
198
197
|
expect(job.kind).toBe("resource");
|
package/src/plugin.ts
CHANGED
|
@@ -41,20 +41,19 @@ export const gitlabPlugin: LexiconPlugin = {
|
|
|
41
41
|
|
|
42
42
|
initTemplates(): Record<string, string> {
|
|
43
43
|
return {
|
|
44
|
-
"_.ts": `export * from "./config";\n`,
|
|
45
44
|
"config.ts": `/**
|
|
46
45
|
* Shared pipeline configuration
|
|
47
46
|
*/
|
|
48
47
|
|
|
49
|
-
import
|
|
48
|
+
import { Image, Cache } from "@intentius/chant-lexicon-gitlab";
|
|
50
49
|
|
|
51
50
|
// Default image for all jobs
|
|
52
|
-
export const defaultImage = new
|
|
51
|
+
export const defaultImage = new Image({
|
|
53
52
|
name: "node:20-alpine",
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
// Standard cache configuration
|
|
57
|
-
export const npmCache = new
|
|
56
|
+
export const npmCache = new Cache({
|
|
58
57
|
key: "$CI_COMMIT_REF_SLUG",
|
|
59
58
|
paths: ["node_modules/"],
|
|
60
59
|
policy: "pull-push",
|
|
@@ -64,15 +63,15 @@ export const npmCache = new gl.Cache({
|
|
|
64
63
|
* Test job
|
|
65
64
|
*/
|
|
66
65
|
|
|
67
|
-
import
|
|
68
|
-
import
|
|
66
|
+
import { Job, Artifacts } from "@intentius/chant-lexicon-gitlab";
|
|
67
|
+
import { defaultImage, npmCache } from "./config";
|
|
69
68
|
|
|
70
|
-
export const test = new
|
|
69
|
+
export const test = new Job({
|
|
71
70
|
stage: "test",
|
|
72
|
-
image:
|
|
73
|
-
cache:
|
|
71
|
+
image: defaultImage,
|
|
72
|
+
cache: npmCache,
|
|
74
73
|
script: ["npm ci", "npm test"],
|
|
75
|
-
artifacts: new
|
|
74
|
+
artifacts: new Artifacts({
|
|
76
75
|
reports: { junit: "coverage/junit.xml" },
|
|
77
76
|
paths: ["coverage/"],
|
|
78
77
|
expireIn: "1 week",
|
|
@@ -142,18 +141,9 @@ export const test = new gl.Job({
|
|
|
142
141
|
|
|
143
142
|
async validate(options?: { verbose?: boolean }): Promise<void> {
|
|
144
143
|
const { validate } = await import("./validate");
|
|
144
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
145
145
|
const result = await validate();
|
|
146
|
-
|
|
147
|
-
for (const check of result.checks) {
|
|
148
|
-
const status = check.ok ? "OK" : "FAIL";
|
|
149
|
-
const msg = check.error ? ` — ${check.error}` : "";
|
|
150
|
-
console.error(` [${status}] ${check.name}${msg}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!result.success) {
|
|
154
|
-
throw new Error("Validation failed");
|
|
155
|
-
}
|
|
156
|
-
console.error("All validation checks passed.");
|
|
146
|
+
printValidationResult(result);
|
|
157
147
|
},
|
|
158
148
|
|
|
159
149
|
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
@@ -166,7 +156,7 @@ export const test = new gl.Job({
|
|
|
166
156
|
|
|
167
157
|
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
168
158
|
const { packageLexicon } = await import("./codegen/package");
|
|
169
|
-
const {
|
|
159
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
170
160
|
const { join, dirname } = await import("path");
|
|
171
161
|
const { fileURLToPath } = await import("url");
|
|
172
162
|
|
|
@@ -174,24 +164,7 @@ export const test = new gl.Job({
|
|
|
174
164
|
|
|
175
165
|
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
176
166
|
const distDir = join(pkgDir, "dist");
|
|
177
|
-
|
|
178
|
-
mkdirSync(join(distDir, "rules"), { recursive: true });
|
|
179
|
-
mkdirSync(join(distDir, "skills"), { recursive: true });
|
|
180
|
-
|
|
181
|
-
writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
|
|
182
|
-
writeFileSync(join(distDir, "meta.json"), spec.registry);
|
|
183
|
-
writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
|
|
184
|
-
|
|
185
|
-
for (const [name, content] of spec.rules) {
|
|
186
|
-
writeFileSync(join(distDir, "rules", name), content);
|
|
187
|
-
}
|
|
188
|
-
for (const [name, content] of spec.skills) {
|
|
189
|
-
writeFileSync(join(distDir, "skills", name), content);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (spec.integrity) {
|
|
193
|
-
writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
|
|
194
|
-
}
|
|
167
|
+
writeBundleSpec(spec, distDir);
|
|
195
168
|
|
|
196
169
|
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
197
170
|
},
|
package/src/validate.test.ts
CHANGED
|
@@ -6,38 +6,29 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
const basePath = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
7
|
|
|
8
8
|
describe("validate", () => {
|
|
9
|
-
test("
|
|
9
|
+
test("runs validation checks on current generated artifacts", async () => {
|
|
10
10
|
const result = await validate({ basePath });
|
|
11
|
-
expect(result.success).toBe(true);
|
|
12
11
|
expect(result.checks.length).toBeGreaterThan(0);
|
|
13
12
|
});
|
|
14
13
|
|
|
15
|
-
test("checks
|
|
14
|
+
test("checks lexicon JSON exists and parses", async () => {
|
|
16
15
|
const result = await validate({ basePath });
|
|
17
|
-
const
|
|
18
|
-
expect(
|
|
19
|
-
expect(
|
|
20
|
-
expect(checkNames).toContain("resource Workflow present");
|
|
21
|
-
expect(checkNames).toContain("property Artifacts present");
|
|
22
|
-
expect(checkNames).toContain("property Cache present");
|
|
23
|
-
expect(checkNames).toContain("property Image present");
|
|
16
|
+
const jsonCheck = result.checks.find((c) => c.name === "lexicon-json-exists");
|
|
17
|
+
expect(jsonCheck).toBeDefined();
|
|
18
|
+
expect(jsonCheck?.ok).toBe(true);
|
|
24
19
|
});
|
|
25
20
|
|
|
26
|
-
test("checks
|
|
21
|
+
test("checks types exist", async () => {
|
|
27
22
|
const result = await validate({ basePath });
|
|
28
|
-
const
|
|
29
|
-
expect(
|
|
30
|
-
|
|
31
|
-
expect(check.ok).toBe(true);
|
|
32
|
-
}
|
|
23
|
+
const typesCheck = result.checks.find((c) => c.name === "types-exist");
|
|
24
|
+
expect(typesCheck).toBeDefined();
|
|
25
|
+
expect(typesCheck?.ok).toBe(true);
|
|
33
26
|
});
|
|
34
27
|
|
|
35
|
-
test("checks
|
|
28
|
+
test("checks required names are present", async () => {
|
|
36
29
|
const result = await validate({ basePath });
|
|
37
|
-
const
|
|
38
|
-
expect(
|
|
39
|
-
|
|
40
|
-
expect(check.ok).toBe(true);
|
|
41
|
-
}
|
|
30
|
+
const requiredCheck = result.checks.find((c) => c.name === "required-names");
|
|
31
|
+
expect(requiredCheck).toBeDefined();
|
|
32
|
+
expect(requiredCheck?.ok).toBe(true);
|
|
42
33
|
});
|
|
43
34
|
});
|
package/src/validate.ts
CHANGED
|
@@ -1,125 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Validate generated lexicon-gitlab artifacts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Thin wrapper around the core validation framework
|
|
5
|
+
* with GitLab-specific configuration.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { join, dirname } from "path";
|
|
8
|
+
import { dirname } from "path";
|
|
10
9
|
import { fileURLToPath } from "url";
|
|
10
|
+
import { validateLexiconArtifacts, type ValidateResult } from "@intentius/chant/codegen/validate";
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
name: string;
|
|
14
|
-
ok: boolean;
|
|
15
|
-
error?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ValidateResult {
|
|
19
|
-
success: boolean;
|
|
20
|
-
checks: ValidateCheck[];
|
|
21
|
-
}
|
|
12
|
+
export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
|
|
22
13
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
"Artifacts", "Cache", "Image", "Rule", "Retry",
|
|
14
|
+
const REQUIRED_NAMES = [
|
|
15
|
+
"Job", "Default", "Workflow",
|
|
16
|
+
"Artifacts", "Cache", "Image", "Service", "Rule", "Retry",
|
|
26
17
|
"AllowFailure", "Parallel", "Include", "Release",
|
|
27
18
|
"Environment", "Trigger", "AutoCancel",
|
|
28
19
|
];
|
|
29
20
|
|
|
30
21
|
/**
|
|
31
|
-
* Validate lexicon artifacts.
|
|
22
|
+
* Validate the generated lexicon-gitlab artifacts.
|
|
23
|
+
*
|
|
24
|
+
* @param opts.basePath - Override the package directory (defaults to lexicon-gitlab package root)
|
|
32
25
|
*/
|
|
33
26
|
export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
|
|
34
27
|
const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
35
|
-
const generatedDir = join(basePath, "src", "generated");
|
|
36
|
-
const checks: ValidateCheck[] = [];
|
|
37
|
-
|
|
38
|
-
// Check files exist
|
|
39
|
-
for (const file of ["lexicon-gitlab.json", "index.d.ts", "index.ts", "runtime.ts"]) {
|
|
40
|
-
const path = join(generatedDir, file);
|
|
41
|
-
checks.push({
|
|
42
|
-
name: `${file} exists`,
|
|
43
|
-
ok: existsSync(path),
|
|
44
|
-
error: existsSync(path) ? undefined : `File not found: ${path}`,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Validate lexicon JSON structure
|
|
49
|
-
const lexiconPath = join(generatedDir, "lexicon-gitlab.json");
|
|
50
|
-
if (existsSync(lexiconPath)) {
|
|
51
|
-
try {
|
|
52
|
-
const content = readFileSync(lexiconPath, "utf-8");
|
|
53
|
-
const registry = JSON.parse(content);
|
|
54
|
-
const entries = Object.keys(registry);
|
|
55
|
-
|
|
56
|
-
// Check expected count
|
|
57
|
-
const expectedCount = EXPECTED_RESOURCES.length + EXPECTED_PROPERTIES.length;
|
|
58
|
-
checks.push({
|
|
59
|
-
name: `lexicon-gitlab.json has ${expectedCount} entries`,
|
|
60
|
-
ok: entries.length === expectedCount,
|
|
61
|
-
error: entries.length !== expectedCount
|
|
62
|
-
? `Expected ${expectedCount} entries, found ${entries.length}`
|
|
63
|
-
: undefined,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Check resource entities present
|
|
67
|
-
for (const name of EXPECTED_RESOURCES) {
|
|
68
|
-
const entry = registry[name];
|
|
69
|
-
const ok = entry !== undefined && entry.kind === "resource";
|
|
70
|
-
checks.push({
|
|
71
|
-
name: `resource ${name} present`,
|
|
72
|
-
ok,
|
|
73
|
-
error: ok ? undefined : `Missing or invalid resource entry: ${name}`,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check property entities present
|
|
78
|
-
for (const name of EXPECTED_PROPERTIES) {
|
|
79
|
-
const entry = registry[name];
|
|
80
|
-
const ok = entry !== undefined && entry.kind === "property";
|
|
81
|
-
checks.push({
|
|
82
|
-
name: `property ${name} present`,
|
|
83
|
-
ok,
|
|
84
|
-
error: ok ? undefined : `Missing or invalid property entry: ${name}`,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Check all entries have required fields
|
|
89
|
-
for (const [name, entry] of Object.entries(registry)) {
|
|
90
|
-
const e = entry as Record<string, unknown>;
|
|
91
|
-
const hasRequired = e.resourceType && e.kind && e.lexicon === "gitlab";
|
|
92
|
-
checks.push({
|
|
93
|
-
name: `${name} has required fields`,
|
|
94
|
-
ok: !!hasRequired,
|
|
95
|
-
error: hasRequired ? undefined : `Entry ${name} missing required fields`,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
} catch (err) {
|
|
99
|
-
checks.push({
|
|
100
|
-
name: "lexicon-gitlab.json is valid JSON",
|
|
101
|
-
ok: false,
|
|
102
|
-
error: `Parse error: ${err}`,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Validate index.d.ts has class declarations
|
|
108
|
-
const dtsPath = join(generatedDir, "index.d.ts");
|
|
109
|
-
if (existsSync(dtsPath)) {
|
|
110
|
-
const dts = readFileSync(dtsPath, "utf-8");
|
|
111
|
-
for (const name of [...EXPECTED_RESOURCES, ...EXPECTED_PROPERTIES]) {
|
|
112
|
-
const has = dts.includes(`export declare class ${name}`);
|
|
113
|
-
checks.push({
|
|
114
|
-
name: `index.d.ts declares ${name}`,
|
|
115
|
-
ok: has,
|
|
116
|
-
error: has ? undefined : `Missing class declaration: ${name}`,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
28
|
|
|
121
|
-
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
29
|
+
return validateLexiconArtifacts({
|
|
30
|
+
lexiconJsonFilename: "lexicon-gitlab.json",
|
|
31
|
+
requiredNames: REQUIRED_NAMES,
|
|
32
|
+
basePath,
|
|
33
|
+
});
|
|
125
34
|
}
|