@intentius/chant-lexicon-gitlab 0.0.4 → 0.0.8
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 +5 -5
- package/dist/manifest.json +1 -1
- package/dist/meta.json +5 -0
- package/dist/skills/chant-gitlab.md +59 -0
- package/dist/types/index.d.ts +13 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +87 -441
- package/src/codegen/generate-lexicon.ts +1 -1
- package/src/codegen/package.ts +2 -0
- 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/import/parser.test.ts +3 -3
- package/src/import/parser.ts +12 -26
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +2 -0
- package/src/plugin.test.ts +6 -8
- package/src/plugin.ts +73 -101
- package/src/serializer.test.ts +6 -6
- package/src/serializer.ts +1 -9
- package/src/validate.test.ts +13 -22
- package/src/validate.ts +17 -108
- package/dist/skills/gitlab-ci.md +0 -37
- package/src/codegen/rollback.ts +0 -26
|
@@ -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, {
|
package/src/codegen/package.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* with GitLab-specific manifest building and skill collection.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createRequire } from "module";
|
|
6
7
|
import { readFileSync } from "fs";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
7
9
|
import { join, dirname } from "path";
|
|
8
10
|
import { fileURLToPath } from "url";
|
|
9
11
|
import type { IntrinsicDef } from "@intentius/chant/lexicon";
|
|
@@ -5,9 +5,9 @@ import { loadSchemaFixture } from "../testdata/load-fixtures";
|
|
|
5
5
|
const fixture = loadSchemaFixture();
|
|
6
6
|
|
|
7
7
|
describe("parseCISchema", () => {
|
|
8
|
-
test("returns
|
|
8
|
+
test("returns 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
|
|
@@ -65,7 +65,7 @@ workflow:
|
|
|
65
65
|
expect(workflow!.properties.name).toBe("My Pipeline");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
test("
|
|
68
|
+
test("preserves spec-native snake_case property keys", () => {
|
|
69
69
|
const yaml = `
|
|
70
70
|
test-job:
|
|
71
71
|
stage: test
|
|
@@ -77,8 +77,8 @@ test-job:
|
|
|
77
77
|
- npm test
|
|
78
78
|
`;
|
|
79
79
|
const ir = parser.parse(yaml);
|
|
80
|
-
expect(ir.resources[0].properties.
|
|
81
|
-
expect(ir.resources[0].properties.
|
|
80
|
+
expect(ir.resources[0].properties.before_script).toEqual(["echo setup"]);
|
|
81
|
+
expect(ir.resources[0].properties.after_script).toEqual(["echo done"]);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
test("converts kebab-case job names to camelCase", () => {
|
package/src/import/parser.ts
CHANGED
|
@@ -10,6 +10,14 @@ import type { TemplateParser, TemplateIR, ResourceIR } from "@intentius/chant/im
|
|
|
10
10
|
/**
|
|
11
11
|
* Reserved top-level keys in .gitlab-ci.yml that are NOT job definitions.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Convert snake_case to camelCase — used only for TS variable names (logicalId),
|
|
15
|
+
* NOT for spec property names.
|
|
16
|
+
*/
|
|
17
|
+
function snakeToCamelCase(name: string): string {
|
|
18
|
+
return name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
const RESERVED_KEYS = new Set([
|
|
14
22
|
"stages",
|
|
15
23
|
"variables",
|
|
@@ -24,28 +32,6 @@ const RESERVED_KEYS = new Set([
|
|
|
24
32
|
"pages",
|
|
25
33
|
]);
|
|
26
34
|
|
|
27
|
-
/**
|
|
28
|
-
* Map snake_case GitLab CI keys to camelCase for Chant properties.
|
|
29
|
-
*/
|
|
30
|
-
function toCamelCase(name: string): string {
|
|
31
|
-
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Recursively convert snake_case keys in an object to camelCase.
|
|
36
|
-
*/
|
|
37
|
-
function camelCaseKeys(value: unknown): unknown {
|
|
38
|
-
if (value === null || value === undefined) return value;
|
|
39
|
-
if (Array.isArray(value)) return value.map(camelCaseKeys);
|
|
40
|
-
if (typeof value === "object") {
|
|
41
|
-
const result: Record<string, unknown> = {};
|
|
42
|
-
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
43
|
-
result[toCamelCase(key)] = camelCaseKeys(val);
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
35
|
|
|
50
36
|
/**
|
|
51
37
|
* Parse a YAML document into a plain object.
|
|
@@ -227,7 +213,7 @@ export class GitLabParser implements TemplateParser {
|
|
|
227
213
|
resources.push({
|
|
228
214
|
logicalId: "defaults",
|
|
229
215
|
type: "GitLab::CI::Default",
|
|
230
|
-
properties:
|
|
216
|
+
properties: doc.default as Record<string, unknown>,
|
|
231
217
|
});
|
|
232
218
|
}
|
|
233
219
|
|
|
@@ -236,7 +222,7 @@ export class GitLabParser implements TemplateParser {
|
|
|
236
222
|
resources.push({
|
|
237
223
|
logicalId: "workflow",
|
|
238
224
|
type: "GitLab::CI::Workflow",
|
|
239
|
-
properties:
|
|
225
|
+
properties: doc.workflow as Record<string, unknown>,
|
|
240
226
|
});
|
|
241
227
|
}
|
|
242
228
|
|
|
@@ -256,9 +242,9 @@ export class GitLabParser implements TemplateParser {
|
|
|
256
242
|
obj.needs !== undefined
|
|
257
243
|
) {
|
|
258
244
|
resources.push({
|
|
259
|
-
logicalId:
|
|
245
|
+
logicalId: snakeToCamelCase(key.replace(/-/g, "_")),
|
|
260
246
|
type: "GitLab::CI::Job",
|
|
261
|
-
properties:
|
|
247
|
+
properties: obj as Record<string, unknown>,
|
|
262
248
|
metadata: {
|
|
263
249
|
originalName: key,
|
|
264
250
|
stage: typeof obj.stage === "string" ? obj.stage : undefined,
|
package/src/lsp/completions.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
1
2
|
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
2
3
|
import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
3
5
|
|
|
4
6
|
let cachedIndex: LexiconIndex | null = null;
|
|
5
7
|
|
package/src/lsp/hover.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
1
2
|
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
2
3
|
import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
3
5
|
|
|
4
6
|
let cachedIndex: LexiconIndex | null = null;
|
|
5
7
|
|
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
|
});
|
|
@@ -160,9 +159,12 @@ describe("gitlabPlugin", () => {
|
|
|
160
159
|
test("returns skills", () => {
|
|
161
160
|
const skills = gitlabPlugin.skills!();
|
|
162
161
|
expect(skills).toHaveLength(1);
|
|
163
|
-
expect(skills[0].name).toBe("gitlab
|
|
162
|
+
expect(skills[0].name).toBe("chant-gitlab");
|
|
164
163
|
expect(skills[0].description).toBeDefined();
|
|
165
|
-
expect(skills[0].content).toContain("
|
|
164
|
+
expect(skills[0].content).toContain("skill: chant-gitlab");
|
|
165
|
+
expect(skills[0].content).toContain("user-invocable: true");
|
|
166
|
+
expect(skills[0].content).toContain("chant build");
|
|
167
|
+
expect(skills[0].content).toContain("chant lint");
|
|
166
168
|
expect(skills[0].triggers).toHaveLength(2);
|
|
167
169
|
expect(skills[0].examples).toHaveLength(1);
|
|
168
170
|
});
|
|
@@ -192,7 +194,7 @@ describe("gitlabPlugin", () => {
|
|
|
192
194
|
const result = await catalog.handler();
|
|
193
195
|
const parsed = JSON.parse(result);
|
|
194
196
|
expect(Array.isArray(parsed)).toBe(true);
|
|
195
|
-
expect(parsed.length).toBe(
|
|
197
|
+
expect(parsed.length).toBe(16);
|
|
196
198
|
const job = parsed.find((e: { className: string }) => e.className === "Job");
|
|
197
199
|
expect(job).toBeDefined();
|
|
198
200
|
expect(job.kind).toBe("resource");
|
|
@@ -218,10 +220,6 @@ describe("gitlabPlugin", () => {
|
|
|
218
220
|
expect(typeof gitlabPlugin.coverage).toBe("function");
|
|
219
221
|
});
|
|
220
222
|
|
|
221
|
-
test("has rollback method", () => {
|
|
222
|
-
expect(typeof gitlabPlugin.rollback).toBe("function");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
223
|
test("has docs method", () => {
|
|
226
224
|
expect(typeof gitlabPlugin.docs).toBe("function");
|
|
227
225
|
});
|
package/src/plugin.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* for GitLab CI/CD pipelines.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { createRequire } from "module";
|
|
8
9
|
import type { LexiconPlugin, IntrinsicDef, SkillDefinition } 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";
|
|
@@ -41,20 +43,19 @@ export const gitlabPlugin: LexiconPlugin = {
|
|
|
41
43
|
|
|
42
44
|
initTemplates(): Record<string, string> {
|
|
43
45
|
return {
|
|
44
|
-
"_.ts": `export * from "./config";\n`,
|
|
45
46
|
"config.ts": `/**
|
|
46
47
|
* Shared pipeline configuration
|
|
47
48
|
*/
|
|
48
49
|
|
|
49
|
-
import
|
|
50
|
+
import { Image, Cache } from "@intentius/chant-lexicon-gitlab";
|
|
50
51
|
|
|
51
52
|
// Default image for all jobs
|
|
52
|
-
export const defaultImage = new
|
|
53
|
+
export const defaultImage = new Image({
|
|
53
54
|
name: "node:20-alpine",
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
// Standard cache configuration
|
|
57
|
-
export const npmCache = new
|
|
58
|
+
export const npmCache = new Cache({
|
|
58
59
|
key: "$CI_COMMIT_REF_SLUG",
|
|
59
60
|
paths: ["node_modules/"],
|
|
60
61
|
policy: "pull-push",
|
|
@@ -64,15 +65,15 @@ export const npmCache = new gl.Cache({
|
|
|
64
65
|
* Test job
|
|
65
66
|
*/
|
|
66
67
|
|
|
67
|
-
import
|
|
68
|
-
import
|
|
68
|
+
import { Job, Artifacts } from "@intentius/chant-lexicon-gitlab";
|
|
69
|
+
import { defaultImage, npmCache } from "./config";
|
|
69
70
|
|
|
70
|
-
export const test = new
|
|
71
|
+
export const test = new Job({
|
|
71
72
|
stage: "test",
|
|
72
|
-
image:
|
|
73
|
-
cache:
|
|
73
|
+
image: defaultImage,
|
|
74
|
+
cache: npmCache,
|
|
74
75
|
script: ["npm ci", "npm test"],
|
|
75
|
-
artifacts: new
|
|
76
|
+
artifacts: new Artifacts({
|
|
76
77
|
reports: { junit: "coverage/junit.xml" },
|
|
77
78
|
paths: ["coverage/"],
|
|
78
79
|
expireIn: "1 week",
|
|
@@ -142,18 +143,9 @@ export const test = new gl.Job({
|
|
|
142
143
|
|
|
143
144
|
async validate(options?: { verbose?: boolean }): Promise<void> {
|
|
144
145
|
const { validate } = await import("./validate");
|
|
146
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
145
147
|
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.");
|
|
148
|
+
printValidationResult(result);
|
|
157
149
|
},
|
|
158
150
|
|
|
159
151
|
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
@@ -166,7 +158,7 @@ export const test = new gl.Job({
|
|
|
166
158
|
|
|
167
159
|
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
168
160
|
const { packageLexicon } = await import("./codegen/package");
|
|
169
|
-
const {
|
|
161
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
170
162
|
const { join, dirname } = await import("path");
|
|
171
163
|
const { fileURLToPath } = await import("url");
|
|
172
164
|
|
|
@@ -174,53 +166,11 @@ export const test = new gl.Job({
|
|
|
174
166
|
|
|
175
167
|
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
176
168
|
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
|
-
}
|
|
169
|
+
writeBundleSpec(spec, distDir);
|
|
195
170
|
|
|
196
171
|
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
197
172
|
},
|
|
198
173
|
|
|
199
|
-
async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
|
|
200
|
-
const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
|
|
201
|
-
const { join, dirname } = await import("path");
|
|
202
|
-
const { fileURLToPath } = await import("url");
|
|
203
|
-
|
|
204
|
-
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
205
|
-
const snapshotsDir = join(pkgDir, ".snapshots");
|
|
206
|
-
|
|
207
|
-
if (options?.restore) {
|
|
208
|
-
const generatedDir = join(pkgDir, "src", "generated");
|
|
209
|
-
restoreSnapshot(String(options.restore), generatedDir);
|
|
210
|
-
console.error(`Restored snapshot: ${options.restore}`);
|
|
211
|
-
} else {
|
|
212
|
-
const snapshots = listSnapshots(snapshotsDir);
|
|
213
|
-
if (snapshots.length === 0) {
|
|
214
|
-
console.error("No snapshots available.");
|
|
215
|
-
} else {
|
|
216
|
-
console.error(`Available snapshots (${snapshots.length}):`);
|
|
217
|
-
for (const s of snapshots) {
|
|
218
|
-
console.error(` ${s.timestamp} ${s.resourceCount} resources ${s.path}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
|
|
224
174
|
mcpTools() {
|
|
225
175
|
return [
|
|
226
176
|
{
|
|
@@ -310,45 +260,67 @@ export const deploy = new Job({
|
|
|
310
260
|
skills(): SkillDefinition[] {
|
|
311
261
|
return [
|
|
312
262
|
{
|
|
313
|
-
name: "gitlab
|
|
314
|
-
description: "GitLab CI/CD
|
|
263
|
+
name: "chant-gitlab",
|
|
264
|
+
description: "GitLab CI/CD pipeline management — workflows, patterns, and troubleshooting",
|
|
315
265
|
content: `---
|
|
316
|
-
|
|
317
|
-
description: GitLab CI
|
|
266
|
+
skill: chant-gitlab
|
|
267
|
+
description: Build, validate, and deploy GitLab CI pipelines from a chant project
|
|
268
|
+
user-invocable: true
|
|
318
269
|
---
|
|
319
270
|
|
|
320
|
-
# GitLab CI
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
271
|
+
# Deploying GitLab CI Pipelines from Chant
|
|
272
|
+
|
|
273
|
+
This project defines GitLab CI jobs as TypeScript in \`src/\`. Use these steps to build, validate, and deploy.
|
|
274
|
+
|
|
275
|
+
## Build the pipeline
|
|
276
|
+
|
|
277
|
+
\`\`\`bash
|
|
278
|
+
chant build src/ --output .gitlab-ci.yml
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
## Validate before pushing
|
|
282
|
+
|
|
283
|
+
\`\`\`bash
|
|
284
|
+
chant lint src/
|
|
285
|
+
\`\`\`
|
|
286
|
+
|
|
287
|
+
For API-level validation against your GitLab instance:
|
|
288
|
+
\`\`\`bash
|
|
289
|
+
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
|
|
290
|
+
"https://gitlab.com/api/v4/ci/lint" \\
|
|
291
|
+
--data "{\\"content\\": \\"$(cat .gitlab-ci.yml)\\"}"
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
## Deploy
|
|
295
|
+
|
|
296
|
+
Commit and push the generated \`.gitlab-ci.yml\` — GitLab runs the pipeline automatically:
|
|
297
|
+
|
|
298
|
+
\`\`\`bash
|
|
299
|
+
chant build src/ --output .gitlab-ci.yml
|
|
300
|
+
git add .gitlab-ci.yml
|
|
301
|
+
git commit -m "Update pipeline"
|
|
302
|
+
git push
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
## Check pipeline status
|
|
306
|
+
|
|
307
|
+
- GitLab UI: project → CI/CD → Pipelines
|
|
308
|
+
- API: \`curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines?per_page=5"\`
|
|
309
|
+
|
|
310
|
+
## Retry a failed job
|
|
311
|
+
|
|
312
|
+
- GitLab UI: click Retry on the failed job
|
|
313
|
+
- API: \`curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/$PROJECT_ID/jobs/$JOB_ID/retry"\`
|
|
314
|
+
|
|
315
|
+
## Cancel a running pipeline
|
|
316
|
+
|
|
317
|
+
- API: \`curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/cancel"\`
|
|
318
|
+
|
|
319
|
+
## Troubleshooting
|
|
320
|
+
|
|
321
|
+
- Check job logs in GitLab UI: project → CI/CD → Jobs → click the job
|
|
322
|
+
- \`chant lint src/\` catches: missing scripts (WGL002), deprecated only/except (WGL001), missing stages (WGL003), artifacts without expiry (WGL004)
|
|
323
|
+
- Post-synth checks (WGL010, WGL011) run during build
|
|
352
324
|
`,
|
|
353
325
|
triggers: [
|
|
354
326
|
{ type: "file-pattern", value: "**/*.gitlab.ts" },
|
package/src/serializer.test.ts
CHANGED
|
@@ -121,12 +121,12 @@ describe("gitlabSerializer.serialize", () => {
|
|
|
121
121
|
expect(output).toContain("my-test-job:");
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
test("
|
|
124
|
+
test("passes through spec-native snake_case property keys", () => {
|
|
125
125
|
const entities = new Map<string, Declarable>();
|
|
126
126
|
entities.set("job", new MockJob({
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
before_script: ["echo hello"],
|
|
128
|
+
after_script: ["echo done"],
|
|
129
|
+
expire_in: "1 week",
|
|
130
130
|
}));
|
|
131
131
|
|
|
132
132
|
const output = gitlabSerializer.serialize(entities);
|
|
@@ -270,7 +270,7 @@ describe("nested objects and arrays", () => {
|
|
|
270
270
|
|
|
271
271
|
const output = gitlabSerializer.serialize(entities);
|
|
272
272
|
expect(output).toContain("variables:");
|
|
273
|
-
expect(output).toContain("
|
|
273
|
+
expect(output).toContain("NODE_ENV: production");
|
|
274
274
|
});
|
|
275
275
|
|
|
276
276
|
test("serializes arrays of strings", () => {
|
|
@@ -289,7 +289,7 @@ describe("nested objects and arrays", () => {
|
|
|
289
289
|
const entities = new Map<string, Declarable>();
|
|
290
290
|
entities.set("job", new MockJob({
|
|
291
291
|
interruptible: true,
|
|
292
|
-
|
|
292
|
+
allow_failure: false,
|
|
293
293
|
}));
|
|
294
294
|
|
|
295
295
|
const output = gitlabSerializer.serialize(entities);
|
package/src/serializer.ts
CHANGED
|
@@ -14,13 +14,6 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
|
14
14
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
15
15
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Convert camelCase or PascalCase to snake_case.
|
|
19
|
-
*/
|
|
20
|
-
function toSnakeCase(name: string): string {
|
|
21
|
-
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
/**
|
|
25
18
|
* GitLab CI visitor for the generic serializer walker.
|
|
26
19
|
*/
|
|
@@ -36,12 +29,11 @@ function gitlabVisitor(entityNames: Map<Declarable, string>): SerializerVisitor
|
|
|
36
29
|
const result: Record<string, unknown> = {};
|
|
37
30
|
for (const [key, value] of Object.entries(props)) {
|
|
38
31
|
if (value !== undefined) {
|
|
39
|
-
result[
|
|
32
|
+
result[key] = walk(value);
|
|
40
33
|
}
|
|
41
34
|
}
|
|
42
35
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
43
36
|
},
|
|
44
|
-
transformKey: toSnakeCase,
|
|
45
37
|
};
|
|
46
38
|
}
|
|
47
39
|
|