@intentius/chant-lexicon-gitlab 0.124.31 → 0.124.33

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 (137) 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.d.ts.map +1 -1
  15. package/dist/components/generate-op-pipeline.js +294 -0
  16. package/dist/components/generate-op-waves-pipeline.js +125 -0
  17. package/dist/components/generate-pipeline.d.ts.map +1 -1
  18. package/dist/components/generate-pipeline.js +376 -0
  19. package/dist/composites/catalog.js +418 -0
  20. package/dist/composites/docker-build.js +38 -0
  21. package/dist/composites/index.js +5 -0
  22. package/dist/composites/mr-plan-report.js +42 -0
  23. package/dist/composites/node-pipeline.js +67 -0
  24. package/dist/composites/python-pipeline.js +45 -0
  25. package/dist/composites/review-app.js +32 -0
  26. package/dist/coverage.js +35 -0
  27. package/dist/generated/index.js +25 -0
  28. package/dist/generated/runtime.js +4 -0
  29. package/dist/graph-edges.js +202 -0
  30. package/dist/import/generator.js +196 -0
  31. package/dist/import/parser.js +111 -0
  32. package/dist/index.js +26 -0
  33. package/dist/integrity.json +3 -3
  34. package/dist/intrinsics.js +33 -0
  35. package/dist/lint/audit-catalog.js +53 -0
  36. package/dist/lint/audit-lineage.js +64 -0
  37. package/dist/lint/post-synth/index.js +84 -0
  38. package/dist/lint/post-synth/wgl010.js +34 -0
  39. package/dist/lint/post-synth/wgl011.js +48 -0
  40. package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
  41. package/dist/lint/post-synth/wgl012.js +68 -0
  42. package/dist/lint/post-synth/wgl013.js +57 -0
  43. package/dist/lint/post-synth/wgl014.js +43 -0
  44. package/dist/lint/post-synth/wgl015.js +70 -0
  45. package/dist/lint/post-synth/wgl016.js +70 -0
  46. package/dist/lint/post-synth/wgl017.js +38 -0
  47. package/dist/lint/post-synth/wgl018.js +34 -0
  48. package/dist/lint/post-synth/wgl019.js +43 -0
  49. package/dist/lint/post-synth/wgl020.js +47 -0
  50. package/dist/lint/post-synth/wgl021.js +52 -0
  51. package/dist/lint/post-synth/wgl022.js +38 -0
  52. package/dist/lint/post-synth/wgl023.js +44 -0
  53. package/dist/lint/post-synth/wgl024.js +41 -0
  54. package/dist/lint/post-synth/wgl025.js +42 -0
  55. package/dist/lint/post-synth/wgl026.js +58 -0
  56. package/dist/lint/post-synth/wgl027.js +49 -0
  57. package/dist/lint/post-synth/wgl028.js +58 -0
  58. package/dist/lint/post-synth/wgl029.js +55 -0
  59. package/dist/lint/post-synth/wgl030.js +43 -0
  60. package/dist/lint/post-synth/wgl031.js +33 -0
  61. package/dist/lint/post-synth/wgl032.js +64 -0
  62. package/dist/lint/post-synth/wgl033.js +32 -0
  63. package/dist/lint/post-synth/wgl034.js +37 -0
  64. package/dist/lint/post-synth/wgl035.js +40 -0
  65. package/dist/lint/post-synth/wgl036.js +38 -0
  66. package/dist/lint/post-synth/wgl037.js +41 -0
  67. package/dist/lint/post-synth/wgl038.js +43 -0
  68. package/dist/lint/post-synth/wgl039.js +34 -0
  69. package/dist/lint/post-synth/wgl040.js +35 -0
  70. package/dist/lint/post-synth/wgl041.js +53 -0
  71. package/dist/lint/post-synth/wgl042.js +41 -0
  72. package/dist/lint/post-synth/wgl043.js +39 -0
  73. package/dist/lint/post-synth/wgl044.js +35 -0
  74. package/dist/lint/post-synth/wgl045.js +54 -0
  75. package/dist/lint/post-synth/wgl046.js +40 -0
  76. package/dist/lint/post-synth/wgl047.js +34 -0
  77. package/dist/lint/post-synth/wgl048.js +32 -0
  78. package/dist/lint/post-synth/wgl049.js +53 -0
  79. package/dist/lint/post-synth/wgl050.js +42 -0
  80. package/dist/lint/post-synth/yaml-helpers.js +432 -0
  81. package/dist/lint/rules/artifact-no-expiry.js +53 -0
  82. package/dist/lint/rules/data/known-include-sources.js +44 -0
  83. package/dist/lint/rules/data/untrusted-variables.js +30 -0
  84. package/dist/lint/rules/deprecated-only-except.js +46 -0
  85. package/dist/lint/rules/import-source.js +61 -0
  86. package/dist/lint/rules/index.js +7 -0
  87. package/dist/lint/rules/missing-script.js +67 -0
  88. package/dist/lint/rules/missing-stage.js +60 -0
  89. package/dist/lsp/completions.d.ts.map +1 -1
  90. package/dist/lsp/completions.js +18 -0
  91. package/dist/lsp/hover.d.ts.map +1 -1
  92. package/dist/lsp/hover.js +33 -0
  93. package/dist/manifest.json +1 -1
  94. package/dist/mcp/context-tools.js +262 -0
  95. package/dist/migrate/from-github/actions/index.js +17 -0
  96. package/dist/migrate/from-github/actions/registry.js +61 -0
  97. package/dist/migrate/from-github/actions/tier-1.js +298 -0
  98. package/dist/migrate/from-github/actions/tier-2.js +241 -0
  99. package/dist/migrate/from-github/actions/tier-3.js +96 -0
  100. package/dist/migrate/from-github/composites/patterns.js +151 -0
  101. package/dist/migrate/from-github/composites/rewriter.js +22 -0
  102. package/dist/migrate/from-github/diagnostics.js +53 -0
  103. package/dist/migrate/from-github/emit-yaml.js +121 -0
  104. package/dist/migrate/from-github/expressions.js +275 -0
  105. package/dist/migrate/from-github/index.js +96 -0
  106. package/dist/migrate/from-github/provenance.js +35 -0
  107. package/dist/migrate/from-github/rules.js +74 -0
  108. package/dist/migrate/from-github/security.js +189 -0
  109. package/dist/migrate/from-github/stages.js +156 -0
  110. package/dist/migrate/from-github/transformer.js +633 -0
  111. package/dist/op/activities/gitlab.js +38 -0
  112. package/dist/op/activities/index.js +10 -0
  113. package/dist/op/builders.js +26 -0
  114. package/dist/package-cli.js +21 -0
  115. package/dist/plugin.js +536 -0
  116. package/dist/rules/wgl012.ts +2 -2
  117. package/dist/serializer.js +197 -0
  118. package/dist/testdata/create-fixture.js +40 -0
  119. package/dist/testdata/load-fixtures.js +20 -0
  120. package/dist/validate-cli.js +16 -0
  121. package/dist/validate.js +29 -0
  122. package/dist/variables.js +30 -0
  123. package/package.json +8 -8
  124. package/src/codegen/fetch.ts +2 -2
  125. package/src/codegen/package.ts +2 -2
  126. package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
  127. package/src/components/__fixtures__/pr-loop-record.gitlab.golden.yml +46 -0
  128. package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
  129. package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
  130. package/src/components/generate-op-pipeline.test.ts +10 -0
  131. package/src/components/generate-op-pipeline.ts +8 -1
  132. package/src/components/generate-pipeline.ts +71 -30
  133. package/src/components/generate-pr-pipeline.test.ts +61 -18
  134. package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
  135. package/src/lint/post-synth/wgl012.ts +2 -2
  136. package/src/lsp/completions.ts +2 -1
  137. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The gitlab lexicon's chant audit catalog — metadata for its post-synth rules
3
+ * (WGL GitLab CI rules). Contributed via gitlabPlugin.auditCatalog() (#687).
4
+ */
5
+ import { auditRule, GH_INJECTION, GH_OIDC, GH_PWN, GH_SECRETS, SCORECARD_PINNED, applyLineage } from "@intentius/chant/audit/catalog";
6
+ import { gitlabAuditLineage } from "./audit-lineage.js";
7
+ export const gitlabAuditCatalog = {
8
+ WGL010: auditRule("WGL010", "merge-worthy", "guidance", "Job references an undefined stage", "Add the stage to `stages:` or fix the job's `stage:`.", { category: "correctness" }),
9
+ WGL011: auditRule("WGL011", "merge-worthy", "guidance", "Job rules always evaluate to never", "Fix the `rules:` so the job can run; it is currently unreachable.", { category: "correctness" }),
10
+ WGL012: auditRule("WGL012", "report-only", "guidance", "Deprecated property", "Replace the deprecated GitLab CI property.", { category: "best-practice" }),
11
+ WGL013: auditRule("WGL013", "merge-worthy", "guidance", "Invalid needs target", "Fix the dangling/self `needs:` reference.", { category: "correctness" }),
12
+ WGL014: auditRule("WGL014", "merge-worthy", "guidance", "Invalid extends target", "Point `extends:` at a template that exists in the pipeline.", { category: "correctness" }),
13
+ WGL015: auditRule("WGL015", "merge-worthy", "guidance", "Circular needs chain", "Break the cycle in the job dependency graph.", { category: "correctness" }),
14
+ WGL016: auditRule("WGL016", "merge-worthy", "guidance", "Hardcoded secret in variables", "Move the secret out of `variables:` into a masked/protected CI variable and rotate it.", { authority: [GH_SECRETS] }),
15
+ WGL017: auditRule("WGL017", "merge-worthy", "guidance", "Insecure (non-HTTPS) registry", "Use an HTTPS registry endpoint.", { category: "security" }),
16
+ WGL018: auditRule("WGL018", "report-only", "guidance", "Missing job timeout", "Add a `timeout:` to bound long-running jobs.", { category: "best-practice" }),
17
+ WGL019: auditRule("WGL019", "report-only", "guidance", "Missing retry on deploy job", "Add a `retry:` strategy to deploy jobs.", { category: "best-practice" }),
18
+ WGL020: auditRule("WGL020", "merge-worthy", "guidance", "Duplicate job names", "Rename so each job resolves to a unique name.", { category: "correctness" }),
19
+ WGL021: auditRule("WGL021", "report-only", "guidance", "Unused global variable", "Remove the unused global `variables:` entry.", { category: "best-practice" }),
20
+ WGL022: auditRule("WGL022", "report-only", "guidance", "Missing artifacts expiry", "Add `expire_in:` to artifacts to avoid disk bloat.", { category: "best-practice" }),
21
+ WGL023: auditRule("WGL023", "report-only", "guidance", "Overly broad rules (when: always)", "Add real conditions to the job's `rules:`.", { category: "best-practice" }),
22
+ WGL024: auditRule("WGL024", "report-only", "guidance", "Manual job without allow_failure", "Add `allow_failure: true` so a manual job doesn't block the pipeline.", { category: "best-practice" }),
23
+ WGL025: auditRule("WGL025", "report-only", "guidance", "Cache without a key", "Add a `cache.key` to avoid cross-job cache collisions.", { category: "best-practice" }),
24
+ WGL026: auditRule("WGL026", "merge-worthy", "guidance", "Privileged DinD service without TLS", "Set `DOCKER_TLS_CERTDIR` for privileged Docker-in-Docker services.", { category: "security" }),
25
+ WGL027: auditRule("WGL027", "merge-worthy", "guidance", "Empty script", "Give the job a non-empty `script:`; it currently does nothing.", { category: "correctness" }),
26
+ WGL028: auditRule("WGL028", "report-only", "guidance", "Redundant needs", "Drop `needs:` already implied by stage ordering.", { category: "best-practice" }),
27
+ WGL029: auditRule("WGL029", "merge-worthy", "guidance", "include/component resolved by a moving ref", "Pin `include:project`/component to a tag or commit SHA, not a branch.", { authority: [SCORECARD_PINNED] }),
28
+ WGL030: auditRule("WGL030", "merge-worthy", "guidance", "Insecure or mutable include:remote", "Use HTTPS and pin the remote include to an immutable ref.", { authority: [SCORECARD_PINNED] }),
29
+ WGL031: auditRule("WGL031", "merge-worthy", "deterministic", "Container image not pinned to a digest", "Pin the image to an immutable `@sha256:` digest.", { authority: [SCORECARD_PINNED] }),
30
+ WGL032: auditRule("WGL032", "merge-worthy", "guidance", "Possible include/component impersonation", "Verify the include source; it resembles a well-known project.", { authority: [SCORECARD_PINNED] }),
31
+ WGL033: auditRule("WGL033", "merge-worthy", "guidance", "OIDC id_token without a scoped audience", "Set a specific `aud:` on the OIDC id_token.", { authority: [GH_OIDC] }),
32
+ WGL034: auditRule("WGL034", "merge-worthy", "guidance", "OIDC id_token mintable from a merge-request pipeline", "Restrict OIDC token minting to protected pipelines.", { authority: [GH_OIDC, GH_PWN] }),
33
+ WGL035: auditRule("WGL035", "merge-worthy", "guidance", "Untrusted CI variable interpolated into a script", "Pass untrusted variables via the environment and quote them; don't inline.", { authority: [GH_INJECTION] }),
34
+ WGL036: auditRule("WGL036", "merge-worthy", "guidance", "Privileged service reachable from merge-request pipelines", "Block privileged/DinD services on merge-request pipelines.", { authority: [GH_PWN] }),
35
+ WGL037: auditRule("WGL037", "merge-worthy", "guidance", "Security gate on an untrusted ref regex", "Don't gate security decisions on a regex over an untrusted ref variable.", { authority: [GH_PWN] }),
36
+ WGL038: auditRule("WGL038", "merge-worthy", "guidance", "Secret reachable from a merge-request pipeline", "Scope secret-like variables to protected branches/pipelines.", { authority: [GH_SECRETS, GH_PWN] }),
37
+ WGL039: auditRule("WGL039", "merge-worthy", "guidance", "Secret printed to job logs", "Stop echoing the secret-like variable; mask it.", { authority: [GH_SECRETS] }),
38
+ WGL040: auditRule("WGL040", "merge-worthy", "guidance", "Hardcoded credential in a registry login", "Move the credential to a masked CI variable and rotate it.", { authority: [GH_SECRETS] }),
39
+ WGL041: auditRule("WGL041", "merge-worthy", "guidance", "Tautological rules:if condition", "Fix the always-true `rules:if`; it may neutralize a gate.", { category: "correctness" }),
40
+ WGL042: auditRule("WGL042", "report-only", "guidance", "Unreachable rules after an unconditional match", "Remove the dead `rules:` entries after the catch-all.", { category: "best-practice" }),
41
+ WGL043: auditRule("WGL043", "merge-worthy", "guidance", "Match-anything regex gate in rules:if", "Tighten the regex; a match-anything gate is no gate.", { authority: [GH_PWN] }),
42
+ WGL044: auditRule("WGL044", "merge-worthy", "guidance", "Public artifacts expose build output", "Mark sensitive artifacts non-public (`public: false`).", { category: "security" }),
43
+ WGL045: auditRule("WGL045", "merge-worthy", "guidance", "Artifact path may capture a credential file", "Narrow the artifact path so it can't capture credential files.", { authority: [GH_SECRETS] }),
44
+ WGL046: auditRule("WGL046", "merge-worthy", "guidance", "Cache populated in a merge-request pipeline", "Don't populate caches from merge-request pipelines (poisoning risk).", { authority: [GH_PWN] }),
45
+ WGL047: auditRule("WGL047", "merge-worthy", "guidance", "Software piped to a shell without verification", "Verify a checksum/signature before executing fetched scripts.", { authority: [SCORECARD_PINNED] }),
46
+ WGL048: auditRule("WGL048", "report-only", "guidance", "Pipeline without workflow:name", "Add a `workflow:name` for clearer pipeline naming.", { category: "best-practice" }),
47
+ // Efficiency (#444) — waste, not a safety/correctness issue. Always
48
+ // report-only: none of these warrant a merge on their own.
49
+ WGL049: auditRule("WGL049", "report-only", "guidance", "Dependency install without a cache", "Add a `cache:` covering the dependency directory.", { category: "efficiency" }),
50
+ WGL050: auditRule("WGL050", "report-only", "guidance", "Merge-request job missing interruptible", "Add `interruptible: true` so a superseded pipeline can be cancelled.", { category: "efficiency" }),
51
+ };
52
+ // Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
53
+ applyLineage(gitlabAuditCatalog, gitlabAuditLineage);
@@ -0,0 +1,64 @@
1
+ export const gitlabAuditLineage = {
2
+ WGL010: [
3
+ { tool: "gitlab-docs", rule: "stage: supported values are the default stages or stages listed in `stages`", url: "https://docs.gitlab.com/ci/yaml/#stage", relation: "equivalent" },
4
+ ],
5
+ WGL012: [
6
+ { tool: "gitlab-docs", rule: "Deprecated keywords (only/except, globally-defined image/services/cache/before_script/after_script)", url: "https://docs.gitlab.com/ci/yaml/deprecated_keywords/#only--except", relation: "equivalent" },
7
+ ],
8
+ WGL013: [
9
+ { tool: "gitlab-docs", rule: "Error: 'job' does not exist in the pipeline (needs)", url: "https://docs.gitlab.com/ci/yaml/needs/#error-job-does-not-exist-in-the-pipeline", relation: "equivalent" },
10
+ ],
11
+ WGL014: [
12
+ { tool: "gitlab-docs", rule: "extends: supported values are the name(s) of another job in the pipeline", url: "https://docs.gitlab.com/ci/yaml/#extends", relation: "equivalent" },
13
+ ],
14
+ WGL016: [
15
+ { tool: "gitlab-docs", rule: "CI/CD variable security (secrets belong in masked/protected variables, not in .gitlab-ci.yml)", url: "https://docs.gitlab.com/ci/variables/#cicd-variable-security", relation: "overlaps" },
16
+ ],
17
+ WGL023: [
18
+ { tool: "gitlab-docs", rule: "Pipeline warning: Job may allow multiple pipelines to run for a single action (final `when` rule without conditions)", url: "https://docs.gitlab.com/ci/debugging/#job-may-allow-multiple-pipelines-to-run-for-a-single-action-warning", relation: "overlaps" },
19
+ ],
20
+ WGL024: [
21
+ { tool: "gitlab-docs", rule: "allow_failure default: true for manual jobs, false for rules:when: manual", url: "https://docs.gitlab.com/ci/yaml/#allow_failure", relation: "overlaps" },
22
+ ],
23
+ WGL025: [
24
+ { tool: "gitlab-docs", rule: "cache:key: all jobs with `cache` but no `cache:key` share the `default` cache", url: "https://docs.gitlab.com/ci/yaml/#cachekey", relation: "overlaps" },
25
+ ],
26
+ WGL026: [
27
+ { tool: "gitlab-docs", rule: "Docker-in-Docker with TLS enabled (recommended); DOCKER_TLS_CERTDIR", url: "https://docs.gitlab.com/ci/docker/docker_in_docker/#docker-in-docker-with-tls-enabled-in-the-docker-executor-recommended", relation: "overlaps" },
28
+ ],
29
+ WGL029: [
30
+ { tool: "gitlab-docs", rule: "include:project ref guidance: use a specific SHA hash, protected branch/tag rules", url: "https://docs.gitlab.com/ci/yaml/#includeproject", relation: "overlaps" },
31
+ { tool: "gitlab-docs", rule: "CI/CD component security best practices: use pinned versions (commit SHA preferred), avoid `latest`", url: "https://docs.gitlab.com/ci/components/#for-component-users", relation: "overlaps" },
32
+ ],
33
+ WGL030: [
34
+ { tool: "gitlab-docs", rule: "include:remote: treat as a third-party dependency; verify integrity with include:integrity", url: "https://docs.gitlab.com/ci/yaml/#includeremote", relation: "overlaps" },
35
+ ],
36
+ WGL031: [
37
+ { tool: "gitlab-docs", rule: "Use checksum to keep your image secure (image@sha256:digest)", url: "https://docs.gitlab.com/ci/docker/using_docker_images/#use-checksum-to-keep-your-image-secure", relation: "overlaps" },
38
+ ],
39
+ WGL033: [
40
+ { tool: "gitlab-docs", rule: "id_tokens: the required `aud` sub-keyword configures the aud claim", url: "https://docs.gitlab.com/ci/yaml/#id_tokens", relation: "overlaps" },
41
+ ],
42
+ WGL035: [
43
+ { tool: "poutine", rule: "injection", url: "https://boostsecurityio.github.io/poutine/rules/injection/", relation: "overlaps" },
44
+ ],
45
+ WGL038: [
46
+ { tool: "gitlab-docs", rule: "Protect a CI/CD variable (available only to protected branches/tags)", url: "https://docs.gitlab.com/ci/variables/#protect-a-cicd-variable", relation: "overlaps" },
47
+ { tool: "gitlab-docs", rule: "Merge request pipelines from forked projects can steal secrets in the parent project", url: "https://docs.gitlab.com/ci/pipelines/merge_request_pipelines/#use-with-forked-projects", relation: "overlaps" },
48
+ ],
49
+ WGL039: [
50
+ { tool: "gitlab-docs", rule: "CI/CD variable security: accidental-leak-job example (echo of $PASSWORD); mask sensitive variables", url: "https://docs.gitlab.com/ci/variables/#mask-a-cicd-variable", relation: "overlaps" },
51
+ ],
52
+ WGL044: [
53
+ { tool: "gitlab-docs", rule: "artifacts:public (default true: downloadable by anonymous users in public pipelines)", url: "https://docs.gitlab.com/ci/yaml/#artifactspublic", relation: "overlaps" },
54
+ ],
55
+ WGL046: [
56
+ { tool: "gitlab-docs", rule: "Cache key names: protected and non-protected branches do not share the cache by default", url: "https://docs.gitlab.com/ci/caching/#cache-key-names", relation: "overlaps" },
57
+ ],
58
+ WGL047: [
59
+ { tool: "poutine", rule: "unverified_script_exec", url: "https://boostsecurityio.github.io/poutine/rules/unverified_script_exec/", relation: "equivalent" },
60
+ ],
61
+ WGL048: [
62
+ { tool: "gitlab-docs", rule: "Use pipeline names (workflow:name)", url: "https://docs.gitlab.com/ci/debugging/#use-pipeline-names", relation: "overlaps" },
63
+ ],
64
+ };
@@ -0,0 +1,84 @@
1
+ import { wgl010 } from "./wgl010.js";
2
+ import { wgl011 } from "./wgl011.js";
3
+ import { wgl012 } from "./wgl012.js";
4
+ import { wgl013 } from "./wgl013.js";
5
+ import { wgl014 } from "./wgl014.js";
6
+ import { wgl015 } from "./wgl015.js";
7
+ import { wgl016 } from "./wgl016.js";
8
+ import { wgl017 } from "./wgl017.js";
9
+ import { wgl018 } from "./wgl018.js";
10
+ import { wgl019 } from "./wgl019.js";
11
+ import { wgl020 } from "./wgl020.js";
12
+ import { wgl021 } from "./wgl021.js";
13
+ import { wgl022 } from "./wgl022.js";
14
+ import { wgl023 } from "./wgl023.js";
15
+ import { wgl024 } from "./wgl024.js";
16
+ import { wgl025 } from "./wgl025.js";
17
+ import { wgl026 } from "./wgl026.js";
18
+ import { wgl027 } from "./wgl027.js";
19
+ import { wgl028 } from "./wgl028.js";
20
+ import { wgl029 } from "./wgl029.js";
21
+ import { wgl030 } from "./wgl030.js";
22
+ import { wgl031 } from "./wgl031.js";
23
+ import { wgl032 } from "./wgl032.js";
24
+ import { wgl033 } from "./wgl033.js";
25
+ import { wgl034 } from "./wgl034.js";
26
+ import { wgl035 } from "./wgl035.js";
27
+ import { wgl036 } from "./wgl036.js";
28
+ import { wgl037 } from "./wgl037.js";
29
+ import { wgl038 } from "./wgl038.js";
30
+ import { wgl039 } from "./wgl039.js";
31
+ import { wgl040 } from "./wgl040.js";
32
+ import { wgl041 } from "./wgl041.js";
33
+ import { wgl042 } from "./wgl042.js";
34
+ import { wgl043 } from "./wgl043.js";
35
+ import { wgl044 } from "./wgl044.js";
36
+ import { wgl045 } from "./wgl045.js";
37
+ import { wgl046 } from "./wgl046.js";
38
+ import { wgl047 } from "./wgl047.js";
39
+ import { wgl048 } from "./wgl048.js";
40
+ import { wgl049 } from "./wgl049.js";
41
+ import { wgl050 } from "./wgl050.js";
42
+ export const postSynthChecks = [
43
+ wgl010,
44
+ wgl011,
45
+ wgl012,
46
+ wgl013,
47
+ wgl014,
48
+ wgl015,
49
+ wgl016,
50
+ wgl017,
51
+ wgl018,
52
+ wgl019,
53
+ wgl020,
54
+ wgl021,
55
+ wgl022,
56
+ wgl023,
57
+ wgl024,
58
+ wgl025,
59
+ wgl026,
60
+ wgl027,
61
+ wgl028,
62
+ wgl029,
63
+ wgl030,
64
+ wgl031,
65
+ wgl032,
66
+ wgl033,
67
+ wgl034,
68
+ wgl035,
69
+ wgl036,
70
+ wgl037,
71
+ wgl038,
72
+ wgl039,
73
+ wgl040,
74
+ wgl041,
75
+ wgl042,
76
+ wgl043,
77
+ wgl044,
78
+ wgl045,
79
+ wgl046,
80
+ wgl047,
81
+ wgl048,
82
+ wgl049,
83
+ wgl050,
84
+ ];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * WGL010: Undefined stage
3
+ *
4
+ * Detects jobs that reference a stage not declared in the `stages:` list.
5
+ * This will cause a pipeline validation error in GitLab.
6
+ */
7
+ import { getPrimaryOutput, extractStages, extractJobs } from "./yaml-helpers.js";
8
+ export const wgl010 = {
9
+ id: "WGL010",
10
+ description: "Job references a stage not in the stages list",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ for (const [, output] of ctx.outputs) {
14
+ const yaml = getPrimaryOutput(output);
15
+ const stages = extractStages(yaml);
16
+ if (stages.length === 0)
17
+ continue; // No explicit stages — GitLab uses defaults
18
+ const stageSet = new Set(stages);
19
+ const jobs = extractJobs(yaml);
20
+ for (const [jobName, job] of jobs) {
21
+ if (job.stage && !stageSet.has(job.stage)) {
22
+ diagnostics.push({
23
+ checkId: "WGL010",
24
+ severity: "error",
25
+ message: `Job "${jobName}" references undefined stage "${job.stage}". Add it to the stages list.`,
26
+ entity: jobName,
27
+ lexicon: "gitlab",
28
+ });
29
+ }
30
+ }
31
+ }
32
+ return diagnostics;
33
+ },
34
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * WGL011: Unreachable job
3
+ *
4
+ * Detects jobs where all `rules:` entries evaluate to `when: never`,
5
+ * making the job unreachable. This usually indicates a configuration error.
6
+ *
7
+ * Note: This is a simple static check — it only catches the obvious case
8
+ * where every rule has `when: "never"` literally set. Complex conditions
9
+ * with `if:` expressions are not evaluated.
10
+ */
11
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
12
+ export const wgl011 = {
13
+ id: "WGL011",
14
+ description: "Job has rules that always evaluate to never (unreachable)",
15
+ check(ctx) {
16
+ const diagnostics = [];
17
+ for (const [entityName, entity] of ctx.entities) {
18
+ if (isPropertyDeclarable(entity))
19
+ continue;
20
+ const entityType = entity.entityType;
21
+ if (entityType !== "GitLab::CI::Job")
22
+ continue;
23
+ const props = entity.props;
24
+ if (!props?.rules || !Array.isArray(props.rules))
25
+ continue;
26
+ const rules = props.rules;
27
+ if (rules.length === 0)
28
+ continue;
29
+ // Check if ALL rules have when: "never"
30
+ const allNever = rules.every((rule) => {
31
+ // If the rule is a declarable (e.g. new Rule({...})), check its props
32
+ const ruleProps = rule.props;
33
+ const when = ruleProps?.when ?? rule.when;
34
+ return when === "never";
35
+ });
36
+ if (allNever) {
37
+ diagnostics.push({
38
+ checkId: "WGL011",
39
+ severity: "warning",
40
+ message: `Job "${entityName}" has rules that all evaluate to "never" — this job will never run.`,
41
+ entity: entityName,
42
+ lexicon: "gitlab",
43
+ });
44
+ }
45
+ }
46
+ return diagnostics;
47
+ },
48
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"wgl012.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wgl012.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAgC9G;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GACnC,mBAAmB,EAAE,CA6BvB;AAED,eAAO,MAAM,MAAM,EAAE,cAOpB,CAAC"}
1
+ {"version":3,"file":"wgl012.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wgl012.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AA+B9G;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GACnC,mBAAmB,EAAE,CA6BvB;AAED,eAAO,MAAM,MAAM,EAAE,cAOpB,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * WGL012: Deprecated Property Usage
3
+ *
4
+ * Flags properties marked as deprecated in the GitLab CI schema.
5
+ * Sources: description text mining (keywords like "Deprecated", "legacy").
6
+ */
7
+ import { readFileSync } from "fs";
8
+ import { join } from "path";
9
+ import { moduleSourceDir } from "@intentius/chant/module-source";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+ /**
12
+ * Load deprecated properties per entity type from the lexicon JSON.
13
+ */
14
+ function loadDeprecatedProperties() {
15
+ const map = new Map();
16
+ try {
17
+ const lexiconPath = join(moduleSourceDir(import.meta.url), "..", "..", "generated", "lexicon-gitlab.json");
18
+ const content = readFileSync(lexiconPath, "utf-8");
19
+ const data = JSON.parse(content);
20
+ for (const [_name, entry] of Object.entries(data)) {
21
+ if (entry.resourceType && entry.deprecatedProperties && entry.deprecatedProperties.length > 0) {
22
+ map.set(entry.resourceType, new Set(entry.deprecatedProperties));
23
+ }
24
+ }
25
+ }
26
+ catch {
27
+ // Lexicon not available — skip
28
+ }
29
+ return map;
30
+ }
31
+ /**
32
+ * Core detection logic — exported for direct testing with synthetic data.
33
+ */
34
+ export function checkDeprecatedProperties(ctx, deprecated) {
35
+ if (deprecated.size === 0)
36
+ return [];
37
+ const diagnostics = [];
38
+ for (const [entityName, entity] of ctx.entities) {
39
+ if (isPropertyDeclarable(entity))
40
+ continue;
41
+ const entityType = entity.entityType;
42
+ const deprProps = deprecated.get(entityType);
43
+ if (!deprProps)
44
+ continue;
45
+ const props = entity.props;
46
+ if (!props)
47
+ continue;
48
+ for (const propName of Object.keys(props)) {
49
+ if (deprProps.has(propName)) {
50
+ diagnostics.push({
51
+ checkId: "WGL012",
52
+ severity: "warning",
53
+ message: `Entity "${entityName}" (${entityType}) uses deprecated property "${propName}" — consider alternatives`,
54
+ entity: entityName,
55
+ lexicon: "gitlab",
56
+ });
57
+ }
58
+ }
59
+ }
60
+ return diagnostics;
61
+ }
62
+ export const wgl012 = {
63
+ id: "WGL012",
64
+ description: "Deprecated property usage — flags properties marked as deprecated in the GitLab CI schema",
65
+ check(ctx) {
66
+ return checkDeprecatedProperties(ctx, loadDeprecatedProperties());
67
+ },
68
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * WGL013: Invalid `needs:` Target
3
+ *
4
+ * Detects two cases in the serialized YAML:
5
+ * - Dangling reference: `needs:` names a job not defined in the pipeline
6
+ * - Self-reference: job lists itself in `needs:`
7
+ *
8
+ * Both cause GitLab pipeline validation failures.
9
+ *
10
+ * Caveat: when `include:` is present, referenced jobs may come from
11
+ * included files, so the check is skipped. A `- job: x` entry with
12
+ * `optional: true` may name a job that is not in the pipeline (GitLab
13
+ * ignores it), so it is not reported as dangling.
14
+ */
15
+ import { getPrimaryOutput, extractJobs, hasInclude } from "./yaml-helpers.js";
16
+ export function checkInvalidNeeds(ctx) {
17
+ const diagnostics = [];
18
+ for (const [, output] of ctx.outputs) {
19
+ const yaml = getPrimaryOutput(output);
20
+ if (hasInclude(yaml))
21
+ continue;
22
+ const jobs = extractJobs(yaml);
23
+ const jobNames = new Set(jobs.keys());
24
+ for (const [jobName, job] of jobs) {
25
+ if (!job.needs)
26
+ continue;
27
+ for (const need of job.needs) {
28
+ if (need === jobName) {
29
+ diagnostics.push({
30
+ checkId: "WGL013",
31
+ severity: "error",
32
+ message: `Job "${jobName}" lists itself in needs: — self-references are invalid`,
33
+ entity: jobName,
34
+ lexicon: "gitlab",
35
+ });
36
+ }
37
+ else if (!jobNames.has(need) && !job.optionalNeeds?.includes(need)) {
38
+ diagnostics.push({
39
+ checkId: "WGL013",
40
+ severity: "error",
41
+ message: `Job "${jobName}" needs "${need}" which is not defined in the pipeline`,
42
+ entity: jobName,
43
+ lexicon: "gitlab",
44
+ });
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return diagnostics;
50
+ }
51
+ export const wgl013 = {
52
+ id: "WGL013",
53
+ description: "Invalid needs: target — dangling reference or self-reference",
54
+ check(ctx) {
55
+ return checkInvalidNeeds(ctx);
56
+ },
57
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WGL014: Invalid `extends:` Target
3
+ *
4
+ * Detects jobs that `extends:` a template not defined in the pipeline YAML.
5
+ * GitLab rejects pipelines with unresolved extends references.
6
+ *
7
+ * Caveat: when `include:` is present, templates may come from
8
+ * included files, so the check is skipped.
9
+ */
10
+ import { getPrimaryOutput, extractJobs, hasInclude } from "./yaml-helpers.js";
11
+ export function checkInvalidExtends(ctx) {
12
+ const diagnostics = [];
13
+ for (const [, output] of ctx.outputs) {
14
+ const yaml = getPrimaryOutput(output);
15
+ if (hasInclude(yaml))
16
+ continue;
17
+ const jobs = extractJobs(yaml);
18
+ const jobNames = new Set(jobs.keys());
19
+ for (const [jobName, job] of jobs) {
20
+ if (!job.extends)
21
+ continue;
22
+ for (const target of job.extends) {
23
+ if (!jobNames.has(target)) {
24
+ diagnostics.push({
25
+ checkId: "WGL014",
26
+ severity: "error",
27
+ message: `Job "${jobName}" extends "${target}" which is not defined in the pipeline`,
28
+ entity: jobName,
29
+ lexicon: "gitlab",
30
+ });
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return diagnostics;
36
+ }
37
+ export const wgl014 = {
38
+ id: "WGL014",
39
+ description: "Invalid extends: target — references a template not in the pipeline",
40
+ check(ctx) {
41
+ return checkInvalidExtends(ctx);
42
+ },
43
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WGL015: Circular `needs:` Chain
3
+ *
4
+ * DFS-based cycle detection on the `needs:` dependency graph.
5
+ * If A needs B and B needs A, GitLab rejects the pipeline.
6
+ *
7
+ * Reports one diagnostic per cycle found, listing the full chain.
8
+ */
9
+ import { getPrimaryOutput, extractJobs } from "./yaml-helpers.js";
10
+ export function checkCircularNeeds(ctx) {
11
+ const diagnostics = [];
12
+ for (const [, output] of ctx.outputs) {
13
+ const yaml = getPrimaryOutput(output);
14
+ const jobs = extractJobs(yaml);
15
+ // Build adjacency list from needs
16
+ const graph = new Map();
17
+ for (const [jobName, job] of jobs) {
18
+ graph.set(jobName, job.needs ?? []);
19
+ }
20
+ // DFS cycle detection
21
+ const visited = new Set();
22
+ const inStack = new Set();
23
+ const reportedInCycle = new Set();
24
+ function dfs(node, path) {
25
+ if (inStack.has(node)) {
26
+ // Found a cycle — extract the cycle portion
27
+ const cycleStart = path.indexOf(node);
28
+ const cycle = path.slice(cycleStart);
29
+ cycle.push(node);
30
+ // Only report if we haven't already reported a cycle containing these nodes
31
+ const cycleKey = [...cycle].sort().join(",");
32
+ if (!reportedInCycle.has(cycleKey)) {
33
+ reportedInCycle.add(cycleKey);
34
+ diagnostics.push({
35
+ checkId: "WGL015",
36
+ severity: "error",
37
+ message: `Circular needs: chain detected: ${cycle.join(" → ")}`,
38
+ entity: node,
39
+ lexicon: "gitlab",
40
+ });
41
+ }
42
+ return;
43
+ }
44
+ if (visited.has(node))
45
+ return;
46
+ visited.add(node);
47
+ inStack.add(node);
48
+ for (const neighbor of graph.get(node) ?? []) {
49
+ // Only follow edges to known jobs
50
+ if (graph.has(neighbor)) {
51
+ dfs(neighbor, [...path, node]);
52
+ }
53
+ }
54
+ inStack.delete(node);
55
+ }
56
+ for (const jobName of graph.keys()) {
57
+ if (!visited.has(jobName)) {
58
+ dfs(jobName, []);
59
+ }
60
+ }
61
+ }
62
+ return diagnostics;
63
+ }
64
+ export const wgl015 = {
65
+ id: "WGL015",
66
+ description: "Circular needs: chain — cycle in job dependency graph",
67
+ check(ctx) {
68
+ return checkCircularNeeds(ctx);
69
+ },
70
+ };
@@ -0,0 +1,70 @@
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
+ import { getPrimaryOutput } from "./yaml-helpers.js";
8
+ const SECRET_PATTERNS = [
9
+ /password\s*[:=]\s*['"]?[^\s'"]+/i,
10
+ /secret\s*[:=]\s*['"]?[^\s'"]+/i,
11
+ /token\s*[:=]\s*['"]?[^\s'"]+/i,
12
+ /api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/i,
13
+ /private[_-]?key\s*[:=]\s*['"]?[^\s'"]+/i,
14
+ ];
15
+ /** Variable name patterns that indicate credentials. */
16
+ const SECRET_VAR_NAMES = [
17
+ /password/i,
18
+ /secret/i,
19
+ /token/i,
20
+ /api[_-]?key/i,
21
+ /private[_-]?key/i,
22
+ /credentials?/i,
23
+ ];
24
+ /** Values that are clearly references (not hardcoded secrets). */
25
+ function isReference(value) {
26
+ return value.startsWith("$") || value.startsWith("${");
27
+ }
28
+ export function checkSecretsInVariables(yaml) {
29
+ const diagnostics = [];
30
+ // Extract variables blocks (global and per-job)
31
+ const varBlocks = yaml.matchAll(/^(\s*)variables:\n((?:\1\s+.+\n?)+)/gm);
32
+ for (const block of varBlocks) {
33
+ const lines = block[2].split("\n");
34
+ for (const line of lines) {
35
+ const kv = line.match(/^\s+(\w+):\s+(.+)$/);
36
+ if (!kv)
37
+ continue;
38
+ const [, varName, rawValue] = kv;
39
+ const value = rawValue.trim().replace(/^['"]|['"]$/g, "");
40
+ if (isReference(value))
41
+ continue;
42
+ // Check if variable name suggests a secret
43
+ for (const pattern of SECRET_VAR_NAMES) {
44
+ if (pattern.test(varName)) {
45
+ diagnostics.push({
46
+ checkId: "WGL016",
47
+ severity: "error",
48
+ message: `Variable "${varName}" appears to contain a hardcoded secret — use a CI/CD masked variable instead`,
49
+ entity: varName,
50
+ lexicon: "gitlab",
51
+ });
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return diagnostics;
58
+ }
59
+ export const wgl016 = {
60
+ id: "WGL016",
61
+ description: "Secrets in variables — hardcoded passwords or tokens in variables blocks",
62
+ check(ctx) {
63
+ const diagnostics = [];
64
+ for (const [, output] of ctx.outputs) {
65
+ const yaml = getPrimaryOutput(output);
66
+ diagnostics.push(...checkSecretsInVariables(yaml));
67
+ }
68
+ return diagnostics;
69
+ },
70
+ };
@@ -0,0 +1,38 @@
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
+ import { getPrimaryOutput, topLevelSections } from "./yaml-helpers.js";
8
+ const INSECURE_REGISTRY_PATTERN = /docker\s+(push|pull|tag|login)\s+.*http:\/\/[^\s]+/;
9
+ export function checkInsecureRegistry(yaml) {
10
+ const diagnostics = [];
11
+ for (const { key: jobName, text } of topLevelSections(yaml)) {
12
+ for (const line of text.split("\n")) {
13
+ if (INSECURE_REGISTRY_PATTERN.test(line)) {
14
+ diagnostics.push({
15
+ checkId: "WGL017",
16
+ severity: "warning",
17
+ message: `Job "${jobName}" uses an insecure (HTTP) container registry — use HTTPS instead`,
18
+ entity: jobName,
19
+ lexicon: "gitlab",
20
+ });
21
+ break;
22
+ }
23
+ }
24
+ }
25
+ return diagnostics;
26
+ }
27
+ export const wgl017 = {
28
+ id: "WGL017",
29
+ description: "Insecure registry — Docker push/pull to non-HTTPS registry",
30
+ check(ctx) {
31
+ const diagnostics = [];
32
+ for (const [, output] of ctx.outputs) {
33
+ const yaml = getPrimaryOutput(output);
34
+ diagnostics.push(...checkInsecureRegistry(yaml));
35
+ }
36
+ return diagnostics;
37
+ },
38
+ };