@intentius/chant-lexicon-gitlab 0.0.16 → 0.0.22

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.
Files changed (65) hide show
  1. package/dist/integrity.json +17 -4
  2. package/dist/manifest.json +1 -1
  3. package/dist/rules/wgl016.ts +82 -0
  4. package/dist/rules/wgl017.ts +54 -0
  5. package/dist/rules/wgl018.ts +39 -0
  6. package/dist/rules/wgl019.ts +44 -0
  7. package/dist/rules/wgl020.ts +56 -0
  8. package/dist/rules/wgl021.ts +62 -0
  9. package/dist/rules/wgl022.ts +44 -0
  10. package/dist/rules/wgl023.ts +51 -0
  11. package/dist/rules/wgl024.ts +46 -0
  12. package/dist/rules/wgl025.ts +49 -0
  13. package/dist/rules/wgl026.ts +67 -0
  14. package/dist/rules/wgl027.ts +54 -0
  15. package/dist/rules/wgl028.ts +67 -0
  16. package/dist/rules/yaml-helpers.ts +82 -0
  17. package/dist/skills/chant-gitlab.md +2 -2
  18. package/package.json +20 -2
  19. package/src/codegen/fetch.test.ts +30 -0
  20. package/src/codegen/generate.test.ts +65 -0
  21. package/src/codegen/idempotency.test.ts +28 -0
  22. package/src/codegen/naming.test.ts +93 -0
  23. package/src/codegen/snapshot.test.ts +28 -19
  24. package/src/composites/composites.test.ts +160 -0
  25. package/src/coverage.test.ts +15 -7
  26. package/src/import/roundtrip.test.ts +132 -0
  27. package/src/lint/post-synth/wgl016.test.ts +72 -0
  28. package/src/lint/post-synth/wgl016.ts +82 -0
  29. package/src/lint/post-synth/wgl017.test.ts +53 -0
  30. package/src/lint/post-synth/wgl017.ts +54 -0
  31. package/src/lint/post-synth/wgl018.test.ts +69 -0
  32. package/src/lint/post-synth/wgl018.ts +39 -0
  33. package/src/lint/post-synth/wgl019.test.ts +76 -0
  34. package/src/lint/post-synth/wgl019.ts +44 -0
  35. package/src/lint/post-synth/wgl020.test.ts +54 -0
  36. package/src/lint/post-synth/wgl020.ts +56 -0
  37. package/src/lint/post-synth/wgl021.test.ts +62 -0
  38. package/src/lint/post-synth/wgl021.ts +62 -0
  39. package/src/lint/post-synth/wgl022.test.ts +86 -0
  40. package/src/lint/post-synth/wgl022.ts +44 -0
  41. package/src/lint/post-synth/wgl023.test.ts +88 -0
  42. package/src/lint/post-synth/wgl023.ts +51 -0
  43. package/src/lint/post-synth/wgl024.test.ts +77 -0
  44. package/src/lint/post-synth/wgl024.ts +46 -0
  45. package/src/lint/post-synth/wgl025.test.ts +85 -0
  46. package/src/lint/post-synth/wgl025.ts +49 -0
  47. package/src/lint/post-synth/wgl026.test.ts +87 -0
  48. package/src/lint/post-synth/wgl026.ts +67 -0
  49. package/src/lint/post-synth/wgl027.test.ts +84 -0
  50. package/src/lint/post-synth/wgl027.ts +54 -0
  51. package/src/lint/post-synth/wgl028.test.ts +95 -0
  52. package/src/lint/post-synth/wgl028.ts +67 -0
  53. package/src/lint/post-synth/yaml-helpers.ts +82 -0
  54. package/src/lsp/completions.test.ts +16 -6
  55. package/src/lsp/hover.test.ts +18 -7
  56. package/src/plugin.test.ts +14 -1
  57. package/src/plugin.ts +20 -3
  58. package/src/testdata/pipelines/deploy-envs.gitlab-ci.yml +60 -0
  59. package/src/testdata/pipelines/docker-build.gitlab-ci.yml +41 -0
  60. package/src/testdata/pipelines/includes-templates.gitlab-ci.yml +52 -0
  61. package/src/testdata/pipelines/monorepo.gitlab-ci.yml +51 -0
  62. package/src/testdata/pipelines/multi-stage.gitlab-ci.yml +56 -0
  63. package/src/testdata/pipelines/simple.gitlab-ci.yml +9 -0
  64. package/src/validate.test.ts +12 -6
  65. 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 { gitlabCompletions } from "./completions";
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",
@@ -1,7 +1,12 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { gitlabHover } from "./hover";
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();
@@ -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(6);
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
- return [wgl010, wgl011, wgl012, wgl013, wgl014, wgl015];
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[] {
@@ -356,7 +373,7 @@ user-invocable: true
356
373
 
357
374
  ## How chant and GitLab CI relate
358
375
 
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:
376
+ chant is a **synthesis compiler** — it compiles TypeScript source files into \`.gitlab-ci.yml\` (YAML). \`chant build\` does not call GitLab APIs; synthesis is pure and deterministic. Your job as an agent is to bridge synthesis and deployment:
360
377
 
361
378
  - Use **chant** for: build, lint, diff (local YAML comparison)
362
379
  - Use **git + GitLab API** for: push, merge requests, pipeline monitoring, job logs, rollback, and all deployment operations
@@ -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), invalid extends targets (WGL014), circular needs chains (WGL015) | Before push |
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