@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
@@ -1,22 +1,35 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "2b7f06fc4199f834",
4
+ "manifest.json": "d334dcb02631ec7e",
5
5
  "meta.json": "c663c6c63748a9d0",
6
6
  "types/index.d.ts": "64e65524615be023",
7
7
  "rules/missing-stage.ts": "6d5379e74209a735",
8
8
  "rules/missing-script.ts": "923dde9acb46cc28",
9
9
  "rules/deprecated-only-except.ts": "1f5a8c785777fb03",
10
10
  "rules/artifact-no-expiry.ts": "26874cb6adfbca26",
11
+ "rules/wgl018.ts": "6ba77c7fa7a59f25",
11
12
  "rules/wgl012.ts": "3d188d13fb2236c0",
13
+ "rules/wgl024.ts": "54fe7a6aec3d3e23",
14
+ "rules/wgl020.ts": "eff135a38c29ff2f",
15
+ "rules/wgl017.ts": "a9d460f549725f85",
16
+ "rules/wgl023.ts": "d02ea51ac7da06c4",
12
17
  "rules/wgl011.ts": "b6b97e5104d91267",
18
+ "rules/wgl021.ts": "42470ba28e215bae",
13
19
  "rules/wgl015.ts": "d7e9e080994f985",
20
+ "rules/wgl025.ts": "fcdf14c9d296e7b5",
21
+ "rules/wgl026.ts": "7c7a15478b702349",
14
22
  "rules/wgl010.ts": "1548cad287cdf286",
15
23
  "rules/wgl013.ts": "3519c933e23fc605",
24
+ "rules/wgl028.ts": "77c31c05a975bacb",
25
+ "rules/wgl016.ts": "f76358f42b1f39ea",
26
+ "rules/wgl022.ts": "adba5b533dc416c9",
27
+ "rules/wgl019.ts": "5a4ab8ebb115f074",
28
+ "rules/wgl027.ts": "ea7928f37607a583",
16
29
  "rules/wgl014.ts": "6248a852888e8028",
17
- "rules/yaml-helpers.ts": "b5416b80369484f2",
18
- "skills/chant-gitlab.md": "4393eb63e0b84b7f",
30
+ "rules/yaml-helpers.ts": "3e414c7affe56728",
31
+ "skills/chant-gitlab.md": "1203c19da2dc5a53",
19
32
  "skills/gitlab-ci-patterns.md": "bdb522359253aac8"
20
33
  },
21
- "composite": "b56a2a2ac9f9f569"
34
+ "composite": "5c25b595ef3236c"
22
35
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.0.16",
3
+ "version": "0.0.22",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitLab",
6
6
  "intrinsics": [
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WGL016: Secrets in Variables
3
+ *
4
+ * Detects hardcoded passwords, tokens, or secrets in `variables:` blocks.
5
+ * These should use CI/CD masked variables instead.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput } from "./yaml-helpers";
10
+
11
+ const SECRET_PATTERNS = [
12
+ /password\s*[:=]\s*['"]?[^\s'"]+/i,
13
+ /secret\s*[:=]\s*['"]?[^\s'"]+/i,
14
+ /token\s*[:=]\s*['"]?[^\s'"]+/i,
15
+ /api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/i,
16
+ /private[_-]?key\s*[:=]\s*['"]?[^\s'"]+/i,
17
+ ];
18
+
19
+ /** Variable name patterns that indicate credentials. */
20
+ const SECRET_VAR_NAMES = [
21
+ /password/i,
22
+ /secret/i,
23
+ /token/i,
24
+ /api[_-]?key/i,
25
+ /private[_-]?key/i,
26
+ /credentials?/i,
27
+ ];
28
+
29
+ /** Values that are clearly references (not hardcoded secrets). */
30
+ function isReference(value: string): boolean {
31
+ return value.startsWith("$") || value.startsWith("${");
32
+ }
33
+
34
+ export function checkSecretsInVariables(yaml: string): PostSynthDiagnostic[] {
35
+ const diagnostics: PostSynthDiagnostic[] = [];
36
+
37
+ // Extract variables blocks (global and per-job)
38
+ const varBlocks = yaml.matchAll(/^(\s*)variables:\n((?:\1\s+.+\n?)+)/gm);
39
+
40
+ for (const block of varBlocks) {
41
+ const lines = block[2].split("\n");
42
+ for (const line of lines) {
43
+ const kv = line.match(/^\s+(\w+):\s+(.+)$/);
44
+ if (!kv) continue;
45
+
46
+ const [, varName, rawValue] = kv;
47
+ const value = rawValue.trim().replace(/^['"]|['"]$/g, "");
48
+
49
+ if (isReference(value)) continue;
50
+
51
+ // Check if variable name suggests a secret
52
+ for (const pattern of SECRET_VAR_NAMES) {
53
+ if (pattern.test(varName)) {
54
+ diagnostics.push({
55
+ checkId: "WGL016",
56
+ severity: "error",
57
+ message: `Variable "${varName}" appears to contain a hardcoded secret — use a CI/CD masked variable instead`,
58
+ entity: varName,
59
+ lexicon: "gitlab",
60
+ });
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ return diagnostics;
68
+ }
69
+
70
+ export const wgl016: PostSynthCheck = {
71
+ id: "WGL016",
72
+ description: "Secrets in variables — hardcoded passwords or tokens in variables blocks",
73
+
74
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
75
+ const diagnostics: PostSynthDiagnostic[] = [];
76
+ for (const [, output] of ctx.outputs) {
77
+ const yaml = getPrimaryOutput(output);
78
+ diagnostics.push(...checkSecretsInVariables(yaml));
79
+ }
80
+ return diagnostics;
81
+ },
82
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WGL017: Insecure Registry
3
+ *
4
+ * Detects Docker push/pull to non-HTTPS registries in job scripts.
5
+ * Using HTTP for container registries is a security risk.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput, extractJobs } from "./yaml-helpers";
10
+
11
+ const INSECURE_REGISTRY_PATTERN = /docker\s+(push|pull|tag|login)\s+.*http:\/\/[^\s]+/;
12
+
13
+ export function checkInsecureRegistry(yaml: string): PostSynthDiagnostic[] {
14
+ const diagnostics: PostSynthDiagnostic[] = [];
15
+
16
+ const sections = yaml.split("\n\n");
17
+ for (const section of sections) {
18
+ const lines = section.split("\n");
19
+ if (lines.length === 0) continue;
20
+
21
+ const topMatch = lines[0].match(/^(\.?[a-z][a-z0-9_.-]*):/);
22
+ if (!topMatch) continue;
23
+ const jobName = topMatch[1];
24
+
25
+ for (const line of lines) {
26
+ if (INSECURE_REGISTRY_PATTERN.test(line)) {
27
+ diagnostics.push({
28
+ checkId: "WGL017",
29
+ severity: "warning",
30
+ message: `Job "${jobName}" uses an insecure (HTTP) container registry — use HTTPS instead`,
31
+ entity: jobName,
32
+ lexicon: "gitlab",
33
+ });
34
+ break;
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ }
41
+
42
+ export const wgl017: PostSynthCheck = {
43
+ id: "WGL017",
44
+ description: "Insecure registry — Docker push/pull to non-HTTPS registry",
45
+
46
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
47
+ const diagnostics: PostSynthDiagnostic[] = [];
48
+ for (const [, output] of ctx.outputs) {
49
+ const yaml = getPrimaryOutput(output);
50
+ diagnostics.push(...checkInsecureRegistry(yaml));
51
+ }
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * WGL018: Missing Timeout
3
+ *
4
+ * Warns about jobs without an explicit `timeout:` setting.
5
+ * The default (1 hour) may be too long for most jobs.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
10
+
11
+ export const wgl018: PostSynthCheck = {
12
+ id: "WGL018",
13
+ description: "Missing timeout — jobs without explicit timeout may run too long",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ const diagnostics: PostSynthDiagnostic[] = [];
17
+
18
+ for (const [entityName, entity] of ctx.entities) {
19
+ if (isPropertyDeclarable(entity)) continue;
20
+ const entityType = (entity as Record<string, unknown>).entityType as string;
21
+ if (entityType !== "GitLab::CI::Job") continue;
22
+
23
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
24
+ if (!props) continue;
25
+
26
+ if (!props.timeout) {
27
+ diagnostics.push({
28
+ checkId: "WGL018",
29
+ severity: "warning",
30
+ message: `Job "${entityName}" has no explicit timeout — default is 1 hour which may be too long`,
31
+ entity: entityName,
32
+ lexicon: "gitlab",
33
+ });
34
+ }
35
+ }
36
+
37
+ return diagnostics;
38
+ },
39
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WGL019: Missing Retry on Deploy Jobs
3
+ *
4
+ * Deploy-stage jobs should have a `retry:` strategy to handle transient
5
+ * infrastructure failures. This is informational, not a hard requirement.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
10
+
11
+ const DEPLOY_STAGES = new Set(["deploy", "deployment", "release", "production", "staging"]);
12
+
13
+ export const wgl019: PostSynthCheck = {
14
+ id: "WGL019",
15
+ description: "Missing retry — deploy jobs without retry strategy",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [entityName, entity] of ctx.entities) {
21
+ if (isPropertyDeclarable(entity)) continue;
22
+ const entityType = (entity as Record<string, unknown>).entityType as string;
23
+ if (entityType !== "GitLab::CI::Job") continue;
24
+
25
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
26
+ if (!props) continue;
27
+
28
+ const stage = props.stage as string | undefined;
29
+ if (!stage || !DEPLOY_STAGES.has(stage.toLowerCase())) continue;
30
+
31
+ if (!props.retry) {
32
+ diagnostics.push({
33
+ checkId: "WGL019",
34
+ severity: "info",
35
+ message: `Deploy job "${entityName}" (stage: ${stage}) has no retry strategy — consider adding retry for transient failures`,
36
+ entity: entityName,
37
+ lexicon: "gitlab",
38
+ });
39
+ }
40
+ }
41
+
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * WGL020: Duplicate Job Names
3
+ *
4
+ * Detects multiple jobs that resolve to the same kebab-case name in
5
+ * the serialized YAML. GitLab silently merges duplicate keys, which
6
+ * causes unexpected behavior.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, extractJobs } from "./yaml-helpers";
11
+
12
+ export function checkDuplicateJobNames(yaml: string): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ // Count occurrences of each top-level key (raw line parsing, not extractJobs,
16
+ // to detect actual YAML key duplication)
17
+ const keyCounts = new Map<string, number>();
18
+ const lines = yaml.split("\n");
19
+
20
+ for (const line of lines) {
21
+ const topMatch = line.match(/^(\.?[a-z][a-z0-9_.-]*):/);
22
+ if (topMatch) {
23
+ const name = topMatch[1];
24
+ if (["stages", "default", "workflow", "variables", "include"].includes(name)) continue;
25
+ keyCounts.set(name, (keyCounts.get(name) ?? 0) + 1);
26
+ }
27
+ }
28
+
29
+ for (const [name, count] of keyCounts) {
30
+ if (count > 1) {
31
+ diagnostics.push({
32
+ checkId: "WGL020",
33
+ severity: "error",
34
+ message: `Duplicate job name "${name}" appears ${count} times — GitLab will silently merge these`,
35
+ entity: name,
36
+ lexicon: "gitlab",
37
+ });
38
+ }
39
+ }
40
+
41
+ return diagnostics;
42
+ }
43
+
44
+ export const wgl020: PostSynthCheck = {
45
+ id: "WGL020",
46
+ description: "Duplicate job names — multiple jobs resolving to same name",
47
+
48
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
49
+ const diagnostics: PostSynthDiagnostic[] = [];
50
+ for (const [, output] of ctx.outputs) {
51
+ const yaml = getPrimaryOutput(output);
52
+ diagnostics.push(...checkDuplicateJobNames(yaml));
53
+ }
54
+ return diagnostics;
55
+ },
56
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WGL021: Unused Variables
3
+ *
4
+ * Detects global `variables:` that are not referenced by any job script.
5
+ * Unused variables add noise and may indicate stale configuration.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput, extractGlobalVariables } from "./yaml-helpers";
10
+
11
+ export function checkUnusedVariables(yaml: string): PostSynthDiagnostic[] {
12
+ const diagnostics: PostSynthDiagnostic[] = [];
13
+
14
+ const globalVars = extractGlobalVariables(yaml);
15
+ if (globalVars.size === 0) return diagnostics;
16
+
17
+ // Get the rest of the YAML (everything after the global variables block)
18
+ // to search for references
19
+ for (const [varName] of globalVars) {
20
+ // Check if $VARNAME or ${VARNAME} appears anywhere in the YAML (outside the variables block)
21
+ const refPattern = new RegExp(`\\$\\{?${varName}\\}?`);
22
+ // Also check for uses in extends, needs, etc. — search all sections
23
+ const sections = yaml.split("\n\n");
24
+ let found = false;
25
+
26
+ for (const section of sections) {
27
+ // Skip the global variables section itself
28
+ if (section.trimStart().startsWith("variables:")) continue;
29
+
30
+ if (refPattern.test(section)) {
31
+ found = true;
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (!found) {
37
+ diagnostics.push({
38
+ checkId: "WGL021",
39
+ severity: "warning",
40
+ message: `Global variable "${varName}" is not referenced in any job script`,
41
+ entity: varName,
42
+ lexicon: "gitlab",
43
+ });
44
+ }
45
+ }
46
+
47
+ return diagnostics;
48
+ }
49
+
50
+ export const wgl021: PostSynthCheck = {
51
+ id: "WGL021",
52
+ description: "Unused variables — global variables not referenced by any job",
53
+
54
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
55
+ const diagnostics: PostSynthDiagnostic[] = [];
56
+ for (const [, output] of ctx.outputs) {
57
+ const yaml = getPrimaryOutput(output);
58
+ diagnostics.push(...checkUnusedVariables(yaml));
59
+ }
60
+ return diagnostics;
61
+ },
62
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WGL022: Missing Artifacts Expiry
3
+ *
4
+ * Warns about `artifacts:` without `expire_in:`, which causes disk bloat
5
+ * on the GitLab instance. Default retention is "never expire" in some
6
+ * GitLab configurations.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+
12
+ export const wgl022: PostSynthCheck = {
13
+ id: "WGL022",
14
+ description: "Missing artifacts expiry — artifacts without expire_in cause disk bloat",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [entityName, entity] of ctx.entities) {
20
+ if (isPropertyDeclarable(entity)) continue;
21
+ const entityType = (entity as Record<string, unknown>).entityType as string;
22
+ if (entityType !== "GitLab::CI::Job") continue;
23
+
24
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
25
+ if (!props?.artifacts) continue;
26
+
27
+ const artifacts = props.artifacts as Record<string, unknown>;
28
+ // artifacts might be a Declarable with its own props
29
+ const artProps = (artifacts.props as Record<string, unknown> | undefined) ?? artifacts;
30
+
31
+ if (!artProps.expire_in && !artProps.expireIn) {
32
+ diagnostics.push({
33
+ checkId: "WGL022",
34
+ severity: "warning",
35
+ message: `Job "${entityName}" has artifacts without expire_in — set an expiry to avoid disk bloat`,
36
+ entity: entityName,
37
+ lexicon: "gitlab",
38
+ });
39
+ }
40
+ }
41
+
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WGL023: Overly Broad Rules
3
+ *
4
+ * Flags jobs with a single rule that has only `when: always` and no
5
+ * conditions (no `if:`, `changes:`, etc.). This effectively disables
6
+ * all pipeline filtering for the job, which is usually unintended.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+
12
+ export const wgl023: PostSynthCheck = {
13
+ id: "WGL023",
14
+ description: "Overly broad rules — job with only when: always rule (no conditions)",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [entityName, entity] of ctx.entities) {
20
+ if (isPropertyDeclarable(entity)) continue;
21
+ const entityType = (entity as Record<string, unknown>).entityType as string;
22
+ if (entityType !== "GitLab::CI::Job") continue;
23
+
24
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
25
+ if (!props?.rules || !Array.isArray(props.rules)) continue;
26
+
27
+ const rules = props.rules as Array<Record<string, unknown>>;
28
+ if (rules.length !== 1) continue;
29
+
30
+ const rule = rules[0];
31
+ const ruleProps = (rule.props as Record<string, unknown> | undefined) ?? rule;
32
+
33
+ const when = ruleProps.when;
34
+ const hasIf = !!ruleProps.if;
35
+ const hasChanges = !!ruleProps.changes;
36
+ const hasExists = !!ruleProps.exists;
37
+
38
+ if (when === "always" && !hasIf && !hasChanges && !hasExists) {
39
+ diagnostics.push({
40
+ checkId: "WGL023",
41
+ severity: "info",
42
+ message: `Job "${entityName}" has a single rule with only "when: always" — this disables all pipeline filtering. Consider adding conditions or removing rules entirely.`,
43
+ entity: entityName,
44
+ lexicon: "gitlab",
45
+ });
46
+ }
47
+ }
48
+
49
+ return diagnostics;
50
+ },
51
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * WGL024: Manual Without allow_failure
3
+ *
4
+ * Warns about jobs with `when: manual` that don't set `allow_failure: true`.
5
+ * Without it, the manual job blocks the pipeline from progressing past
6
+ * its stage until someone manually triggers it.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+
12
+ export const wgl024: PostSynthCheck = {
13
+ id: "WGL024",
14
+ description: "Manual without allow_failure — manual jobs block pipeline without allow_failure: true",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [entityName, entity] of ctx.entities) {
20
+ if (isPropertyDeclarable(entity)) continue;
21
+ const entityType = (entity as Record<string, unknown>).entityType as string;
22
+ if (entityType !== "GitLab::CI::Job") continue;
23
+
24
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
25
+ if (!props) continue;
26
+
27
+ // Check top-level when: manual
28
+ const isManual = props.when === "manual";
29
+ if (!isManual) continue;
30
+
31
+ // Check allow_failure
32
+ const allowFailure = props.allow_failure ?? props.allowFailure;
33
+ if (allowFailure !== true) {
34
+ diagnostics.push({
35
+ checkId: "WGL024",
36
+ severity: "warning",
37
+ message: `Job "${entityName}" has when: manual but no allow_failure: true — this will block the pipeline`,
38
+ entity: entityName,
39
+ lexicon: "gitlab",
40
+ });
41
+ }
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WGL025: Missing Cache Key
3
+ *
4
+ * Warns about `cache:` without `key:`. Without an explicit key, GitLab
5
+ * uses `default` as the key, which causes cache collisions between
6
+ * unrelated jobs sharing the same runner.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+
12
+ export const wgl025: PostSynthCheck = {
13
+ id: "WGL025",
14
+ description: "Missing cache key — cache without key causes collisions",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [entityName, entity] of ctx.entities) {
20
+ if (isPropertyDeclarable(entity)) continue;
21
+ const entityType = (entity as Record<string, unknown>).entityType as string;
22
+ if (entityType !== "GitLab::CI::Job") continue;
23
+
24
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
25
+ if (!props?.cache) continue;
26
+
27
+ // Cache can be a single object or an array
28
+ const caches = Array.isArray(props.cache) ? props.cache : [props.cache];
29
+
30
+ for (const cache of caches) {
31
+ const cacheObj = cache as Record<string, unknown>;
32
+ const cacheProps = (cacheObj.props as Record<string, unknown> | undefined) ?? cacheObj;
33
+
34
+ if (!cacheProps.key) {
35
+ diagnostics.push({
36
+ checkId: "WGL025",
37
+ severity: "warning",
38
+ message: `Job "${entityName}" has cache without a key — this causes cache collisions between jobs`,
39
+ entity: entityName,
40
+ lexicon: "gitlab",
41
+ });
42
+ break; // One diagnostic per job is enough
43
+ }
44
+ }
45
+ }
46
+
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * WGL026: Privileged Services Without TLS
3
+ *
4
+ * Warns about Docker-in-Docker (DinD) services that don't set
5
+ * DOCKER_TLS_CERTDIR. Running DinD without TLS exposes the Docker
6
+ * daemon on an unencrypted socket.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+
12
+ const DIND_IMAGES = ["docker:dind", "docker:stable-dind"];
13
+
14
+ function isDindImage(image: string): boolean {
15
+ return DIND_IMAGES.some((dind) => image.includes(dind));
16
+ }
17
+
18
+ export const wgl026: PostSynthCheck = {
19
+ id: "WGL026",
20
+ description: "Privileged services without TLS — DinD services without DOCKER_TLS_CERTDIR",
21
+
22
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
23
+ const diagnostics: PostSynthDiagnostic[] = [];
24
+
25
+ for (const [entityName, entity] of ctx.entities) {
26
+ if (isPropertyDeclarable(entity)) continue;
27
+ const entityType = (entity as Record<string, unknown>).entityType as string;
28
+ if (entityType !== "GitLab::CI::Job") continue;
29
+
30
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
31
+ if (!props?.services || !Array.isArray(props.services)) continue;
32
+
33
+ for (const service of props.services) {
34
+ let imageName: string | undefined;
35
+ let serviceVars: Record<string, unknown> | undefined;
36
+
37
+ if (typeof service === "string") {
38
+ imageName = service;
39
+ } else if (typeof service === "object" && service !== null) {
40
+ const svc = service as Record<string, unknown>;
41
+ const svcProps = (svc.props as Record<string, unknown> | undefined) ?? svc;
42
+ imageName = svcProps.name as string | undefined;
43
+ serviceVars = svcProps.variables as Record<string, unknown> | undefined;
44
+ }
45
+
46
+ if (!imageName || !isDindImage(imageName)) continue;
47
+
48
+ // Check if DOCKER_TLS_CERTDIR is set in service variables or job variables
49
+ const jobVars = props.variables as Record<string, unknown> | undefined;
50
+ const hasTLS = serviceVars?.DOCKER_TLS_CERTDIR !== undefined ||
51
+ jobVars?.DOCKER_TLS_CERTDIR !== undefined;
52
+
53
+ if (!hasTLS) {
54
+ diagnostics.push({
55
+ checkId: "WGL026",
56
+ severity: "warning",
57
+ message: `Job "${entityName}" uses DinD service without DOCKER_TLS_CERTDIR — the Docker daemon will be unencrypted`,
58
+ entity: entityName,
59
+ lexicon: "gitlab",
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ return diagnostics;
66
+ },
67
+ };