@intentius/chant-lexicon-gitlab 0.124.31 → 0.124.32

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 (133) hide show
  1. package/dist/codegen/docs-cli.js +2 -0
  2. package/dist/codegen/docs.js +157 -0
  3. package/dist/codegen/fetch.js +93 -0
  4. package/dist/codegen/generate-cli.js +30 -0
  5. package/dist/codegen/generate-lexicon.js +36 -0
  6. package/dist/codegen/generate-typescript.js +105 -0
  7. package/dist/codegen/generate.js +125 -0
  8. package/dist/codegen/naming.js +48 -0
  9. package/dist/codegen/package.js +41 -0
  10. package/dist/codegen/parse.js +539 -0
  11. package/dist/codegen/patches.js +84 -0
  12. package/dist/codegen/typecheck.js +4 -0
  13. package/dist/components/ci-jobs.js +16 -0
  14. package/dist/components/generate-op-pipeline.js +288 -0
  15. package/dist/components/generate-op-waves-pipeline.js +125 -0
  16. package/dist/components/generate-pipeline.d.ts.map +1 -1
  17. package/dist/components/generate-pipeline.js +369 -0
  18. package/dist/composites/catalog.js +418 -0
  19. package/dist/composites/docker-build.js +38 -0
  20. package/dist/composites/index.js +5 -0
  21. package/dist/composites/mr-plan-report.js +42 -0
  22. package/dist/composites/node-pipeline.js +67 -0
  23. package/dist/composites/python-pipeline.js +45 -0
  24. package/dist/composites/review-app.js +32 -0
  25. package/dist/coverage.js +35 -0
  26. package/dist/generated/index.js +25 -0
  27. package/dist/generated/runtime.js +4 -0
  28. package/dist/graph-edges.js +202 -0
  29. package/dist/import/generator.js +196 -0
  30. package/dist/import/parser.js +111 -0
  31. package/dist/index.js +26 -0
  32. package/dist/integrity.json +3 -3
  33. package/dist/intrinsics.js +33 -0
  34. package/dist/lint/audit-catalog.js +53 -0
  35. package/dist/lint/audit-lineage.js +64 -0
  36. package/dist/lint/post-synth/index.js +84 -0
  37. package/dist/lint/post-synth/wgl010.js +34 -0
  38. package/dist/lint/post-synth/wgl011.js +48 -0
  39. package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
  40. package/dist/lint/post-synth/wgl012.js +68 -0
  41. package/dist/lint/post-synth/wgl013.js +57 -0
  42. package/dist/lint/post-synth/wgl014.js +43 -0
  43. package/dist/lint/post-synth/wgl015.js +70 -0
  44. package/dist/lint/post-synth/wgl016.js +70 -0
  45. package/dist/lint/post-synth/wgl017.js +38 -0
  46. package/dist/lint/post-synth/wgl018.js +34 -0
  47. package/dist/lint/post-synth/wgl019.js +43 -0
  48. package/dist/lint/post-synth/wgl020.js +47 -0
  49. package/dist/lint/post-synth/wgl021.js +52 -0
  50. package/dist/lint/post-synth/wgl022.js +38 -0
  51. package/dist/lint/post-synth/wgl023.js +44 -0
  52. package/dist/lint/post-synth/wgl024.js +41 -0
  53. package/dist/lint/post-synth/wgl025.js +42 -0
  54. package/dist/lint/post-synth/wgl026.js +58 -0
  55. package/dist/lint/post-synth/wgl027.js +49 -0
  56. package/dist/lint/post-synth/wgl028.js +58 -0
  57. package/dist/lint/post-synth/wgl029.js +55 -0
  58. package/dist/lint/post-synth/wgl030.js +43 -0
  59. package/dist/lint/post-synth/wgl031.js +33 -0
  60. package/dist/lint/post-synth/wgl032.js +64 -0
  61. package/dist/lint/post-synth/wgl033.js +32 -0
  62. package/dist/lint/post-synth/wgl034.js +37 -0
  63. package/dist/lint/post-synth/wgl035.js +40 -0
  64. package/dist/lint/post-synth/wgl036.js +38 -0
  65. package/dist/lint/post-synth/wgl037.js +41 -0
  66. package/dist/lint/post-synth/wgl038.js +43 -0
  67. package/dist/lint/post-synth/wgl039.js +34 -0
  68. package/dist/lint/post-synth/wgl040.js +35 -0
  69. package/dist/lint/post-synth/wgl041.js +53 -0
  70. package/dist/lint/post-synth/wgl042.js +41 -0
  71. package/dist/lint/post-synth/wgl043.js +39 -0
  72. package/dist/lint/post-synth/wgl044.js +35 -0
  73. package/dist/lint/post-synth/wgl045.js +54 -0
  74. package/dist/lint/post-synth/wgl046.js +40 -0
  75. package/dist/lint/post-synth/wgl047.js +34 -0
  76. package/dist/lint/post-synth/wgl048.js +32 -0
  77. package/dist/lint/post-synth/wgl049.js +53 -0
  78. package/dist/lint/post-synth/wgl050.js +42 -0
  79. package/dist/lint/post-synth/yaml-helpers.js +432 -0
  80. package/dist/lint/rules/artifact-no-expiry.js +53 -0
  81. package/dist/lint/rules/data/known-include-sources.js +44 -0
  82. package/dist/lint/rules/data/untrusted-variables.js +30 -0
  83. package/dist/lint/rules/deprecated-only-except.js +46 -0
  84. package/dist/lint/rules/import-source.js +61 -0
  85. package/dist/lint/rules/index.js +7 -0
  86. package/dist/lint/rules/missing-script.js +67 -0
  87. package/dist/lint/rules/missing-stage.js +60 -0
  88. package/dist/lsp/completions.d.ts.map +1 -1
  89. package/dist/lsp/completions.js +18 -0
  90. package/dist/lsp/hover.d.ts.map +1 -1
  91. package/dist/lsp/hover.js +33 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/mcp/context-tools.js +262 -0
  94. package/dist/migrate/from-github/actions/index.js +17 -0
  95. package/dist/migrate/from-github/actions/registry.js +61 -0
  96. package/dist/migrate/from-github/actions/tier-1.js +298 -0
  97. package/dist/migrate/from-github/actions/tier-2.js +241 -0
  98. package/dist/migrate/from-github/actions/tier-3.js +96 -0
  99. package/dist/migrate/from-github/composites/patterns.js +151 -0
  100. package/dist/migrate/from-github/composites/rewriter.js +22 -0
  101. package/dist/migrate/from-github/diagnostics.js +53 -0
  102. package/dist/migrate/from-github/emit-yaml.js +121 -0
  103. package/dist/migrate/from-github/expressions.js +275 -0
  104. package/dist/migrate/from-github/index.js +96 -0
  105. package/dist/migrate/from-github/provenance.js +35 -0
  106. package/dist/migrate/from-github/rules.js +74 -0
  107. package/dist/migrate/from-github/security.js +189 -0
  108. package/dist/migrate/from-github/stages.js +156 -0
  109. package/dist/migrate/from-github/transformer.js +633 -0
  110. package/dist/op/activities/gitlab.js +38 -0
  111. package/dist/op/activities/index.js +10 -0
  112. package/dist/op/builders.js +26 -0
  113. package/dist/package-cli.js +21 -0
  114. package/dist/plugin.js +536 -0
  115. package/dist/rules/wgl012.ts +2 -2
  116. package/dist/serializer.js +197 -0
  117. package/dist/testdata/create-fixture.js +40 -0
  118. package/dist/testdata/load-fixtures.js +20 -0
  119. package/dist/validate-cli.js +16 -0
  120. package/dist/validate.js +29 -0
  121. package/dist/variables.js +30 -0
  122. package/package.json +8 -8
  123. package/src/codegen/fetch.ts +2 -2
  124. package/src/codegen/package.ts +2 -2
  125. package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
  126. package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
  127. package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
  128. package/src/components/generate-pipeline.ts +60 -27
  129. package/src/components/generate-pr-pipeline.test.ts +14 -9
  130. package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
  131. package/src/lint/post-synth/wgl012.ts +2 -2
  132. package/src/lsp/completions.ts +2 -1
  133. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,34 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
8
+ export const wgl018 = {
9
+ id: "WGL018",
10
+ description: "Missing timeout — jobs without explicit timeout may run too long",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ for (const [entityName, entity] of ctx.entities) {
14
+ if (isPropertyDeclarable(entity))
15
+ continue;
16
+ const entityType = entity.entityType;
17
+ if (entityType !== "GitLab::CI::Job")
18
+ continue;
19
+ const props = entity.props;
20
+ if (!props)
21
+ continue;
22
+ if (!props.timeout) {
23
+ diagnostics.push({
24
+ checkId: "WGL018",
25
+ severity: "warning",
26
+ message: `Job "${entityName}" has no explicit timeout — default is 1 hour which may be too long`,
27
+ entity: entityName,
28
+ lexicon: "gitlab",
29
+ });
30
+ }
31
+ }
32
+ return diagnostics;
33
+ },
34
+ };
@@ -0,0 +1,43 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
8
+ const DEPLOY_STAGES = new Set(["deploy", "deployment", "release", "production", "staging"]);
9
+ export const wgl019 = {
10
+ id: "WGL019",
11
+ description: "Missing retry — deploy jobs without retry strategy",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const [entityName, entity] of ctx.entities) {
15
+ if (isPropertyDeclarable(entity))
16
+ continue;
17
+ const entityType = entity.entityType;
18
+ if (entityType !== "GitLab::CI::Job")
19
+ continue;
20
+ const props = entity.props;
21
+ if (!props)
22
+ continue;
23
+ const stage = props.stage;
24
+ if (!stage || !DEPLOY_STAGES.has(stage.toLowerCase()))
25
+ continue;
26
+ // chant #1544 — `!props.retry` treated an explicit `retry: 0` the same
27
+ // as no `retry` at all, warning on a job that stated its policy
28
+ // ("retrying a deploy without a human is deliberate") as if it hadn't.
29
+ // `retry: 0` is falsy but present — only an actually-absent key means
30
+ // "no retry strategy".
31
+ if (props.retry === undefined) {
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
+ return diagnostics;
42
+ },
43
+ };
@@ -0,0 +1,47 @@
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
+ import { getPrimaryOutput, topLevelKey, RESERVED_TOP_LEVEL_KEYS } from "./yaml-helpers.js";
9
+ export function checkDuplicateJobNames(yaml) {
10
+ const diagnostics = [];
11
+ // Count occurrences of each top-level key (raw line parsing, not extractJobs,
12
+ // to detect actual YAML key duplication)
13
+ const keyCounts = new Map();
14
+ const lines = yaml.split("\n");
15
+ for (const line of lines) {
16
+ const name = topLevelKey(line);
17
+ if (name !== undefined) {
18
+ if (RESERVED_TOP_LEVEL_KEYS.has(name))
19
+ continue;
20
+ keyCounts.set(name, (keyCounts.get(name) ?? 0) + 1);
21
+ }
22
+ }
23
+ for (const [name, count] of keyCounts) {
24
+ if (count > 1) {
25
+ diagnostics.push({
26
+ checkId: "WGL020",
27
+ severity: "error",
28
+ message: `Duplicate job name "${name}" appears ${count} times — GitLab will silently merge these`,
29
+ entity: name,
30
+ lexicon: "gitlab",
31
+ });
32
+ }
33
+ }
34
+ return diagnostics;
35
+ }
36
+ export const wgl020 = {
37
+ id: "WGL020",
38
+ description: "Duplicate job names — multiple jobs resolving to same name",
39
+ check(ctx) {
40
+ const diagnostics = [];
41
+ for (const [, output] of ctx.outputs) {
42
+ const yaml = getPrimaryOutput(output);
43
+ diagnostics.push(...checkDuplicateJobNames(yaml));
44
+ }
45
+ return diagnostics;
46
+ },
47
+ };
@@ -0,0 +1,52 @@
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
+ import { getPrimaryOutput, extractGlobalVariables, topLevelSections } from "./yaml-helpers.js";
8
+ export function checkUnusedVariables(yaml) {
9
+ const diagnostics = [];
10
+ const globalVars = extractGlobalVariables(yaml);
11
+ if (globalVars.size === 0)
12
+ return diagnostics;
13
+ // Get the rest of the YAML (everything after the global variables block)
14
+ // to search for references
15
+ for (const [varName] of globalVars) {
16
+ // Check if $VARNAME or ${VARNAME} appears anywhere in the YAML (outside the variables block)
17
+ const refPattern = new RegExp(`\\$\\{?${varName}\\}?`);
18
+ // Also check for uses in extends, needs, etc. — search all sections
19
+ let found = false;
20
+ for (const { key, text } of topLevelSections(yaml)) {
21
+ // Skip the global variables section itself
22
+ if (key === "variables")
23
+ continue;
24
+ if (refPattern.test(text)) {
25
+ found = true;
26
+ break;
27
+ }
28
+ }
29
+ if (!found) {
30
+ diagnostics.push({
31
+ checkId: "WGL021",
32
+ severity: "warning",
33
+ message: `Global variable "${varName}" is not referenced in any job script`,
34
+ entity: varName,
35
+ lexicon: "gitlab",
36
+ });
37
+ }
38
+ }
39
+ return diagnostics;
40
+ }
41
+ export const wgl021 = {
42
+ id: "WGL021",
43
+ description: "Unused variables — global variables not referenced by any job",
44
+ check(ctx) {
45
+ const diagnostics = [];
46
+ for (const [, output] of ctx.outputs) {
47
+ const yaml = getPrimaryOutput(output);
48
+ diagnostics.push(...checkUnusedVariables(yaml));
49
+ }
50
+ return diagnostics;
51
+ },
52
+ };
@@ -0,0 +1,38 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
9
+ export const wgl022 = {
10
+ id: "WGL022",
11
+ description: "Missing artifacts expiry — artifacts without expire_in cause disk bloat",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const [entityName, entity] of ctx.entities) {
15
+ if (isPropertyDeclarable(entity))
16
+ continue;
17
+ const entityType = entity.entityType;
18
+ if (entityType !== "GitLab::CI::Job")
19
+ continue;
20
+ const props = entity.props;
21
+ if (!props?.artifacts)
22
+ continue;
23
+ const artifacts = props.artifacts;
24
+ // artifacts might be a Declarable with its own props
25
+ const artProps = artifacts.props ?? artifacts;
26
+ if (!artProps.expire_in && !artProps.expireIn) {
27
+ diagnostics.push({
28
+ checkId: "WGL022",
29
+ severity: "warning",
30
+ message: `Job "${entityName}" has artifacts without expire_in — set an expiry to avoid disk bloat`,
31
+ entity: entityName,
32
+ lexicon: "gitlab",
33
+ });
34
+ }
35
+ }
36
+ return diagnostics;
37
+ },
38
+ };
@@ -0,0 +1,44 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
9
+ export const wgl023 = {
10
+ id: "WGL023",
11
+ description: "Overly broad rules — job with only when: always rule (no conditions)",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const [entityName, entity] of ctx.entities) {
15
+ if (isPropertyDeclarable(entity))
16
+ continue;
17
+ const entityType = entity.entityType;
18
+ if (entityType !== "GitLab::CI::Job")
19
+ continue;
20
+ const props = entity.props;
21
+ if (!props?.rules || !Array.isArray(props.rules))
22
+ continue;
23
+ const rules = props.rules;
24
+ if (rules.length !== 1)
25
+ continue;
26
+ const rule = rules[0];
27
+ const ruleProps = rule.props ?? rule;
28
+ const when = ruleProps.when;
29
+ const hasIf = !!ruleProps.if;
30
+ const hasChanges = !!ruleProps.changes;
31
+ const hasExists = !!ruleProps.exists;
32
+ if (when === "always" && !hasIf && !hasChanges && !hasExists) {
33
+ diagnostics.push({
34
+ checkId: "WGL023",
35
+ severity: "info",
36
+ message: `Job "${entityName}" has a single rule with only "when: always" — this disables all pipeline filtering. Consider adding conditions or removing rules entirely.`,
37
+ entity: entityName,
38
+ lexicon: "gitlab",
39
+ });
40
+ }
41
+ }
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,41 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
9
+ export const wgl024 = {
10
+ id: "WGL024",
11
+ description: "Manual without allow_failure — manual jobs block pipeline without allow_failure: true",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const [entityName, entity] of ctx.entities) {
15
+ if (isPropertyDeclarable(entity))
16
+ continue;
17
+ const entityType = entity.entityType;
18
+ if (entityType !== "GitLab::CI::Job")
19
+ continue;
20
+ const props = entity.props;
21
+ if (!props)
22
+ continue;
23
+ // Check top-level when: manual
24
+ const isManual = props.when === "manual";
25
+ if (!isManual)
26
+ continue;
27
+ // Check allow_failure
28
+ const allowFailure = props.allow_failure ?? props.allowFailure;
29
+ if (allowFailure !== true) {
30
+ diagnostics.push({
31
+ checkId: "WGL024",
32
+ severity: "warning",
33
+ message: `Job "${entityName}" has when: manual but no allow_failure: true — this will block the pipeline`,
34
+ entity: entityName,
35
+ lexicon: "gitlab",
36
+ });
37
+ }
38
+ }
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,42 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
9
+ export const wgl025 = {
10
+ id: "WGL025",
11
+ description: "Missing cache key — cache without key causes collisions",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const [entityName, entity] of ctx.entities) {
15
+ if (isPropertyDeclarable(entity))
16
+ continue;
17
+ const entityType = entity.entityType;
18
+ if (entityType !== "GitLab::CI::Job")
19
+ continue;
20
+ const props = entity.props;
21
+ if (!props?.cache)
22
+ continue;
23
+ // Cache can be a single object or an array
24
+ const caches = Array.isArray(props.cache) ? props.cache : [props.cache];
25
+ for (const cache of caches) {
26
+ const cacheObj = cache;
27
+ const cacheProps = cacheObj.props ?? cacheObj;
28
+ if (!cacheProps.key) {
29
+ diagnostics.push({
30
+ checkId: "WGL025",
31
+ severity: "warning",
32
+ message: `Job "${entityName}" has cache without a key — this causes cache collisions between jobs`,
33
+ entity: entityName,
34
+ lexicon: "gitlab",
35
+ });
36
+ break; // One diagnostic per job is enough
37
+ }
38
+ }
39
+ }
40
+ return diagnostics;
41
+ },
42
+ };
@@ -0,0 +1,58 @@
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
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
9
+ const DIND_IMAGES = ["docker:dind", "docker:stable-dind"];
10
+ function isDindImage(image) {
11
+ return DIND_IMAGES.some((dind) => image.includes(dind));
12
+ }
13
+ export const wgl026 = {
14
+ id: "WGL026",
15
+ description: "Privileged services without TLS — DinD services without DOCKER_TLS_CERTDIR",
16
+ check(ctx) {
17
+ const diagnostics = [];
18
+ for (const [entityName, entity] of ctx.entities) {
19
+ if (isPropertyDeclarable(entity))
20
+ continue;
21
+ const entityType = entity.entityType;
22
+ if (entityType !== "GitLab::CI::Job")
23
+ continue;
24
+ const props = entity.props;
25
+ if (!props?.services || !Array.isArray(props.services))
26
+ continue;
27
+ for (const service of props.services) {
28
+ let imageName;
29
+ let serviceVars;
30
+ if (typeof service === "string") {
31
+ imageName = service;
32
+ }
33
+ else if (typeof service === "object" && service !== null) {
34
+ const svc = service;
35
+ const svcProps = svc.props ?? svc;
36
+ imageName = svcProps.name;
37
+ serviceVars = svcProps.variables;
38
+ }
39
+ if (!imageName || !isDindImage(imageName))
40
+ continue;
41
+ // Check if DOCKER_TLS_CERTDIR is set in service variables or job variables
42
+ const jobVars = props.variables;
43
+ const hasTLS = serviceVars?.DOCKER_TLS_CERTDIR !== undefined ||
44
+ jobVars?.DOCKER_TLS_CERTDIR !== undefined;
45
+ if (!hasTLS) {
46
+ diagnostics.push({
47
+ checkId: "WGL026",
48
+ severity: "warning",
49
+ message: `Job "${entityName}" uses DinD service without DOCKER_TLS_CERTDIR — the Docker daemon will be unencrypted`,
50
+ entity: entityName,
51
+ lexicon: "gitlab",
52
+ });
53
+ }
54
+ }
55
+ }
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WGL027: Empty Script
3
+ *
4
+ * Detects jobs with `script: []` or scripts containing only empty strings.
5
+ * GitLab rejects jobs with empty scripts at pipeline validation time.
6
+ */
7
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
8
+ export const wgl027 = {
9
+ id: "WGL027",
10
+ description: "Empty script — jobs with empty or blank script entries",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ for (const [entityName, entity] of ctx.entities) {
14
+ if (isPropertyDeclarable(entity))
15
+ continue;
16
+ const entityType = entity.entityType;
17
+ if (entityType !== "GitLab::CI::Job")
18
+ continue;
19
+ const props = entity.props;
20
+ if (!props)
21
+ continue;
22
+ const script = props.script;
23
+ if (script === undefined || script === null)
24
+ continue;
25
+ let isEmpty = false;
26
+ if (Array.isArray(script)) {
27
+ if (script.length === 0) {
28
+ isEmpty = true;
29
+ }
30
+ else if (script.every((s) => typeof s === "string" && s.trim() === "")) {
31
+ isEmpty = true;
32
+ }
33
+ }
34
+ else if (typeof script === "string" && script.trim() === "") {
35
+ isEmpty = true;
36
+ }
37
+ if (isEmpty) {
38
+ diagnostics.push({
39
+ checkId: "WGL027",
40
+ severity: "error",
41
+ message: `Job "${entityName}" has an empty script — GitLab will reject this pipeline`,
42
+ entity: entityName,
43
+ lexicon: "gitlab",
44
+ });
45
+ }
46
+ }
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,58 @@
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
+ import { getPrimaryOutput, extractStages, extractJobs } from "./yaml-helpers.js";
9
+ export function checkRedundantNeeds(yaml) {
10
+ const diagnostics = [];
11
+ const stages = extractStages(yaml);
12
+ if (stages.length === 0)
13
+ return diagnostics;
14
+ const stageIndex = new Map();
15
+ for (let i = 0; i < stages.length; i++) {
16
+ stageIndex.set(stages[i], i);
17
+ }
18
+ const jobs = extractJobs(yaml);
19
+ for (const [jobName, job] of jobs) {
20
+ if (!job.needs || !job.stage)
21
+ continue;
22
+ const jobStageIdx = stageIndex.get(job.stage);
23
+ if (jobStageIdx === undefined)
24
+ continue;
25
+ for (const need of job.needs) {
26
+ const neededJob = jobs.get(need);
27
+ if (!neededJob?.stage)
28
+ continue;
29
+ const needStageIdx = stageIndex.get(neededJob.stage);
30
+ if (needStageIdx === undefined)
31
+ continue;
32
+ // If the needed job is in an earlier stage, it's already implied
33
+ // by GitLab's default stage-based ordering
34
+ if (needStageIdx < jobStageIdx) {
35
+ diagnostics.push({
36
+ checkId: "WGL028",
37
+ severity: "info",
38
+ message: `Job "${jobName}" lists "${need}" in needs: but it's already in an earlier stage (${neededJob.stage} → ${job.stage})`,
39
+ entity: jobName,
40
+ lexicon: "gitlab",
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return diagnostics;
46
+ }
47
+ export const wgl028 = {
48
+ id: "WGL028",
49
+ description: "Redundant needs — needs listing jobs already implied by stage ordering",
50
+ check(ctx) {
51
+ const diagnostics = [];
52
+ for (const [, output] of ctx.outputs) {
53
+ const yaml = getPrimaryOutput(output);
54
+ diagnostics.push(...checkRedundantNeeds(yaml));
55
+ }
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * WGL029: Unpinned include:project / component Reference
3
+ *
4
+ * Flags an `include:project` or CI/CD `component:` resolved by a moving ref — a
5
+ * branch, no `ref:` at all (defaults to the project's default branch), or a
6
+ * floating component version — instead of a pinned tag or commit SHA. A
7
+ * repointed ref turns the included configuration into a supply-chain entry point.
8
+ */
9
+ import { getPrimaryOutput, extractIncludes, isPinnedRef } from "./yaml-helpers.js";
10
+ export const wgl029 = {
11
+ id: "WGL029",
12
+ description: "include:project / component resolved by a moving ref instead of a pinned tag or SHA",
13
+ check(ctx) {
14
+ const diagnostics = [];
15
+ for (const [, output] of ctx.outputs) {
16
+ const yaml = getPrimaryOutput(output);
17
+ for (const entry of extractIncludes(yaml)) {
18
+ if (entry.kind === "project") {
19
+ if (!entry.ref) {
20
+ diagnostics.push({
21
+ checkId: "WGL029",
22
+ severity: "warning",
23
+ message: `include:project "${entry.value}" has no ref: and resolves to the project's default branch (a moving target). Pin it to a tag or commit SHA.`,
24
+ entity: entry.value,
25
+ lexicon: "gitlab",
26
+ });
27
+ }
28
+ else if (!isPinnedRef(entry.ref)) {
29
+ diagnostics.push({
30
+ checkId: "WGL029",
31
+ severity: "warning",
32
+ message: `include:project "${entry.value}" is pinned to the moving ref "${entry.ref}". Pin it to a tag or commit SHA.`,
33
+ entity: entry.value,
34
+ lexicon: "gitlab",
35
+ });
36
+ }
37
+ }
38
+ else if (entry.kind === "component") {
39
+ const at = entry.value.lastIndexOf("@");
40
+ const version = at === -1 ? "" : entry.value.slice(at + 1);
41
+ if (!version || !isPinnedRef(version)) {
42
+ diagnostics.push({
43
+ checkId: "WGL029",
44
+ severity: "warning",
45
+ message: `component "${entry.value}" uses ${version ? `the moving version "${version}"` : "no pinned version"}. Pin it to a released version tag or commit SHA.`,
46
+ entity: entry.value,
47
+ lexicon: "gitlab",
48
+ });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return diagnostics;
54
+ },
55
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WGL030: Mutable or Insecure include:remote
3
+ *
4
+ * Flags `include:remote` URLs that are fetched over HTTP (no transport
5
+ * integrity) or over HTTPS but inherently mutable (a raw URL can be repointed
6
+ * at any time). Prefer `include:project` pinned to a tag/SHA, or a pinned
7
+ * component, over a remote URL. Generalizes WGL017 (non-HTTPS) to includes.
8
+ */
9
+ import { getPrimaryOutput, extractIncludes } from "./yaml-helpers.js";
10
+ export const wgl030 = {
11
+ id: "WGL030",
12
+ description: "include:remote is insecure (HTTP) or mutable",
13
+ check(ctx) {
14
+ const diagnostics = [];
15
+ for (const [, output] of ctx.outputs) {
16
+ const yaml = getPrimaryOutput(output);
17
+ for (const entry of extractIncludes(yaml)) {
18
+ const isRemote = entry.kind === "remote" || (entry.kind === "string" && /^https?:\/\//.test(entry.value));
19
+ if (!isRemote)
20
+ continue;
21
+ if (/^http:\/\//.test(entry.value)) {
22
+ diagnostics.push({
23
+ checkId: "WGL030",
24
+ severity: "error",
25
+ message: `include:remote "${entry.value}" is fetched over HTTP — an attacker on the network can swap the included config. Use HTTPS, and prefer a pinned include:project or component.`,
26
+ entity: entry.value,
27
+ lexicon: "gitlab",
28
+ });
29
+ }
30
+ else {
31
+ diagnostics.push({
32
+ checkId: "WGL030",
33
+ severity: "warning",
34
+ message: `include:remote "${entry.value}" is a mutable URL — its contents can change after review. Prefer include:project pinned to a tag/SHA, or a pinned component.`,
35
+ entity: entry.value,
36
+ lexicon: "gitlab",
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return diagnostics;
42
+ },
43
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * WGL031: Container Image Without an Immutable Digest
3
+ *
4
+ * Flags `image:` and `services:` references that are not pinned to an immutable
5
+ * `@sha256:` digest. A mutable tag can be repointed to a different image after
6
+ * review. Images built from a variable (e.g. `$CI_REGISTRY_IMAGE:tag`) are
7
+ * skipped — their concrete reference is not knowable from the static config.
8
+ */
9
+ import { getPrimaryOutput, extractImageRefs } from "./yaml-helpers.js";
10
+ export const wgl031 = {
11
+ id: "WGL031",
12
+ description: "Container image not pinned to an immutable digest",
13
+ check(ctx) {
14
+ const diagnostics = [];
15
+ for (const [, output] of ctx.outputs) {
16
+ const yaml = getPrimaryOutput(output);
17
+ for (const { job, image, source } of extractImageRefs(yaml)) {
18
+ if (image.includes("@sha256:"))
19
+ continue;
20
+ if (image.includes("$"))
21
+ continue; // variable-based reference — not statically knowable
22
+ diagnostics.push({
23
+ checkId: "WGL031",
24
+ severity: "warning",
25
+ message: `Job "${job}" ${source === "service" ? "service image" : "image"} "${image}" is not pinned to a digest — reference it by @sha256:... so the image cannot change after review.`,
26
+ entity: job,
27
+ lexicon: "gitlab",
28
+ });
29
+ }
30
+ }
31
+ return diagnostics;
32
+ },
33
+ };