@intentius/chant-lexicon-gitlab 0.0.6 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/integrity.json +10 -6
- package/dist/manifest.json +1 -1
- package/dist/meta.json +186 -8
- package/dist/rules/wgl012.ts +86 -0
- package/dist/rules/wgl013.ts +62 -0
- package/dist/rules/wgl014.ts +51 -0
- package/dist/rules/wgl015.ts +85 -0
- package/dist/rules/yaml-helpers.ts +65 -3
- package/dist/skills/chant-gitlab.md +502 -0
- package/dist/types/index.d.ts +55 -16
- package/package.json +2 -2
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +58 -0
- package/src/codegen/docs.ts +88 -11
- package/src/codegen/generate-lexicon.ts +6 -1
- package/src/codegen/generate.ts +45 -50
- package/src/codegen/naming.ts +3 -0
- package/src/codegen/package.ts +2 -0
- package/src/codegen/parse.test.ts +154 -4
- package/src/codegen/parse.ts +161 -49
- package/src/codegen/snapshot.test.ts +7 -5
- package/src/composites/composites.test.ts +452 -0
- package/src/composites/docker-build.ts +81 -0
- package/src/composites/index.ts +8 -0
- package/src/composites/node-pipeline.ts +104 -0
- package/src/composites/python-pipeline.ts +75 -0
- package/src/composites/review-app.ts +63 -0
- package/src/generated/index.d.ts +55 -16
- package/src/generated/index.ts +3 -0
- package/src/generated/lexicon-gitlab.json +186 -8
- package/src/import/generator.ts +3 -2
- package/src/import/parser.test.ts +3 -3
- package/src/import/parser.ts +12 -26
- package/src/index.ts +4 -0
- package/src/lint/post-synth/wgl012.test.ts +131 -0
- package/src/lint/post-synth/wgl012.ts +86 -0
- package/src/lint/post-synth/wgl013.test.ts +164 -0
- package/src/lint/post-synth/wgl013.ts +62 -0
- package/src/lint/post-synth/wgl014.test.ts +97 -0
- package/src/lint/post-synth/wgl014.ts +51 -0
- package/src/lint/post-synth/wgl015.test.ts +139 -0
- package/src/lint/post-synth/wgl015.ts +85 -0
- package/src/lint/post-synth/yaml-helpers.ts +65 -3
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +2 -0
- package/src/plugin.test.ts +44 -19
- package/src/plugin.ts +671 -76
- package/src/serializer.test.ts +146 -6
- package/src/serializer.ts +64 -14
- package/src/validate.ts +1 -0
- package/src/variables.ts +4 -0
- package/dist/skills/gitlab-ci.md +0 -37
- package/src/codegen/rollback.ts +0 -26
package/src/plugin.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* for GitLab CI/CD pipelines.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
import type { LexiconPlugin, IntrinsicDef, SkillDefinition, InitTemplateSet } from "@intentius/chant/lexicon";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
9
11
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
10
12
|
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
11
13
|
import { gitlabSerializer } from "./serializer";
|
|
@@ -25,7 +27,11 @@ export const gitlabPlugin: LexiconPlugin = {
|
|
|
25
27
|
postSynthChecks(): PostSynthCheck[] {
|
|
26
28
|
const { wgl010 } = require("./lint/post-synth/wgl010");
|
|
27
29
|
const { wgl011 } = require("./lint/post-synth/wgl011");
|
|
28
|
-
|
|
30
|
+
const { wgl012 } = require("./lint/post-synth/wgl012");
|
|
31
|
+
const { wgl013 } = require("./lint/post-synth/wgl013");
|
|
32
|
+
const { wgl014 } = require("./lint/post-synth/wgl014");
|
|
33
|
+
const { wgl015 } = require("./lint/post-synth/wgl015");
|
|
34
|
+
return [wgl010, wgl011, wgl012, wgl013, wgl014, wgl015];
|
|
29
35
|
},
|
|
30
36
|
|
|
31
37
|
intrinsics(): IntrinsicDef[] {
|
|
@@ -39,9 +45,81 @@ export const gitlabPlugin: LexiconPlugin = {
|
|
|
39
45
|
];
|
|
40
46
|
},
|
|
41
47
|
|
|
42
|
-
initTemplates():
|
|
48
|
+
initTemplates(template?: string): InitTemplateSet {
|
|
49
|
+
if (template === "node-pipeline") {
|
|
50
|
+
return {
|
|
51
|
+
src: {
|
|
52
|
+
"pipeline.ts": `import { NodePipeline } from "@intentius/chant-lexicon-gitlab";
|
|
53
|
+
|
|
54
|
+
export const app = NodePipeline({
|
|
55
|
+
nodeVersion: "22",
|
|
56
|
+
installCommand: "npm install",
|
|
57
|
+
buildScript: "build",
|
|
58
|
+
testScript: "test",
|
|
59
|
+
});
|
|
60
|
+
`,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (template === "python-pipeline") {
|
|
66
|
+
return {
|
|
67
|
+
src: {
|
|
68
|
+
"pipeline.ts": `import { PythonPipeline } from "@intentius/chant-lexicon-gitlab";
|
|
69
|
+
|
|
70
|
+
export const app = PythonPipeline({
|
|
71
|
+
pythonVersion: "3.12",
|
|
72
|
+
lintCommand: null,
|
|
73
|
+
});
|
|
74
|
+
`,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (template === "docker-build") {
|
|
80
|
+
return {
|
|
81
|
+
src: {
|
|
82
|
+
"pipeline.ts": `import { DockerBuild, Job, Image } from "@intentius/chant-lexicon-gitlab";
|
|
83
|
+
|
|
84
|
+
export const docker = DockerBuild({
|
|
85
|
+
dockerfile: "Dockerfile",
|
|
86
|
+
tagLatest: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const test = new Job({
|
|
90
|
+
stage: "test",
|
|
91
|
+
image: new Image({ name: "node:22-alpine" }),
|
|
92
|
+
script: ["node test.js"],
|
|
93
|
+
});
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (template === "review-app") {
|
|
100
|
+
return {
|
|
101
|
+
src: {
|
|
102
|
+
"pipeline.ts": `import { ReviewApp, Job, Image } from "@intentius/chant-lexicon-gitlab";
|
|
103
|
+
|
|
104
|
+
export const review = ReviewApp({
|
|
105
|
+
name: "review",
|
|
106
|
+
deployScript: "echo deploy",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const test = new Job({
|
|
110
|
+
stage: "test",
|
|
111
|
+
image: new Image({ name: "node:22-alpine" }),
|
|
112
|
+
script: ["node test.js"],
|
|
113
|
+
});
|
|
114
|
+
`,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Default template — basic pipeline with shared config
|
|
43
120
|
return {
|
|
44
|
-
|
|
121
|
+
src: {
|
|
122
|
+
"config.ts": `/**
|
|
45
123
|
* Shared pipeline configuration
|
|
46
124
|
*/
|
|
47
125
|
|
|
@@ -59,25 +137,33 @@ export const npmCache = new Cache({
|
|
|
59
137
|
policy: "pull-push",
|
|
60
138
|
});
|
|
61
139
|
`,
|
|
62
|
-
|
|
63
|
-
* Test job
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
import { Job, Artifacts } from "@intentius/chant-lexicon-gitlab";
|
|
140
|
+
"pipeline.ts": `import { Job, Artifacts } from "@intentius/chant-lexicon-gitlab";
|
|
67
141
|
import { defaultImage, npmCache } from "./config";
|
|
68
142
|
|
|
143
|
+
export const junitReports = { junit: "coverage/junit.xml" };
|
|
144
|
+
|
|
145
|
+
export const testArtifacts = new Artifacts({
|
|
146
|
+
reports: junitReports,
|
|
147
|
+
paths: ["coverage/"],
|
|
148
|
+
expire_in: "1 week",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const build = new Job({
|
|
152
|
+
stage: "build",
|
|
153
|
+
image: defaultImage,
|
|
154
|
+
cache: npmCache,
|
|
155
|
+
script: ["npm install", "npm run build"],
|
|
156
|
+
});
|
|
157
|
+
|
|
69
158
|
export const test = new Job({
|
|
70
159
|
stage: "test",
|
|
71
160
|
image: defaultImage,
|
|
72
161
|
cache: npmCache,
|
|
73
|
-
script: ["npm
|
|
74
|
-
artifacts:
|
|
75
|
-
reports: { junit: "coverage/junit.xml" },
|
|
76
|
-
paths: ["coverage/"],
|
|
77
|
-
expireIn: "1 week",
|
|
78
|
-
}),
|
|
162
|
+
script: ["npm install", "npm test"],
|
|
163
|
+
artifacts: testArtifacts,
|
|
79
164
|
});
|
|
80
165
|
`,
|
|
166
|
+
},
|
|
81
167
|
};
|
|
82
168
|
},
|
|
83
169
|
|
|
@@ -169,31 +255,6 @@ export const test = new Job({
|
|
|
169
255
|
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
170
256
|
},
|
|
171
257
|
|
|
172
|
-
async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
|
|
173
|
-
const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
|
|
174
|
-
const { join, dirname } = await import("path");
|
|
175
|
-
const { fileURLToPath } = await import("url");
|
|
176
|
-
|
|
177
|
-
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
178
|
-
const snapshotsDir = join(pkgDir, ".snapshots");
|
|
179
|
-
|
|
180
|
-
if (options?.restore) {
|
|
181
|
-
const generatedDir = join(pkgDir, "src", "generated");
|
|
182
|
-
restoreSnapshot(String(options.restore), generatedDir);
|
|
183
|
-
console.error(`Restored snapshot: ${options.restore}`);
|
|
184
|
-
} else {
|
|
185
|
-
const snapshots = listSnapshots(snapshotsDir);
|
|
186
|
-
if (snapshots.length === 0) {
|
|
187
|
-
console.error("No snapshots available.");
|
|
188
|
-
} else {
|
|
189
|
-
console.error(`Available snapshots (${snapshots.length}):`);
|
|
190
|
-
for (const s of snapshots) {
|
|
191
|
-
console.error(` ${s.timestamp} ${s.resourceCount} resources ${s.path}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
|
|
197
258
|
mcpTools() {
|
|
198
259
|
return [
|
|
199
260
|
{
|
|
@@ -283,49 +344,526 @@ export const deploy = new Job({
|
|
|
283
344
|
skills(): SkillDefinition[] {
|
|
284
345
|
return [
|
|
285
346
|
{
|
|
286
|
-
name: "gitlab
|
|
287
|
-
description: "GitLab CI/CD
|
|
347
|
+
name: "chant-gitlab",
|
|
348
|
+
description: "GitLab CI/CD pipeline lifecycle — build, validate, deploy, monitor, rollback, and troubleshoot",
|
|
288
349
|
content: `---
|
|
289
|
-
|
|
290
|
-
description: GitLab CI
|
|
350
|
+
skill: chant-gitlab
|
|
351
|
+
description: Build, validate, and deploy GitLab CI pipelines from a chant project
|
|
352
|
+
user-invocable: true
|
|
291
353
|
---
|
|
292
354
|
|
|
293
|
-
# GitLab CI/CD
|
|
294
|
-
|
|
295
|
-
##
|
|
296
|
-
|
|
297
|
-
-
|
|
298
|
-
|
|
299
|
-
-
|
|
300
|
-
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
355
|
+
# GitLab CI/CD Operational Playbook
|
|
356
|
+
|
|
357
|
+
## How chant and GitLab CI relate
|
|
358
|
+
|
|
359
|
+
chant is a **synthesis-only** tool — it compiles TypeScript source files into \`.gitlab-ci.yml\` (YAML). chant does NOT call GitLab APIs. Your job as an agent is to bridge the two:
|
|
360
|
+
|
|
361
|
+
- Use **chant** for: build, lint, diff (local YAML comparison)
|
|
362
|
+
- Use **git + GitLab API** for: push, merge requests, pipeline monitoring, job logs, rollback, and all deployment operations
|
|
363
|
+
|
|
364
|
+
The source of truth for pipeline configuration is the TypeScript in \`src/\`. The generated \`.gitlab-ci.yml\` is an intermediate artifact — never edit it by hand.
|
|
365
|
+
|
|
366
|
+
## Scaffolding a new project
|
|
367
|
+
|
|
368
|
+
### Initialize with a template
|
|
369
|
+
|
|
370
|
+
\`\`\`bash
|
|
371
|
+
chant init --lexicon gitlab # default: config.ts + pipeline.ts
|
|
372
|
+
chant init --lexicon gitlab --template node-pipeline # NodePipeline composite
|
|
373
|
+
chant init --lexicon gitlab --template python-pipeline # PythonPipeline composite
|
|
374
|
+
chant init --lexicon gitlab --template docker-build # DockerBuild composite
|
|
375
|
+
chant init --lexicon gitlab --template review-app # ReviewApp composite
|
|
376
|
+
\`\`\`
|
|
377
|
+
|
|
378
|
+
This creates \`src/\` with chant pipeline definitions. It does NOT create application files — you bring your own app code.
|
|
379
|
+
|
|
380
|
+
### Available templates
|
|
381
|
+
|
|
382
|
+
| Template | What it generates | Best for |
|
|
383
|
+
|----------|-------------------|----------|
|
|
384
|
+
| *(default)* | \`config.ts\` + \`pipeline.ts\` with build/test jobs | Custom pipelines from scratch |
|
|
385
|
+
| \`node-pipeline\` | \`NodePipeline\` composite with npm install/build/test | Node.js apps |
|
|
386
|
+
| \`python-pipeline\` | \`PythonPipeline\` composite with venv/pytest | Python apps |
|
|
387
|
+
| \`docker-build\` | \`DockerBuild\` composite + test job | Containerized apps |
|
|
388
|
+
| \`review-app\` | \`ReviewApp\` composite + test job | Apps needing per-MR environments |
|
|
389
|
+
|
|
390
|
+
## Deploying to GitLab
|
|
391
|
+
|
|
392
|
+
### What goes in the GitLab repo
|
|
393
|
+
|
|
394
|
+
The GitLab repo needs TWO things:
|
|
395
|
+
1. **\`.gitlab-ci.yml\`** — generated by \`chant build\`
|
|
396
|
+
2. **Your application files** — whatever the pipeline scripts reference
|
|
397
|
+
|
|
398
|
+
chant only generates the YAML. Application files (\`package.json\`, \`Dockerfile\`, \`requirements.txt\`, source code, tests) are your responsibility.
|
|
399
|
+
|
|
400
|
+
### Typical project structure in the GitLab repo
|
|
401
|
+
|
|
402
|
+
**Node.js project:**
|
|
403
|
+
\`\`\`
|
|
404
|
+
.gitlab-ci.yml # generated by chant
|
|
405
|
+
package.json # your app's package.json with build/test scripts
|
|
406
|
+
index.js # your app code
|
|
407
|
+
test.js # your tests
|
|
408
|
+
\`\`\`
|
|
409
|
+
|
|
410
|
+
**Python project:**
|
|
411
|
+
\`\`\`
|
|
412
|
+
.gitlab-ci.yml
|
|
413
|
+
requirements.txt # must include pytest, pytest-cov if using PythonPipeline defaults
|
|
414
|
+
app.py
|
|
415
|
+
test_app.py
|
|
416
|
+
\`\`\`
|
|
417
|
+
|
|
418
|
+
**Docker project:**
|
|
419
|
+
\`\`\`
|
|
420
|
+
.gitlab-ci.yml
|
|
421
|
+
Dockerfile
|
|
422
|
+
src/ # your app source
|
|
423
|
+
\`\`\`
|
|
424
|
+
|
|
425
|
+
### Important: npm ci vs npm install
|
|
426
|
+
|
|
427
|
+
The \`NodePipeline\` composite defaults to \`npm ci\`, which requires a \`package-lock.json\` in the repo. If your repo does not have a lockfile, override with:
|
|
428
|
+
|
|
429
|
+
\`\`\`typescript
|
|
430
|
+
NodePipeline({
|
|
431
|
+
installCommand: "npm install", // use instead of npm ci
|
|
432
|
+
...
|
|
433
|
+
})
|
|
434
|
+
\`\`\`
|
|
435
|
+
|
|
436
|
+
Or generate a lockfile: \`npm install && git add package-lock.json\`.
|
|
437
|
+
|
|
438
|
+
### Step-by-step: push to GitLab
|
|
439
|
+
|
|
440
|
+
\`\`\`bash
|
|
441
|
+
# 1. Build the YAML from chant source
|
|
442
|
+
chant build src/ --output .gitlab-ci.yml
|
|
443
|
+
|
|
444
|
+
# 2. Initialize git (if needed) and commit everything
|
|
445
|
+
git init -b main
|
|
446
|
+
git add .gitlab-ci.yml package.json index.js test.js # add your app files
|
|
447
|
+
git commit -m "Initial pipeline"
|
|
448
|
+
|
|
449
|
+
# 3. Push to GitLab
|
|
450
|
+
git remote add origin git@gitlab.com:YOUR_GROUP/YOUR_PROJECT.git
|
|
451
|
+
git push -u origin main
|
|
452
|
+
\`\`\`
|
|
453
|
+
|
|
454
|
+
The pipeline triggers automatically on push. Do NOT commit the chant \`src/\` directory, \`node_modules/\`, or \`.chant/\` to the GitLab repo — those are local development files.
|
|
455
|
+
|
|
456
|
+
## Build and validate
|
|
457
|
+
|
|
458
|
+
### Build the pipeline
|
|
459
|
+
|
|
460
|
+
\`\`\`bash
|
|
461
|
+
chant build src/ --output .gitlab-ci.yml
|
|
462
|
+
\`\`\`
|
|
463
|
+
|
|
464
|
+
Options:
|
|
465
|
+
- \`--format yaml\` — emit YAML (default for GitLab)
|
|
466
|
+
- \`--watch\` — rebuild on source changes
|
|
467
|
+
- \`--output <path>\` — write to a specific file
|
|
468
|
+
|
|
469
|
+
### Lint the source
|
|
470
|
+
|
|
471
|
+
\`\`\`bash
|
|
472
|
+
chant lint src/
|
|
473
|
+
\`\`\`
|
|
474
|
+
|
|
475
|
+
Options:
|
|
476
|
+
- \`--fix\` — auto-fix violations where possible
|
|
477
|
+
- \`--format sarif\` — SARIF output for CI integration
|
|
478
|
+
- \`--watch\` — re-lint on changes
|
|
479
|
+
|
|
480
|
+
### Validate with GitLab CI Lint API
|
|
481
|
+
|
|
482
|
+
\`\`\`bash
|
|
483
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
484
|
+
--header "Content-Type: application/json" \\
|
|
485
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \\
|
|
486
|
+
--data-binary @- <<EOF
|
|
487
|
+
{"content": $(cat .gitlab-ci.yml | jq -Rs .)}
|
|
488
|
+
EOF
|
|
489
|
+
\`\`\`
|
|
490
|
+
|
|
491
|
+
Add \`"dry_run": true, "include_merged_yaml": true\` for full expansion with includes resolved.
|
|
492
|
+
|
|
493
|
+
### What each step catches
|
|
494
|
+
|
|
495
|
+
| Step | Catches | When to run |
|
|
496
|
+
|------|---------|-------------|
|
|
497
|
+
| \`chant lint\` | Deprecated only/except (WGL001), missing script (WGL002), missing stage (WGL003), artifacts without expiry (WGL004) | Every edit |
|
|
498
|
+
| \`chant build\` | Post-synth checks: undefined stages (WGL010), unreachable jobs (WGL011), deprecated properties (WGL012), invalid needs targets (WGL013), invalid extends targets (WGL014), circular needs chains (WGL015) | Before push |
|
|
499
|
+
| CI Lint API | GitLab-specific validation: include resolution, variable expansion, YAML schema errors | Before merge to default branch |
|
|
500
|
+
|
|
501
|
+
Always run all three before deploying. Lint catches things the API cannot (and vice versa).
|
|
502
|
+
|
|
503
|
+
## Diffing and change preview
|
|
504
|
+
|
|
505
|
+
### Local diff
|
|
506
|
+
|
|
507
|
+
Compare generated \`.gitlab-ci.yml\` against the version on the remote branch:
|
|
508
|
+
|
|
509
|
+
\`\`\`bash
|
|
510
|
+
# Build the proposed config
|
|
511
|
+
chant build src/ --output .gitlab-ci.yml
|
|
512
|
+
|
|
513
|
+
# Diff against the remote version
|
|
514
|
+
git diff origin/main -- .gitlab-ci.yml
|
|
515
|
+
\`\`\`
|
|
516
|
+
|
|
517
|
+
### MR pipeline preview
|
|
518
|
+
|
|
519
|
+
Push to a branch and open a merge request — GitLab shows the pipeline that would run without executing it. This is the safest way to preview pipeline changes for production.
|
|
520
|
+
|
|
521
|
+
### CI Lint API with dry run
|
|
522
|
+
|
|
523
|
+
\`\`\`bash
|
|
524
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
525
|
+
--header "Content-Type: application/json" \\
|
|
526
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \\
|
|
527
|
+
--data-binary @- <<EOF
|
|
528
|
+
{"content": $(cat .gitlab-ci.yml | jq -Rs .), "dry_run": true, "include_merged_yaml": true}
|
|
529
|
+
EOF
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
This resolves all \`include:\` directives and expands the full pipeline — useful for catching issues with cross-file references.
|
|
533
|
+
|
|
534
|
+
### Safe preview checklist
|
|
535
|
+
|
|
536
|
+
Before merging pipeline changes, verify:
|
|
537
|
+
1. All jobs have valid \`stage:\` values and stages are defined
|
|
538
|
+
2. \`needs:\` references point to existing job names
|
|
539
|
+
3. \`rules:\` conditions match the intended branches/events
|
|
540
|
+
4. Environment names and \`on_stop\` references are correct
|
|
541
|
+
5. Docker images are accessible from your runners
|
|
542
|
+
6. Secrets/variables referenced in scripts exist in project settings
|
|
543
|
+
|
|
544
|
+
## Deploying pipeline changes
|
|
545
|
+
|
|
546
|
+
### Safe path (production pipelines)
|
|
547
|
+
|
|
548
|
+
1. Build: \`chant build src/ --output .gitlab-ci.yml\`
|
|
549
|
+
2. Lint: \`chant lint src/\`
|
|
550
|
+
3. Validate: CI Lint API (see above)
|
|
551
|
+
4. Push to feature branch: \`git push -u origin feature/pipeline-update\`
|
|
552
|
+
5. Open MR — review pipeline diff in the MR widget
|
|
553
|
+
6. Merge — pipeline runs on the default branch
|
|
554
|
+
|
|
555
|
+
### Fast path (dev/iteration)
|
|
556
|
+
|
|
557
|
+
\`\`\`bash
|
|
558
|
+
chant build src/ --output .gitlab-ci.yml
|
|
559
|
+
git add .gitlab-ci.yml && git commit -m "Update pipeline" && git push
|
|
560
|
+
\`\`\`
|
|
561
|
+
|
|
562
|
+
### Which path to use
|
|
563
|
+
|
|
564
|
+
| Scenario | Path |
|
|
565
|
+
|----------|------|
|
|
566
|
+
| Production pipeline with deploy jobs | Safe path (MR review) |
|
|
567
|
+
| Pipeline with environment/secrets changes | Safe path (MR review) |
|
|
568
|
+
| Dev/test pipeline iteration | Fast path (direct push) |
|
|
569
|
+
| CI/CD with approval gates or protected environments | Safe path + protected branch |
|
|
570
|
+
|
|
571
|
+
## Environment lifecycle
|
|
572
|
+
|
|
573
|
+
Environments are created by jobs with an \`environment:\` keyword. They track deployments and enable rollback.
|
|
574
|
+
|
|
575
|
+
### Review apps pattern
|
|
576
|
+
|
|
577
|
+
Deploy on MR, auto-stop when MR is merged or closed:
|
|
578
|
+
|
|
579
|
+
\`\`\`typescript
|
|
580
|
+
new Job({
|
|
581
|
+
stage: "deploy",
|
|
582
|
+
environment: new Environment({
|
|
583
|
+
name: "review/$CI_COMMIT_REF_SLUG",
|
|
584
|
+
url: "https://$CI_COMMIT_REF_SLUG.example.com",
|
|
585
|
+
onStop: "stop_review",
|
|
586
|
+
autoStopIn: "1 week",
|
|
587
|
+
}),
|
|
588
|
+
script: ["./deploy-review.sh"],
|
|
589
|
+
rules: [{ if: "$CI_MERGE_REQUEST_IID" }],
|
|
590
|
+
});
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
### Environment promotion
|
|
594
|
+
|
|
595
|
+
Deploy through environments in order: dev → staging → production. Use \`rules:\` and \`when: manual\` to gate promotions.
|
|
596
|
+
|
|
597
|
+
### Rollback to a previous deployment
|
|
598
|
+
|
|
599
|
+
\`\`\`bash
|
|
600
|
+
# List deployments for an environment
|
|
601
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
602
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/environments/$ENV_ID/deployments?order_by=created_at&sort=desc&per_page=5"
|
|
603
|
+
|
|
604
|
+
# Re-deploy a previous deployment's commit
|
|
605
|
+
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
606
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/deployments" \\
|
|
607
|
+
--data "environment=production&sha=$PREVIOUS_SHA&ref=main&tag=false&status=created"
|
|
608
|
+
\`\`\`
|
|
609
|
+
|
|
610
|
+
Alternatively, revert the MR that introduced the change and let the pipeline re-run.
|
|
611
|
+
|
|
612
|
+
## Pipeline and job states
|
|
613
|
+
|
|
614
|
+
### Pipeline states
|
|
615
|
+
|
|
616
|
+
| State | Meaning | Action |
|
|
617
|
+
|-------|---------|--------|
|
|
618
|
+
| \`created\` | Pipeline created, not yet started | Wait |
|
|
619
|
+
| \`waiting_for_resource\` | Waiting for runner | Check runner availability |
|
|
620
|
+
| \`preparing\` | Job is being prepared | Wait |
|
|
621
|
+
| \`pending\` | Waiting for runner to pick up | Check runner tags/availability |
|
|
622
|
+
| \`running\` | Pipeline is executing | Monitor |
|
|
623
|
+
| \`success\` | All jobs passed | None — healthy |
|
|
624
|
+
| \`failed\` | One or more jobs failed | Check failed job logs |
|
|
625
|
+
| \`canceled\` | Pipeline was canceled | Re-run if needed |
|
|
626
|
+
| \`skipped\` | Pipeline was skipped by rules | Check rules configuration |
|
|
627
|
+
| \`manual\` | Pipeline waiting for manual action | Trigger manual job or cancel |
|
|
628
|
+
| \`scheduled\` | Waiting for scheduled time | Wait |
|
|
629
|
+
|
|
630
|
+
### Job states
|
|
631
|
+
|
|
632
|
+
| State | Meaning | Action |
|
|
633
|
+
|-------|---------|--------|
|
|
634
|
+
| \`created\` | Job created | Wait |
|
|
635
|
+
| \`pending\` | Waiting for runner | Check runner tags |
|
|
636
|
+
| \`running\` | Job executing | Monitor logs |
|
|
637
|
+
| \`success\` | Job passed | None |
|
|
638
|
+
| \`failed\` | Job failed | Read trace log |
|
|
639
|
+
| \`canceled\` | Job canceled | Re-run if needed |
|
|
640
|
+
| \`skipped\` | Job skipped by rules/needs | Check rules |
|
|
641
|
+
| \`manual\` | Waiting for manual trigger | Play or skip |
|
|
642
|
+
| \`allowed_failure\` | Failed but allowed | Review — may indicate flaky test |
|
|
643
|
+
|
|
644
|
+
## Monitoring pipelines
|
|
645
|
+
|
|
646
|
+
### Check pipeline status
|
|
647
|
+
|
|
648
|
+
\`\`\`bash
|
|
649
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
650
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID"
|
|
651
|
+
\`\`\`
|
|
652
|
+
|
|
653
|
+
### List recent pipelines for a branch
|
|
654
|
+
|
|
655
|
+
\`\`\`bash
|
|
656
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
657
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines?ref=main&per_page=5"
|
|
658
|
+
\`\`\`
|
|
659
|
+
|
|
660
|
+
### Get jobs in a pipeline
|
|
661
|
+
|
|
662
|
+
\`\`\`bash
|
|
663
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
664
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs"
|
|
665
|
+
\`\`\`
|
|
666
|
+
|
|
667
|
+
### Stream job logs
|
|
668
|
+
|
|
669
|
+
\`\`\`bash
|
|
670
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
671
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/jobs/$JOB_ID/trace"
|
|
672
|
+
\`\`\`
|
|
673
|
+
|
|
674
|
+
### Download job artifacts
|
|
675
|
+
|
|
676
|
+
\`\`\`bash
|
|
677
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
678
|
+
--output artifacts.zip \\
|
|
679
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/jobs/$JOB_ID/artifacts"
|
|
680
|
+
\`\`\`
|
|
681
|
+
|
|
682
|
+
## Merge request pipeline workflow
|
|
683
|
+
|
|
684
|
+
### How MR pipelines differ
|
|
685
|
+
|
|
686
|
+
MR pipelines run in a merge request context with \`CI_MERGE_REQUEST_IID\` available. Branch pipelines run on push with \`CI_COMMIT_BRANCH\`. A job cannot have both — use \`rules:\` to target one or the other.
|
|
687
|
+
|
|
688
|
+
### Common rules patterns
|
|
689
|
+
|
|
690
|
+
\`\`\`yaml
|
|
691
|
+
# Run only on MR pipelines
|
|
692
|
+
rules:
|
|
693
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
694
|
+
|
|
695
|
+
# Run only on the default branch
|
|
696
|
+
rules:
|
|
697
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
698
|
+
|
|
699
|
+
# Run on MRs and the default branch (but not both at once)
|
|
700
|
+
rules:
|
|
701
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
702
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
703
|
+
\`\`\`
|
|
704
|
+
|
|
705
|
+
### Merged results pipelines
|
|
706
|
+
|
|
707
|
+
Enable in project settings → CI/CD → General pipelines → "Merged results pipelines". These test the result of merging your branch into the target — catching integration issues before merge.
|
|
708
|
+
|
|
709
|
+
### Merge trains
|
|
710
|
+
|
|
711
|
+
Merge trains queue MRs and test each one merged on top of the previous. Enable in project settings → Merge requests → "Merge trains". Requires merged results pipelines.
|
|
712
|
+
|
|
713
|
+
## Troubleshooting decision tree
|
|
714
|
+
|
|
715
|
+
### Step 1: Check pipeline status
|
|
716
|
+
|
|
717
|
+
\`\`\`bash
|
|
718
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
719
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID" | jq '.status'
|
|
720
|
+
\`\`\`
|
|
721
|
+
|
|
722
|
+
### Step 2: Branch on status
|
|
723
|
+
|
|
724
|
+
- **\`running\` / \`pending\` / \`created\`** → Wait. Do not take action while the pipeline is in progress.
|
|
725
|
+
- **\`failed\`** → Read the failed job logs (Step 3).
|
|
726
|
+
- **\`success\`** → Pipeline is healthy. If behavior is wrong, check job scripts and configuration.
|
|
727
|
+
- **\`canceled\`** → Re-run if needed: \`curl --request POST ... /pipelines/$PIPELINE_ID/retry\`
|
|
728
|
+
- **\`skipped\`** → All jobs were filtered out by \`rules:\`. Check rule conditions.
|
|
729
|
+
|
|
730
|
+
### Step 3: Read failed job logs
|
|
731
|
+
|
|
732
|
+
\`\`\`bash
|
|
733
|
+
# Get failed jobs
|
|
734
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
735
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs?scope=failed" | jq '.[].id'
|
|
736
|
+
|
|
737
|
+
# Read the trace for a failed job
|
|
738
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
739
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/jobs/$JOB_ID/trace"
|
|
740
|
+
\`\`\`
|
|
741
|
+
|
|
742
|
+
### Step 4: Diagnose by error pattern
|
|
743
|
+
|
|
744
|
+
| Error pattern | Likely cause | Fix |
|
|
745
|
+
|---------------|-------------|-----|
|
|
746
|
+
| "no matching runner" | No runner with matching tags | Check runner tags, register a runner |
|
|
747
|
+
| "image pull failed" | Docker image not found or auth failed | Check image name, registry credentials |
|
|
748
|
+
| "script exit code 1" | Script command failed | Read job log for the failing command |
|
|
749
|
+
| "artifact upload failed" | Artifact path doesn't exist or too large | Check \`artifacts.paths\`, size limits |
|
|
750
|
+
| "cache not found" | Cache key mismatch or first run | Expected on first run; check \`cache.key\` |
|
|
751
|
+
| "yaml invalid" | Syntax error in generated YAML | Run \`chant lint src/\` and CI Lint API |
|
|
752
|
+
| "pipeline filtered out" | All jobs filtered by rules | Check \`rules:\` conditions |
|
|
753
|
+
| "job timed out" | Job exceeded timeout | Increase \`timeout:\` or optimize job |
|
|
754
|
+
| "stuck or pending" | No available runner | Check runner status, tags, executor capacity |
|
|
755
|
+
| "environment does not exist" | \`on_stop\` references non-existent job | Check \`on_stop\` job name matches expanded name |
|
|
756
|
+
| "needs job not found" | \`needs:\` references non-existent job | Check job names, stage ordering |
|
|
757
|
+
|
|
758
|
+
## Variable management
|
|
759
|
+
|
|
760
|
+
### Variable types and precedence
|
|
761
|
+
|
|
762
|
+
Variables are resolved in this order (highest priority first):
|
|
763
|
+
1. Job-level \`variables:\`
|
|
764
|
+
2. Project CI/CD variables (Settings → CI/CD → Variables)
|
|
765
|
+
3. Group CI/CD variables
|
|
766
|
+
4. Instance CI/CD variables
|
|
767
|
+
|
|
768
|
+
### Protected and masked variables
|
|
769
|
+
|
|
770
|
+
- **Protected**: only available in pipelines on protected branches/tags
|
|
771
|
+
- **Masked**: hidden in job logs (value must meet masking requirements)
|
|
772
|
+
|
|
773
|
+
### Managing variables via API
|
|
774
|
+
|
|
775
|
+
\`\`\`bash
|
|
776
|
+
# List project variables
|
|
777
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
778
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/variables"
|
|
779
|
+
|
|
780
|
+
# Create a variable
|
|
781
|
+
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
782
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/variables" \\
|
|
783
|
+
--form "key=DEPLOY_TOKEN" --form "value=secret" --form "masked=true" --form "protected=true"
|
|
784
|
+
|
|
785
|
+
# Update a variable
|
|
786
|
+
curl --request PUT --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
787
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/variables/DEPLOY_TOKEN" \\
|
|
788
|
+
--form "value=new-secret"
|
|
789
|
+
|
|
790
|
+
# Delete a variable
|
|
791
|
+
curl --request DELETE --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
792
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/variables/DEPLOY_TOKEN"
|
|
793
|
+
\`\`\`
|
|
794
|
+
|
|
795
|
+
## Quick reference
|
|
796
|
+
|
|
797
|
+
### Pipeline info commands
|
|
798
|
+
|
|
799
|
+
\`\`\`bash
|
|
800
|
+
# List recent pipelines
|
|
801
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
802
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines?per_page=5"
|
|
803
|
+
|
|
804
|
+
# Get pipeline status
|
|
805
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
806
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID"
|
|
807
|
+
|
|
808
|
+
# Get jobs in a pipeline
|
|
809
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
810
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs"
|
|
811
|
+
|
|
812
|
+
# Read job log
|
|
813
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
814
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/jobs/$JOB_ID/trace"
|
|
815
|
+
|
|
816
|
+
# Retry a failed pipeline
|
|
817
|
+
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
818
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/retry"
|
|
819
|
+
|
|
820
|
+
# Cancel a running pipeline
|
|
821
|
+
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
822
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/cancel"
|
|
823
|
+
\`\`\`
|
|
824
|
+
|
|
825
|
+
### Full build-to-deploy pipeline
|
|
826
|
+
|
|
827
|
+
\`\`\`bash
|
|
828
|
+
# 1. Lint
|
|
829
|
+
chant lint src/
|
|
830
|
+
|
|
831
|
+
# 2. Build
|
|
832
|
+
chant build src/ --output .gitlab-ci.yml
|
|
833
|
+
|
|
834
|
+
# 3. Validate via API
|
|
835
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
836
|
+
--header "Content-Type: application/json" \\
|
|
837
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \\
|
|
838
|
+
--data-binary @- <<EOF
|
|
839
|
+
{"content": $(cat .gitlab-ci.yml | jq -Rs .)}
|
|
840
|
+
EOF
|
|
841
|
+
|
|
842
|
+
# 4. Push to feature branch
|
|
843
|
+
git checkout -b feature/pipeline-update
|
|
844
|
+
git add .gitlab-ci.yml
|
|
845
|
+
git commit -m "Update pipeline"
|
|
846
|
+
git push -u origin feature/pipeline-update
|
|
847
|
+
|
|
848
|
+
# 5. Open MR, review pipeline diff, merge
|
|
849
|
+
# Pipeline runs automatically on the default branch after merge
|
|
850
|
+
\`\`\`
|
|
325
851
|
`,
|
|
326
852
|
triggers: [
|
|
327
853
|
{ type: "file-pattern", value: "**/*.gitlab.ts" },
|
|
854
|
+
{ type: "file-pattern", value: "**/.gitlab-ci.yml" },
|
|
328
855
|
{ type: "context", value: "gitlab" },
|
|
856
|
+
{ type: "context", value: "pipeline" },
|
|
857
|
+
{ type: "context", value: "deploy" },
|
|
858
|
+
],
|
|
859
|
+
preConditions: [
|
|
860
|
+
"chant CLI is installed (chant --version succeeds)",
|
|
861
|
+
"git is configured and can push to the remote",
|
|
862
|
+
"Project has chant source files in src/",
|
|
863
|
+
],
|
|
864
|
+
postConditions: [
|
|
865
|
+
"Pipeline is in a stable state (success/manual/scheduled)",
|
|
866
|
+
"No failed jobs in the pipeline",
|
|
329
867
|
],
|
|
330
868
|
parameters: [],
|
|
331
869
|
examples: [
|
|
@@ -346,6 +884,63 @@ description: GitLab CI/CD best practices and common patterns
|
|
|
346
884
|
}),
|
|
347
885
|
})`,
|
|
348
886
|
},
|
|
887
|
+
{
|
|
888
|
+
title: "Deploy pipeline update",
|
|
889
|
+
description: "Build, validate, and deploy a pipeline change via MR workflow",
|
|
890
|
+
input: "Deploy my pipeline changes to production",
|
|
891
|
+
output: `chant lint src/
|
|
892
|
+
chant build src/ --output .gitlab-ci.yml
|
|
893
|
+
git checkout -b feature/pipeline-update
|
|
894
|
+
git add .gitlab-ci.yml
|
|
895
|
+
git commit -m "Update pipeline"
|
|
896
|
+
git push -u origin feature/pipeline-update
|
|
897
|
+
# Open MR in GitLab, review pipeline diff, then merge`,
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
title: "Preview pipeline changes",
|
|
901
|
+
description: "Validate pipeline configuration via lint and CI Lint API before deploying",
|
|
902
|
+
input: "Check if my pipeline changes are valid before pushing",
|
|
903
|
+
output: `chant lint src/
|
|
904
|
+
chant build src/ --output .gitlab-ci.yml
|
|
905
|
+
# Validate via GitLab CI Lint API
|
|
906
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
907
|
+
--header "Content-Type: application/json" \\
|
|
908
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \\
|
|
909
|
+
--data-binary '{"content": "'$(cat .gitlab-ci.yml | jq -Rs .)'", "dry_run": true}'`,
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
title: "Scaffold and deploy a Node.js pipeline",
|
|
913
|
+
description: "Use --template to scaffold a Node.js project, build YAML, and push to GitLab",
|
|
914
|
+
input: "Create a Node.js CI pipeline and deploy it to GitLab",
|
|
915
|
+
output: `# Scaffold the project
|
|
916
|
+
chant init --lexicon gitlab --template node-pipeline my-node-app
|
|
917
|
+
cd my-node-app
|
|
918
|
+
|
|
919
|
+
# Build the YAML
|
|
920
|
+
chant build src/ --output .gitlab-ci.yml
|
|
921
|
+
|
|
922
|
+
# The GitLab repo needs app files — create them
|
|
923
|
+
echo '{"scripts":{"build":"echo build","test":"node test.js"}}' > package.json
|
|
924
|
+
echo 'console.log("ok")' > test.js
|
|
925
|
+
|
|
926
|
+
# Push to GitLab
|
|
927
|
+
git init -b main
|
|
928
|
+
git add .gitlab-ci.yml package.json test.js
|
|
929
|
+
git commit -m "Initial pipeline"
|
|
930
|
+
git remote add origin git@gitlab.com:YOUR_GROUP/YOUR_PROJECT.git
|
|
931
|
+
git push -u origin main`,
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
title: "Retry a failed pipeline",
|
|
935
|
+
description: "Retry a failed pipeline and monitor its progress",
|
|
936
|
+
input: "Pipeline 12345 failed, retry it",
|
|
937
|
+
output: `# Retry the pipeline
|
|
938
|
+
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
939
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/12345/retry"
|
|
940
|
+
# Monitor status
|
|
941
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
942
|
+
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/12345"`,
|
|
943
|
+
},
|
|
349
944
|
],
|
|
350
945
|
},
|
|
351
946
|
];
|