@intentius/chant-lexicon-gitlab 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +27 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +33 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +962 -0
- package/src/codegen/fetch.ts +73 -0
- package/src/codegen/generate-cli.ts +41 -0
- package/src/codegen/generate-lexicon.ts +53 -0
- package/src/codegen/generate-typescript.ts +144 -0
- package/src/codegen/generate.ts +166 -0
- package/src/codegen/naming.ts +52 -0
- package/src/codegen/package.ts +64 -0
- package/src/codegen/parse.test.ts +195 -0
- package/src/codegen/parse.ts +531 -0
- package/src/codegen/patches.test.ts +99 -0
- package/src/codegen/patches.ts +100 -0
- package/src/codegen/rollback.ts +26 -0
- package/src/codegen/snapshot.test.ts +109 -0
- package/src/coverage.test.ts +39 -0
- package/src/coverage.ts +52 -0
- package/src/generated/index.d.ts +248 -0
- package/src/generated/index.ts +23 -0
- package/src/generated/lexicon-gitlab.json +77 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +151 -0
- package/src/import/generator.ts +173 -0
- package/src/import/parser.test.ts +160 -0
- package/src/import/parser.ts +282 -0
- package/src/import/roundtrip.test.ts +89 -0
- package/src/index.ts +25 -0
- package/src/intrinsics.test.ts +42 -0
- package/src/intrinsics.ts +40 -0
- package/src/lint/post-synth/post-synth.test.ts +155 -0
- package/src/lint/post-synth/wgl010.ts +41 -0
- package/src/lint/post-synth/wgl011.ts +54 -0
- package/src/lint/post-synth/yaml-helpers.ts +88 -0
- package/src/lint/rules/artifact-no-expiry.ts +62 -0
- package/src/lint/rules/deprecated-only-except.ts +53 -0
- package/src/lint/rules/index.ts +8 -0
- package/src/lint/rules/missing-script.ts +65 -0
- package/src/lint/rules/missing-stage.ts +62 -0
- package/src/lint/rules/rules.test.ts +146 -0
- package/src/lsp/completions.test.ts +85 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +60 -0
- package/src/lsp/hover.ts +36 -0
- package/src/plugin.test.ts +228 -0
- package/src/plugin.ts +380 -0
- package/src/serializer.test.ts +309 -0
- package/src/serializer.ts +226 -0
- package/src/testdata/ci-schema-fixture.json +2184 -0
- package/src/testdata/create-fixture.ts +46 -0
- package/src/testdata/load-fixtures.ts +23 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.test.ts +43 -0
- package/src/validate.ts +125 -0
- package/src/variables.ts +27 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { gitlabHover } from "./hover";
|
|
3
|
+
import type { HoverContext } from "@intentius/chant/lsp/types";
|
|
4
|
+
|
|
5
|
+
function makeCtx(overrides: Partial<HoverContext>): HoverContext {
|
|
6
|
+
return {
|
|
7
|
+
uri: "file:///test.ts",
|
|
8
|
+
content: "",
|
|
9
|
+
position: { line: 0, character: 0 },
|
|
10
|
+
word: "",
|
|
11
|
+
lineText: "",
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("gitlabHover", () => {
|
|
17
|
+
test("returns hover for Job class", () => {
|
|
18
|
+
const ctx = makeCtx({ word: "Job" });
|
|
19
|
+
const hover = gitlabHover(ctx);
|
|
20
|
+
expect(hover).toBeDefined();
|
|
21
|
+
expect(hover!.contents).toContain("**Job**");
|
|
22
|
+
expect(hover!.contents).toContain("GitLab::CI::Job");
|
|
23
|
+
expect(hover!.contents).toContain("Resource entity");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns hover for property entity", () => {
|
|
27
|
+
const ctx = makeCtx({ word: "Cache" });
|
|
28
|
+
const hover = gitlabHover(ctx);
|
|
29
|
+
expect(hover).toBeDefined();
|
|
30
|
+
expect(hover!.contents).toContain("**Cache**");
|
|
31
|
+
expect(hover!.contents).toContain("GitLab::CI::Cache");
|
|
32
|
+
expect(hover!.contents).toContain("Property entity");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns hover for Default", () => {
|
|
36
|
+
const ctx = makeCtx({ word: "Default" });
|
|
37
|
+
const hover = gitlabHover(ctx);
|
|
38
|
+
expect(hover).toBeDefined();
|
|
39
|
+
expect(hover!.contents).toContain("GitLab::CI::Default");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("returns hover for Artifacts", () => {
|
|
43
|
+
const ctx = makeCtx({ word: "Artifacts" });
|
|
44
|
+
const hover = gitlabHover(ctx);
|
|
45
|
+
expect(hover).toBeDefined();
|
|
46
|
+
expect(hover!.contents).toContain("GitLab::CI::Artifacts");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns undefined for unknown word", () => {
|
|
50
|
+
const ctx = makeCtx({ word: "UnknownEntity" });
|
|
51
|
+
const hover = gitlabHover(ctx);
|
|
52
|
+
expect(hover).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns undefined for empty word", () => {
|
|
56
|
+
const ctx = makeCtx({ word: "" });
|
|
57
|
+
const hover = gitlabHover(ctx);
|
|
58
|
+
expect(hover).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/lsp/hover.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
2
|
+
import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
3
|
+
|
|
4
|
+
let cachedIndex: LexiconIndex | null = null;
|
|
5
|
+
|
|
6
|
+
function getIndex(): LexiconIndex {
|
|
7
|
+
if (cachedIndex) return cachedIndex;
|
|
8
|
+
const data = require("../generated/lexicon-gitlab.json") as Record<string, LexiconEntry>;
|
|
9
|
+
cachedIndex = new LexiconIndex(data);
|
|
10
|
+
return cachedIndex;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provide hover information for GitLab CI entity types.
|
|
15
|
+
*/
|
|
16
|
+
export function gitlabHover(ctx: HoverContext): HoverInfo | undefined {
|
|
17
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
|
|
23
|
+
lines.push(`**${className}**`);
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(`GitLab CI type: \`${entry.resourceType}\``);
|
|
26
|
+
|
|
27
|
+
if (entry.kind === "resource") {
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push("*Resource entity — serialized as a top-level CI key*");
|
|
30
|
+
} else {
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push("*Property entity — used as a nested value in resources*");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { contents: lines.join("\n") };
|
|
36
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { gitlabPlugin } from "./plugin";
|
|
3
|
+
import { isLexiconPlugin } from "@intentius/chant/lexicon";
|
|
4
|
+
|
|
5
|
+
describe("gitlabPlugin", () => {
|
|
6
|
+
// -----------------------------------------------------------------------
|
|
7
|
+
// Basic interface
|
|
8
|
+
// -----------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
test("satisfies isLexiconPlugin type guard", () => {
|
|
11
|
+
expect(isLexiconPlugin(gitlabPlugin)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("has correct name and serializer", () => {
|
|
15
|
+
expect(gitlabPlugin.name).toBe("gitlab");
|
|
16
|
+
expect(gitlabPlugin.serializer.name).toBe("gitlab");
|
|
17
|
+
expect(gitlabPlugin.serializer.rulePrefix).toBe("WGL");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// -----------------------------------------------------------------------
|
|
21
|
+
// Intrinsics
|
|
22
|
+
// -----------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
test("returns intrinsics", () => {
|
|
25
|
+
const intrinsics = gitlabPlugin.intrinsics!();
|
|
26
|
+
expect(intrinsics).toHaveLength(1);
|
|
27
|
+
expect(intrinsics[0].name).toBe("reference");
|
|
28
|
+
expect(intrinsics[0].outputKey).toBe("!reference");
|
|
29
|
+
expect(intrinsics[0].isTag).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// -----------------------------------------------------------------------
|
|
33
|
+
// Template detection
|
|
34
|
+
// -----------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
test("detectTemplate returns true for stages array", () => {
|
|
37
|
+
expect(gitlabPlugin.detectTemplate!({ stages: ["build", "test"] })).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("detectTemplate returns true for image + script", () => {
|
|
41
|
+
expect(gitlabPlugin.detectTemplate!({ image: "node:20", script: ["test"] })).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("detectTemplate returns true for job-like entries", () => {
|
|
45
|
+
expect(
|
|
46
|
+
gitlabPlugin.detectTemplate!({
|
|
47
|
+
test_job: { stage: "test", script: ["npm test"] },
|
|
48
|
+
}),
|
|
49
|
+
).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("detectTemplate returns false for empty object", () => {
|
|
53
|
+
expect(gitlabPlugin.detectTemplate!({})).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("detectTemplate returns false for non-object", () => {
|
|
57
|
+
expect(gitlabPlugin.detectTemplate!("string")).toBe(false);
|
|
58
|
+
expect(gitlabPlugin.detectTemplate!(null)).toBe(false);
|
|
59
|
+
expect(gitlabPlugin.detectTemplate!(42)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("detectTemplate returns false for unrelated object", () => {
|
|
63
|
+
expect(gitlabPlugin.detectTemplate!({ AWSTemplateFormatVersion: "2010-09-09" })).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// -----------------------------------------------------------------------
|
|
67
|
+
// Lint rules
|
|
68
|
+
// -----------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
test("returns lint rules", () => {
|
|
71
|
+
const rules = gitlabPlugin.lintRules!();
|
|
72
|
+
expect(rules).toHaveLength(4);
|
|
73
|
+
const ids = rules.map((r) => r.id);
|
|
74
|
+
expect(ids).toContain("WGL001");
|
|
75
|
+
expect(ids).toContain("WGL002");
|
|
76
|
+
expect(ids).toContain("WGL003");
|
|
77
|
+
expect(ids).toContain("WGL004");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// -----------------------------------------------------------------------
|
|
81
|
+
// Post-synth checks
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
test("returns post-synth checks", () => {
|
|
85
|
+
const checks = gitlabPlugin.postSynthChecks!();
|
|
86
|
+
expect(checks).toHaveLength(2);
|
|
87
|
+
const ids = checks.map((c) => c.id);
|
|
88
|
+
expect(ids).toContain("WGL010");
|
|
89
|
+
expect(ids).toContain("WGL011");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// -----------------------------------------------------------------------
|
|
93
|
+
// Init templates
|
|
94
|
+
// -----------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
test("returns init templates", () => {
|
|
97
|
+
const templates = gitlabPlugin.initTemplates!();
|
|
98
|
+
expect(templates).toBeDefined();
|
|
99
|
+
expect(templates["_.ts"]).toBeDefined();
|
|
100
|
+
expect(templates["config.ts"]).toBeDefined();
|
|
101
|
+
expect(templates["test.ts"]).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("init templates import from gitlab lexicon", () => {
|
|
105
|
+
const templates = gitlabPlugin.initTemplates!();
|
|
106
|
+
expect(templates["config.ts"]).toContain("@intentius/chant-lexicon-gitlab");
|
|
107
|
+
expect(templates["test.ts"]).toContain("@intentius/chant-lexicon-gitlab");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
// LSP
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
test("completionProvider returns results for new prefix", () => {
|
|
115
|
+
const items = gitlabPlugin.completionProvider!({
|
|
116
|
+
uri: "file:///a.ts",
|
|
117
|
+
content: "const j = new Job",
|
|
118
|
+
position: { line: 0, character: 17 },
|
|
119
|
+
wordAtCursor: "Job",
|
|
120
|
+
linePrefix: "const j = new Job",
|
|
121
|
+
});
|
|
122
|
+
expect(items.length).toBeGreaterThan(0);
|
|
123
|
+
const job = items.find((i) => i.label === "Job");
|
|
124
|
+
expect(job).toBeDefined();
|
|
125
|
+
expect(job!.kind).toBe("resource");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("hoverProvider returns info for known entity", () => {
|
|
129
|
+
const hover = gitlabPlugin.hoverProvider!({
|
|
130
|
+
uri: "file:///a.ts",
|
|
131
|
+
content: "new Job({})",
|
|
132
|
+
position: { line: 0, character: 4 },
|
|
133
|
+
word: "Job",
|
|
134
|
+
lineText: "new Job({})",
|
|
135
|
+
});
|
|
136
|
+
expect(hover).toBeDefined();
|
|
137
|
+
expect(hover!.contents).toContain("Job");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// -----------------------------------------------------------------------
|
|
141
|
+
// Template import
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
test("has templateParser method", () => {
|
|
145
|
+
expect(typeof gitlabPlugin.templateParser).toBe("function");
|
|
146
|
+
const parser = gitlabPlugin.templateParser!();
|
|
147
|
+
expect(typeof parser.parse).toBe("function");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("has templateGenerator method", () => {
|
|
151
|
+
expect(typeof gitlabPlugin.templateGenerator).toBe("function");
|
|
152
|
+
const gen = gitlabPlugin.templateGenerator!();
|
|
153
|
+
expect(typeof gen.generate).toBe("function");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
// Skills
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
test("returns skills", () => {
|
|
161
|
+
const skills = gitlabPlugin.skills!();
|
|
162
|
+
expect(skills).toHaveLength(1);
|
|
163
|
+
expect(skills[0].name).toBe("gitlab-ci");
|
|
164
|
+
expect(skills[0].description).toBeDefined();
|
|
165
|
+
expect(skills[0].content).toContain("GitLab CI/CD");
|
|
166
|
+
expect(skills[0].triggers).toHaveLength(2);
|
|
167
|
+
expect(skills[0].examples).toHaveLength(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
// MCP
|
|
172
|
+
// -----------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
test("returns MCP tools", () => {
|
|
175
|
+
const tools = gitlabPlugin.mcpTools!();
|
|
176
|
+
expect(tools).toHaveLength(1);
|
|
177
|
+
expect(tools[0].name).toBe("diff");
|
|
178
|
+
expect(typeof tools[0].handler).toBe("function");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("returns MCP resources", () => {
|
|
182
|
+
const resources = gitlabPlugin.mcpResources!();
|
|
183
|
+
expect(resources.length).toBeGreaterThan(0);
|
|
184
|
+
const uris = resources.map((r) => r.uri);
|
|
185
|
+
expect(uris).toContain("resource-catalog");
|
|
186
|
+
expect(uris).toContain("examples/basic-pipeline");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("MCP resource-catalog handler returns JSON", async () => {
|
|
190
|
+
const resources = gitlabPlugin.mcpResources!();
|
|
191
|
+
const catalog = resources.find((r) => r.uri === "resource-catalog")!;
|
|
192
|
+
const result = await catalog.handler();
|
|
193
|
+
const parsed = JSON.parse(result);
|
|
194
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
195
|
+
expect(parsed.length).toBe(15);
|
|
196
|
+
const job = parsed.find((e: { className: string }) => e.className === "Job");
|
|
197
|
+
expect(job).toBeDefined();
|
|
198
|
+
expect(job.kind).toBe("resource");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
// Methods exist
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
test("has generate method", () => {
|
|
206
|
+
expect(typeof gitlabPlugin.generate).toBe("function");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("has validate method", () => {
|
|
210
|
+
expect(typeof gitlabPlugin.validate).toBe("function");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("has package method", () => {
|
|
214
|
+
expect(typeof gitlabPlugin.package).toBe("function");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("has coverage method", () => {
|
|
218
|
+
expect(typeof gitlabPlugin.coverage).toBe("function");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("has rollback method", () => {
|
|
222
|
+
expect(typeof gitlabPlugin.rollback).toBe("function");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("has docs method", () => {
|
|
226
|
+
expect(typeof gitlabPlugin.docs).toBe("function");
|
|
227
|
+
});
|
|
228
|
+
});
|