@intentius/chant-lexicon-gitlab 0.0.16 → 0.0.18
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 +17 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/wgl016.ts +82 -0
- package/dist/rules/wgl017.ts +54 -0
- package/dist/rules/wgl018.ts +39 -0
- package/dist/rules/wgl019.ts +44 -0
- package/dist/rules/wgl020.ts +56 -0
- package/dist/rules/wgl021.ts +62 -0
- package/dist/rules/wgl022.ts +44 -0
- package/dist/rules/wgl023.ts +51 -0
- package/dist/rules/wgl024.ts +46 -0
- package/dist/rules/wgl025.ts +49 -0
- package/dist/rules/wgl026.ts +67 -0
- package/dist/rules/wgl027.ts +54 -0
- package/dist/rules/wgl028.ts +67 -0
- package/dist/rules/yaml-helpers.ts +82 -0
- package/dist/skills/chant-gitlab.md +1 -1
- package/package.json +2 -2
- package/src/codegen/fetch.test.ts +30 -0
- package/src/codegen/generate.test.ts +65 -0
- package/src/codegen/idempotency.test.ts +28 -0
- package/src/codegen/naming.test.ts +93 -0
- package/src/codegen/snapshot.test.ts +28 -19
- package/src/composites/composites.test.ts +160 -0
- package/src/coverage.test.ts +15 -7
- package/src/import/roundtrip.test.ts +132 -0
- package/src/lint/post-synth/wgl016.test.ts +72 -0
- package/src/lint/post-synth/wgl016.ts +82 -0
- package/src/lint/post-synth/wgl017.test.ts +53 -0
- package/src/lint/post-synth/wgl017.ts +54 -0
- package/src/lint/post-synth/wgl018.test.ts +69 -0
- package/src/lint/post-synth/wgl018.ts +39 -0
- package/src/lint/post-synth/wgl019.test.ts +76 -0
- package/src/lint/post-synth/wgl019.ts +44 -0
- package/src/lint/post-synth/wgl020.test.ts +54 -0
- package/src/lint/post-synth/wgl020.ts +56 -0
- package/src/lint/post-synth/wgl021.test.ts +62 -0
- package/src/lint/post-synth/wgl021.ts +62 -0
- package/src/lint/post-synth/wgl022.test.ts +86 -0
- package/src/lint/post-synth/wgl022.ts +44 -0
- package/src/lint/post-synth/wgl023.test.ts +88 -0
- package/src/lint/post-synth/wgl023.ts +51 -0
- package/src/lint/post-synth/wgl024.test.ts +77 -0
- package/src/lint/post-synth/wgl024.ts +46 -0
- package/src/lint/post-synth/wgl025.test.ts +85 -0
- package/src/lint/post-synth/wgl025.ts +49 -0
- package/src/lint/post-synth/wgl026.test.ts +87 -0
- package/src/lint/post-synth/wgl026.ts +67 -0
- package/src/lint/post-synth/wgl027.test.ts +84 -0
- package/src/lint/post-synth/wgl027.ts +54 -0
- package/src/lint/post-synth/wgl028.test.ts +95 -0
- package/src/lint/post-synth/wgl028.ts +67 -0
- package/src/lint/post-synth/yaml-helpers.ts +82 -0
- package/src/lsp/completions.test.ts +16 -6
- package/src/lsp/hover.test.ts +18 -7
- package/src/plugin.test.ts +14 -1
- package/src/plugin.ts +19 -2
- package/src/testdata/pipelines/deploy-envs.gitlab-ci.yml +60 -0
- package/src/testdata/pipelines/docker-build.gitlab-ci.yml +41 -0
- package/src/testdata/pipelines/includes-templates.gitlab-ci.yml +52 -0
- package/src/testdata/pipelines/monorepo.gitlab-ci.yml +51 -0
- package/src/testdata/pipelines/multi-stage.gitlab-ci.yml +56 -0
- package/src/testdata/pipelines/simple.gitlab-ci.yml +9 -0
- package/src/validate.test.ts +12 -6
- package/src/variables.test.ts +58 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { wgl028, checkRedundantNeeds } from "./wgl028";
|
|
3
|
+
|
|
4
|
+
describe("WGL028: Redundant Needs", () => {
|
|
5
|
+
test("check metadata", () => {
|
|
6
|
+
expect(wgl028.id).toBe("WGL028");
|
|
7
|
+
expect(wgl028.description).toContain("Redundant");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("flags needs pointing to earlier stage job", () => {
|
|
11
|
+
const yaml = `stages:
|
|
12
|
+
- build
|
|
13
|
+
- test
|
|
14
|
+
- deploy
|
|
15
|
+
|
|
16
|
+
build-app:
|
|
17
|
+
stage: build
|
|
18
|
+
script:
|
|
19
|
+
- npm build
|
|
20
|
+
|
|
21
|
+
deploy-app:
|
|
22
|
+
stage: deploy
|
|
23
|
+
needs:
|
|
24
|
+
- build-app
|
|
25
|
+
script:
|
|
26
|
+
- deploy.sh
|
|
27
|
+
`;
|
|
28
|
+
const diags = checkRedundantNeeds(yaml);
|
|
29
|
+
expect(diags).toHaveLength(1);
|
|
30
|
+
expect(diags[0].severity).toBe("info");
|
|
31
|
+
expect(diags[0].message).toContain("deploy-app");
|
|
32
|
+
expect(diags[0].message).toContain("build-app");
|
|
33
|
+
expect(diags[0].message).toContain("earlier stage");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("does not flag needs within same stage", () => {
|
|
37
|
+
const yaml = `stages:
|
|
38
|
+
- build
|
|
39
|
+
- test
|
|
40
|
+
|
|
41
|
+
test-a:
|
|
42
|
+
stage: test
|
|
43
|
+
script:
|
|
44
|
+
- test-a
|
|
45
|
+
|
|
46
|
+
test-b:
|
|
47
|
+
stage: test
|
|
48
|
+
needs:
|
|
49
|
+
- test-a
|
|
50
|
+
script:
|
|
51
|
+
- test-b
|
|
52
|
+
`;
|
|
53
|
+
const diags = checkRedundantNeeds(yaml);
|
|
54
|
+
expect(diags).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("does not flag when no stages defined", () => {
|
|
58
|
+
const yaml = `build-app:
|
|
59
|
+
script:
|
|
60
|
+
- npm build
|
|
61
|
+
|
|
62
|
+
deploy-app:
|
|
63
|
+
needs:
|
|
64
|
+
- build-app
|
|
65
|
+
script:
|
|
66
|
+
- deploy.sh
|
|
67
|
+
`;
|
|
68
|
+
const diags = checkRedundantNeeds(yaml);
|
|
69
|
+
expect(diags).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("does not flag when no needs defined", () => {
|
|
73
|
+
const yaml = `stages:
|
|
74
|
+
- build
|
|
75
|
+
- test
|
|
76
|
+
|
|
77
|
+
build-app:
|
|
78
|
+
stage: build
|
|
79
|
+
script:
|
|
80
|
+
- npm build
|
|
81
|
+
|
|
82
|
+
test-app:
|
|
83
|
+
stage: test
|
|
84
|
+
script:
|
|
85
|
+
- npm test
|
|
86
|
+
`;
|
|
87
|
+
const diags = checkRedundantNeeds(yaml);
|
|
88
|
+
expect(diags).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("no diagnostics on empty yaml", () => {
|
|
92
|
+
const diags = checkRedundantNeeds("");
|
|
93
|
+
expect(diags).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL028: Redundant Needs
|
|
3
|
+
*
|
|
4
|
+
* Detects `needs:` entries that list jobs already implied by stage ordering.
|
|
5
|
+
* While not incorrect, redundant needs add noise and make the pipeline
|
|
6
|
+
* harder to maintain. This is informational only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, extractStages, extractJobs } from "./yaml-helpers";
|
|
11
|
+
|
|
12
|
+
export function checkRedundantNeeds(yaml: string): PostSynthDiagnostic[] {
|
|
13
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
14
|
+
|
|
15
|
+
const stages = extractStages(yaml);
|
|
16
|
+
if (stages.length === 0) return diagnostics;
|
|
17
|
+
|
|
18
|
+
const stageIndex = new Map<string, number>();
|
|
19
|
+
for (let i = 0; i < stages.length; i++) {
|
|
20
|
+
stageIndex.set(stages[i], i);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const jobs = extractJobs(yaml);
|
|
24
|
+
|
|
25
|
+
for (const [jobName, job] of jobs) {
|
|
26
|
+
if (!job.needs || !job.stage) continue;
|
|
27
|
+
|
|
28
|
+
const jobStageIdx = stageIndex.get(job.stage);
|
|
29
|
+
if (jobStageIdx === undefined) continue;
|
|
30
|
+
|
|
31
|
+
for (const need of job.needs) {
|
|
32
|
+
const neededJob = jobs.get(need);
|
|
33
|
+
if (!neededJob?.stage) continue;
|
|
34
|
+
|
|
35
|
+
const needStageIdx = stageIndex.get(neededJob.stage);
|
|
36
|
+
if (needStageIdx === undefined) continue;
|
|
37
|
+
|
|
38
|
+
// If the needed job is in an earlier stage, it's already implied
|
|
39
|
+
// by GitLab's default stage-based ordering
|
|
40
|
+
if (needStageIdx < jobStageIdx) {
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
checkId: "WGL028",
|
|
43
|
+
severity: "info",
|
|
44
|
+
message: `Job "${jobName}" lists "${need}" in needs: but it's already in an earlier stage (${neededJob.stage} → ${job.stage})`,
|
|
45
|
+
entity: jobName,
|
|
46
|
+
lexicon: "gitlab",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return diagnostics;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const wgl028: PostSynthCheck = {
|
|
56
|
+
id: "WGL028",
|
|
57
|
+
description: "Redundant needs — needs listing jobs already implied by stage ordering",
|
|
58
|
+
|
|
59
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
60
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
61
|
+
for (const [, output] of ctx.outputs) {
|
|
62
|
+
const yaml = getPrimaryOutput(output);
|
|
63
|
+
diagnostics.push(...checkRedundantNeeds(yaml));
|
|
64
|
+
}
|
|
65
|
+
return diagnostics;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -141,3 +141,85 @@ export function extractJobs(yaml: string): Map<string, ParsedJob> {
|
|
|
141
141
|
export function hasInclude(yaml: string): boolean {
|
|
142
142
|
return /^include:/m.test(yaml);
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Extract global variables from serialized YAML.
|
|
147
|
+
*/
|
|
148
|
+
export function extractGlobalVariables(yaml: string): Map<string, string> {
|
|
149
|
+
const vars = new Map<string, string>();
|
|
150
|
+
const match = yaml.match(/^variables:\n((?:\s+.+\n?)+)/m);
|
|
151
|
+
if (!match) return vars;
|
|
152
|
+
|
|
153
|
+
for (const line of match[1].split("\n")) {
|
|
154
|
+
const kv = line.match(/^\s+(\w+):\s+(.+)$/);
|
|
155
|
+
if (kv) {
|
|
156
|
+
vars.set(kv[1], kv[2].trim().replace(/^['"]|['"]$/g, ""));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return vars;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract the full section text for a given job name.
|
|
164
|
+
*/
|
|
165
|
+
export function extractJobSection(yaml: string, jobName: string): string | null {
|
|
166
|
+
const sections = yaml.split("\n\n");
|
|
167
|
+
for (const section of sections) {
|
|
168
|
+
const lines = section.split("\n");
|
|
169
|
+
if (lines.length > 0 && lines[0].startsWith(`${jobName}:`)) {
|
|
170
|
+
return section;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract rules from a job section.
|
|
178
|
+
*/
|
|
179
|
+
export function extractJobRules(section: string): ParsedRule[] {
|
|
180
|
+
const rules: ParsedRule[] = [];
|
|
181
|
+
const lines = section.split("\n");
|
|
182
|
+
|
|
183
|
+
let inRules = false;
|
|
184
|
+
let currentRule: ParsedRule = {};
|
|
185
|
+
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
if (line.match(/^\s+rules:$/)) {
|
|
188
|
+
inRules = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (inRules) {
|
|
193
|
+
const ruleStart = line.match(/^\s+- (if|when|changes):\s*(.*)$/);
|
|
194
|
+
if (ruleStart) {
|
|
195
|
+
if (Object.keys(currentRule).length > 0) {
|
|
196
|
+
rules.push(currentRule);
|
|
197
|
+
}
|
|
198
|
+
currentRule = {};
|
|
199
|
+
if (ruleStart[1] === "if") currentRule.if = ruleStart[2].trim();
|
|
200
|
+
if (ruleStart[1] === "when") currentRule.when = ruleStart[2].trim();
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const whenMatch = line.match(/^\s+when:\s+(.+)$/);
|
|
205
|
+
if (whenMatch) {
|
|
206
|
+
currentRule.when = whenMatch[1].trim();
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// End of rules block
|
|
211
|
+
if (!line.match(/^\s+\s/) || line.match(/^\s+[a-z_]+:/) && !line.match(/^\s+when:/)) {
|
|
212
|
+
if (Object.keys(currentRule).length > 0) {
|
|
213
|
+
rules.push(currentRule);
|
|
214
|
+
}
|
|
215
|
+
inRules = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (inRules && Object.keys(currentRule).length > 0) {
|
|
221
|
+
rules.push(currentRule);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return rules;
|
|
225
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
3
5
|
import type { CompletionContext } from "@intentius/chant/lsp/types";
|
|
4
6
|
|
|
7
|
+
const generatedDir = join(dirname(dirname(fileURLToPath(import.meta.url))), "generated");
|
|
8
|
+
const hasGenerated = existsSync(join(generatedDir, "lexicon-gitlab.json"));
|
|
9
|
+
|
|
5
10
|
function makeCtx(overrides: Partial<CompletionContext>): CompletionContext {
|
|
6
11
|
return {
|
|
7
12
|
uri: "file:///test.ts",
|
|
@@ -14,7 +19,8 @@ function makeCtx(overrides: Partial<CompletionContext>): CompletionContext {
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
describe("gitlabCompletions", () => {
|
|
17
|
-
test("returns resource completions for 'new ' prefix", () => {
|
|
22
|
+
test.skipIf(!hasGenerated)("returns resource completions for 'new ' prefix", async () => {
|
|
23
|
+
const { gitlabCompletions } = await import("./completions");
|
|
18
24
|
const ctx = makeCtx({
|
|
19
25
|
linePrefix: "const j = new Job",
|
|
20
26
|
wordAtCursor: "Job",
|
|
@@ -29,7 +35,8 @@ describe("gitlabCompletions", () => {
|
|
|
29
35
|
expect(job!.kind).toBe("resource");
|
|
30
36
|
});
|
|
31
37
|
|
|
32
|
-
test("returns all resource completions when no filter", () => {
|
|
38
|
+
test.skipIf(!hasGenerated)("returns all resource completions when no filter", async () => {
|
|
39
|
+
const { gitlabCompletions } = await import("./completions");
|
|
33
40
|
const ctx = makeCtx({
|
|
34
41
|
linePrefix: "const x = new ",
|
|
35
42
|
wordAtCursor: "",
|
|
@@ -44,7 +51,8 @@ describe("gitlabCompletions", () => {
|
|
|
44
51
|
expect(labels).toContain("Workflow");
|
|
45
52
|
});
|
|
46
53
|
|
|
47
|
-
test("filters completions by prefix", () => {
|
|
54
|
+
test.skipIf(!hasGenerated)("filters completions by prefix", async () => {
|
|
55
|
+
const { gitlabCompletions } = await import("./completions");
|
|
48
56
|
const ctx = makeCtx({
|
|
49
57
|
linePrefix: "const x = new D",
|
|
50
58
|
wordAtCursor: "D",
|
|
@@ -58,7 +66,8 @@ describe("gitlabCompletions", () => {
|
|
|
58
66
|
expect(labels).not.toContain("Job");
|
|
59
67
|
});
|
|
60
68
|
|
|
61
|
-
test("returns empty for non-constructor context", () => {
|
|
69
|
+
test.skipIf(!hasGenerated)("returns empty for non-constructor context", async () => {
|
|
70
|
+
const { gitlabCompletions } = await import("./completions");
|
|
62
71
|
const ctx = makeCtx({
|
|
63
72
|
linePrefix: "const x = foo(",
|
|
64
73
|
wordAtCursor: "",
|
|
@@ -70,7 +79,8 @@ describe("gitlabCompletions", () => {
|
|
|
70
79
|
expect(items).toHaveLength(0);
|
|
71
80
|
});
|
|
72
81
|
|
|
73
|
-
test("completion items have detail with resource type", () => {
|
|
82
|
+
test.skipIf(!hasGenerated)("completion items have detail with resource type", async () => {
|
|
83
|
+
const { gitlabCompletions } = await import("./completions");
|
|
74
84
|
const ctx = makeCtx({
|
|
75
85
|
linePrefix: "const j = new Job",
|
|
76
86
|
wordAtCursor: "Job",
|
package/src/lsp/hover.test.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
3
5
|
import type { HoverContext } from "@intentius/chant/lsp/types";
|
|
4
6
|
|
|
7
|
+
const generatedDir = join(dirname(dirname(fileURLToPath(import.meta.url))), "generated");
|
|
8
|
+
const hasGenerated = existsSync(join(generatedDir, "lexicon-gitlab.json"));
|
|
9
|
+
|
|
5
10
|
function makeCtx(overrides: Partial<HoverContext>): HoverContext {
|
|
6
11
|
return {
|
|
7
12
|
uri: "file:///test.ts",
|
|
@@ -14,7 +19,8 @@ function makeCtx(overrides: Partial<HoverContext>): HoverContext {
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
describe("gitlabHover", () => {
|
|
17
|
-
test("returns hover for Job class", () => {
|
|
22
|
+
test.skipIf(!hasGenerated)("returns hover for Job class", async () => {
|
|
23
|
+
const { gitlabHover } = await import("./hover");
|
|
18
24
|
const ctx = makeCtx({ word: "Job" });
|
|
19
25
|
const hover = gitlabHover(ctx);
|
|
20
26
|
expect(hover).toBeDefined();
|
|
@@ -23,7 +29,8 @@ describe("gitlabHover", () => {
|
|
|
23
29
|
expect(hover!.contents).toContain("Resource entity");
|
|
24
30
|
});
|
|
25
31
|
|
|
26
|
-
test("returns hover for property entity", () => {
|
|
32
|
+
test.skipIf(!hasGenerated)("returns hover for property entity", async () => {
|
|
33
|
+
const { gitlabHover } = await import("./hover");
|
|
27
34
|
const ctx = makeCtx({ word: "Cache" });
|
|
28
35
|
const hover = gitlabHover(ctx);
|
|
29
36
|
expect(hover).toBeDefined();
|
|
@@ -32,27 +39,31 @@ describe("gitlabHover", () => {
|
|
|
32
39
|
expect(hover!.contents).toContain("Property entity");
|
|
33
40
|
});
|
|
34
41
|
|
|
35
|
-
test("returns hover for Default", () => {
|
|
42
|
+
test.skipIf(!hasGenerated)("returns hover for Default", async () => {
|
|
43
|
+
const { gitlabHover } = await import("./hover");
|
|
36
44
|
const ctx = makeCtx({ word: "Default" });
|
|
37
45
|
const hover = gitlabHover(ctx);
|
|
38
46
|
expect(hover).toBeDefined();
|
|
39
47
|
expect(hover!.contents).toContain("GitLab::CI::Default");
|
|
40
48
|
});
|
|
41
49
|
|
|
42
|
-
test("returns hover for Artifacts", () => {
|
|
50
|
+
test.skipIf(!hasGenerated)("returns hover for Artifacts", async () => {
|
|
51
|
+
const { gitlabHover } = await import("./hover");
|
|
43
52
|
const ctx = makeCtx({ word: "Artifacts" });
|
|
44
53
|
const hover = gitlabHover(ctx);
|
|
45
54
|
expect(hover).toBeDefined();
|
|
46
55
|
expect(hover!.contents).toContain("GitLab::CI::Artifacts");
|
|
47
56
|
});
|
|
48
57
|
|
|
49
|
-
test("returns undefined for unknown word", () => {
|
|
58
|
+
test.skipIf(!hasGenerated)("returns undefined for unknown word", async () => {
|
|
59
|
+
const { gitlabHover } = await import("./hover");
|
|
50
60
|
const ctx = makeCtx({ word: "UnknownEntity" });
|
|
51
61
|
const hover = gitlabHover(ctx);
|
|
52
62
|
expect(hover).toBeUndefined();
|
|
53
63
|
});
|
|
54
64
|
|
|
55
|
-
test("returns undefined for empty word", () => {
|
|
65
|
+
test.skipIf(!hasGenerated)("returns undefined for empty word", async () => {
|
|
66
|
+
const { gitlabHover } = await import("./hover");
|
|
56
67
|
const ctx = makeCtx({ word: "" });
|
|
57
68
|
const hover = gitlabHover(ctx);
|
|
58
69
|
expect(hover).toBeUndefined();
|
package/src/plugin.test.ts
CHANGED
|
@@ -83,7 +83,7 @@ describe("gitlabPlugin", () => {
|
|
|
83
83
|
|
|
84
84
|
test("returns post-synth checks", () => {
|
|
85
85
|
const checks = gitlabPlugin.postSynthChecks!();
|
|
86
|
-
expect(checks).toHaveLength(
|
|
86
|
+
expect(checks).toHaveLength(19);
|
|
87
87
|
const ids = checks.map((c) => c.id);
|
|
88
88
|
expect(ids).toContain("WGL010");
|
|
89
89
|
expect(ids).toContain("WGL011");
|
|
@@ -91,6 +91,19 @@ describe("gitlabPlugin", () => {
|
|
|
91
91
|
expect(ids).toContain("WGL013");
|
|
92
92
|
expect(ids).toContain("WGL014");
|
|
93
93
|
expect(ids).toContain("WGL015");
|
|
94
|
+
expect(ids).toContain("WGL016");
|
|
95
|
+
expect(ids).toContain("WGL017");
|
|
96
|
+
expect(ids).toContain("WGL018");
|
|
97
|
+
expect(ids).toContain("WGL019");
|
|
98
|
+
expect(ids).toContain("WGL020");
|
|
99
|
+
expect(ids).toContain("WGL021");
|
|
100
|
+
expect(ids).toContain("WGL022");
|
|
101
|
+
expect(ids).toContain("WGL023");
|
|
102
|
+
expect(ids).toContain("WGL024");
|
|
103
|
+
expect(ids).toContain("WGL025");
|
|
104
|
+
expect(ids).toContain("WGL026");
|
|
105
|
+
expect(ids).toContain("WGL027");
|
|
106
|
+
expect(ids).toContain("WGL028");
|
|
94
107
|
});
|
|
95
108
|
|
|
96
109
|
// -----------------------------------------------------------------------
|
package/src/plugin.ts
CHANGED
|
@@ -31,7 +31,24 @@ export const gitlabPlugin: LexiconPlugin = {
|
|
|
31
31
|
const { wgl013 } = require("./lint/post-synth/wgl013");
|
|
32
32
|
const { wgl014 } = require("./lint/post-synth/wgl014");
|
|
33
33
|
const { wgl015 } = require("./lint/post-synth/wgl015");
|
|
34
|
-
|
|
34
|
+
const { wgl016 } = require("./lint/post-synth/wgl016");
|
|
35
|
+
const { wgl017 } = require("./lint/post-synth/wgl017");
|
|
36
|
+
const { wgl018 } = require("./lint/post-synth/wgl018");
|
|
37
|
+
const { wgl019 } = require("./lint/post-synth/wgl019");
|
|
38
|
+
const { wgl020 } = require("./lint/post-synth/wgl020");
|
|
39
|
+
const { wgl021 } = require("./lint/post-synth/wgl021");
|
|
40
|
+
const { wgl022 } = require("./lint/post-synth/wgl022");
|
|
41
|
+
const { wgl023 } = require("./lint/post-synth/wgl023");
|
|
42
|
+
const { wgl024 } = require("./lint/post-synth/wgl024");
|
|
43
|
+
const { wgl025 } = require("./lint/post-synth/wgl025");
|
|
44
|
+
const { wgl026 } = require("./lint/post-synth/wgl026");
|
|
45
|
+
const { wgl027 } = require("./lint/post-synth/wgl027");
|
|
46
|
+
const { wgl028 } = require("./lint/post-synth/wgl028");
|
|
47
|
+
return [
|
|
48
|
+
wgl010, wgl011, wgl012, wgl013, wgl014, wgl015,
|
|
49
|
+
wgl016, wgl017, wgl018, wgl019, wgl020, wgl021,
|
|
50
|
+
wgl022, wgl023, wgl024, wgl025, wgl026, wgl027, wgl028,
|
|
51
|
+
];
|
|
35
52
|
},
|
|
36
53
|
|
|
37
54
|
intrinsics(): IntrinsicDef[] {
|
|
@@ -495,7 +512,7 @@ Add \`"dry_run": true, "include_merged_yaml": true\` for full expansion with inc
|
|
|
495
512
|
| Step | Catches | When to run |
|
|
496
513
|
|------|---------|-------------|
|
|
497
514
|
| \`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),
|
|
515
|
+
| \`chant build\` | Post-synth checks: undefined stages (WGL010), unreachable jobs (WGL011), deprecated properties (WGL012), invalid needs/extends targets (WGL013-014), circular needs (WGL015), secrets in variables (WGL016), insecure registry (WGL017), missing timeout/retry (WGL018-019), duplicate jobs (WGL020), unused variables (WGL021), missing artifacts expiry (WGL022), overly broad rules (WGL023), manual without allow_failure (WGL024), missing cache key (WGL025), DinD without TLS (WGL026), empty script (WGL027), redundant needs (WGL028) | Before push |
|
|
499
516
|
| CI Lint API | GitLab-specific validation: include resolution, variable expansion, YAML schema errors | Before merge to default branch |
|
|
500
517
|
|
|
501
518
|
Always run all three before deploying. Lint catches things the API cannot (and vice versa).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- build
|
|
3
|
+
- deploy
|
|
4
|
+
- cleanup
|
|
5
|
+
|
|
6
|
+
build:
|
|
7
|
+
stage: build
|
|
8
|
+
script:
|
|
9
|
+
- npm ci
|
|
10
|
+
- npm run build
|
|
11
|
+
artifacts:
|
|
12
|
+
paths:
|
|
13
|
+
- dist/
|
|
14
|
+
|
|
15
|
+
deploy-review:
|
|
16
|
+
stage: deploy
|
|
17
|
+
script:
|
|
18
|
+
- deploy-review.sh
|
|
19
|
+
environment:
|
|
20
|
+
name: review/$CI_COMMIT_REF_SLUG
|
|
21
|
+
url: https://$CI_ENVIRONMENT_SLUG.review.example.com
|
|
22
|
+
on_stop: stop-review
|
|
23
|
+
auto_stop_in: 1 week
|
|
24
|
+
resource_group: review/$CI_COMMIT_REF_SLUG
|
|
25
|
+
rules:
|
|
26
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
27
|
+
|
|
28
|
+
stop-review:
|
|
29
|
+
stage: cleanup
|
|
30
|
+
script:
|
|
31
|
+
- teardown-review.sh
|
|
32
|
+
environment:
|
|
33
|
+
name: review/$CI_COMMIT_REF_SLUG
|
|
34
|
+
action: stop
|
|
35
|
+
rules:
|
|
36
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
37
|
+
when: manual
|
|
38
|
+
needs:
|
|
39
|
+
- deploy-review
|
|
40
|
+
|
|
41
|
+
deploy-staging:
|
|
42
|
+
stage: deploy
|
|
43
|
+
script:
|
|
44
|
+
- deploy.sh staging
|
|
45
|
+
environment:
|
|
46
|
+
name: staging
|
|
47
|
+
url: https://staging.example.com
|
|
48
|
+
rules:
|
|
49
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
50
|
+
|
|
51
|
+
deploy-production:
|
|
52
|
+
stage: deploy
|
|
53
|
+
script:
|
|
54
|
+
- deploy.sh production
|
|
55
|
+
environment:
|
|
56
|
+
name: production
|
|
57
|
+
url: https://example.com
|
|
58
|
+
rules:
|
|
59
|
+
- if: $CI_COMMIT_TAG
|
|
60
|
+
when: manual
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- build
|
|
3
|
+
- push
|
|
4
|
+
|
|
5
|
+
variables:
|
|
6
|
+
DOCKER_TLS_CERTDIR: "/certs"
|
|
7
|
+
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
|
8
|
+
|
|
9
|
+
build-image:
|
|
10
|
+
stage: build
|
|
11
|
+
image: docker:27-cli
|
|
12
|
+
services:
|
|
13
|
+
- name: docker:27-dind
|
|
14
|
+
alias: docker
|
|
15
|
+
variables:
|
|
16
|
+
DOCKER_HOST: tcp://docker:2376
|
|
17
|
+
before_script:
|
|
18
|
+
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
19
|
+
script:
|
|
20
|
+
- docker build -t $IMAGE_TAG .
|
|
21
|
+
- docker push $IMAGE_TAG
|
|
22
|
+
rules:
|
|
23
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
24
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
25
|
+
|
|
26
|
+
push-latest:
|
|
27
|
+
stage: push
|
|
28
|
+
image: docker:27-cli
|
|
29
|
+
services:
|
|
30
|
+
- name: docker:27-dind
|
|
31
|
+
alias: docker
|
|
32
|
+
needs:
|
|
33
|
+
- build-image
|
|
34
|
+
before_script:
|
|
35
|
+
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
36
|
+
script:
|
|
37
|
+
- docker pull $IMAGE_TAG
|
|
38
|
+
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
|
|
39
|
+
- docker push $CI_REGISTRY_IMAGE:latest
|
|
40
|
+
rules:
|
|
41
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
include:
|
|
2
|
+
- template: Auto-DevOps.gitlab-ci.yml
|
|
3
|
+
- project: my-group/ci-templates
|
|
4
|
+
ref: main
|
|
5
|
+
file: /templates/build.yml
|
|
6
|
+
|
|
7
|
+
stages:
|
|
8
|
+
- build
|
|
9
|
+
- test
|
|
10
|
+
- deploy
|
|
11
|
+
|
|
12
|
+
default:
|
|
13
|
+
image: node:22-alpine
|
|
14
|
+
interruptible: true
|
|
15
|
+
retry:
|
|
16
|
+
max: 2
|
|
17
|
+
when:
|
|
18
|
+
- runner_system_failure
|
|
19
|
+
- stuck_or_timeout_failure
|
|
20
|
+
|
|
21
|
+
.test-template: &test-defaults
|
|
22
|
+
stage: test
|
|
23
|
+
before_script:
|
|
24
|
+
- npm ci
|
|
25
|
+
|
|
26
|
+
build:
|
|
27
|
+
stage: build
|
|
28
|
+
extends: .build-template
|
|
29
|
+
script:
|
|
30
|
+
- npm ci
|
|
31
|
+
- npm run build
|
|
32
|
+
|
|
33
|
+
unit-test:
|
|
34
|
+
<<: *test-defaults
|
|
35
|
+
script:
|
|
36
|
+
- npm test
|
|
37
|
+
|
|
38
|
+
integration-test:
|
|
39
|
+
<<: *test-defaults
|
|
40
|
+
script:
|
|
41
|
+
- npm run test:integration
|
|
42
|
+
rules:
|
|
43
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
44
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
45
|
+
|
|
46
|
+
deploy:
|
|
47
|
+
stage: deploy
|
|
48
|
+
extends: .deploy-template
|
|
49
|
+
script:
|
|
50
|
+
- deploy.sh
|
|
51
|
+
rules:
|
|
52
|
+
- if: $CI_COMMIT_TAG
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- build
|
|
3
|
+
- test
|
|
4
|
+
- deploy
|
|
5
|
+
|
|
6
|
+
variables:
|
|
7
|
+
GIT_DEPTH: 20
|
|
8
|
+
|
|
9
|
+
frontend:
|
|
10
|
+
stage: build
|
|
11
|
+
trigger:
|
|
12
|
+
include: frontend/.gitlab-ci.yml
|
|
13
|
+
strategy: depend
|
|
14
|
+
rules:
|
|
15
|
+
- changes:
|
|
16
|
+
- frontend/**/*
|
|
17
|
+
|
|
18
|
+
backend:
|
|
19
|
+
stage: build
|
|
20
|
+
trigger:
|
|
21
|
+
include: backend/.gitlab-ci.yml
|
|
22
|
+
strategy: depend
|
|
23
|
+
rules:
|
|
24
|
+
- changes:
|
|
25
|
+
- backend/**/*
|
|
26
|
+
|
|
27
|
+
e2e-matrix:
|
|
28
|
+
stage: test
|
|
29
|
+
image: cypress/included:13
|
|
30
|
+
needs:
|
|
31
|
+
- frontend
|
|
32
|
+
- backend
|
|
33
|
+
parallel:
|
|
34
|
+
matrix:
|
|
35
|
+
- BROWSER: [chrome, firefox]
|
|
36
|
+
VIEWPORT: [desktop, mobile]
|
|
37
|
+
script:
|
|
38
|
+
- cypress run --browser $BROWSER --config viewportPreset=$VIEWPORT
|
|
39
|
+
artifacts:
|
|
40
|
+
when: always
|
|
41
|
+
paths:
|
|
42
|
+
- cypress/screenshots/
|
|
43
|
+
- cypress/videos/
|
|
44
|
+
expire_in: 3 days
|
|
45
|
+
|
|
46
|
+
deploy-all:
|
|
47
|
+
stage: deploy
|
|
48
|
+
script:
|
|
49
|
+
- deploy.sh
|
|
50
|
+
rules:
|
|
51
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|