@intentius/chant-lexicon-gitlab 0.1.11 → 0.1.14

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 (82) hide show
  1. package/README.md +4 -0
  2. package/dist/integrity.json +3 -2
  3. package/dist/manifest.json +1 -1
  4. package/dist/skills/chant-gitlab-migrate.md +117 -0
  5. package/package.json +11 -4
  6. package/src/import/generator.ts +20 -2
  7. package/src/migrate/from-github/actions/index.ts +27 -0
  8. package/src/migrate/from-github/actions/registry.ts +112 -0
  9. package/src/migrate/from-github/actions/tier-1.test.ts +128 -0
  10. package/src/migrate/from-github/actions/tier-1.ts +325 -0
  11. package/src/migrate/from-github/actions/tier-2-3.test.ts +144 -0
  12. package/src/migrate/from-github/actions/tier-2.ts +296 -0
  13. package/src/migrate/from-github/actions/tier-3.ts +124 -0
  14. package/src/migrate/from-github/composites/patterns.ts +167 -0
  15. package/src/migrate/from-github/composites/rewriter.test.ts +98 -0
  16. package/src/migrate/from-github/composites/rewriter.ts +29 -0
  17. package/src/migrate/from-github/diagnostics.ts +45 -0
  18. package/src/migrate/from-github/emit-ts.test.ts +49 -0
  19. package/src/migrate/from-github/emit-yaml.ts +128 -0
  20. package/src/migrate/from-github/expressions.test.ts +124 -0
  21. package/src/migrate/from-github/expressions.ts +302 -0
  22. package/src/migrate/from-github/fixtures/README.md +27 -0
  23. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected-report.json +15 -0
  24. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected.gitlab-ci.yml +13 -0
  25. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/input.yml +7 -0
  26. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected-report.json +20 -0
  27. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected.gitlab-ci.yml +20 -0
  28. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/input.yml +12 -0
  29. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected-report.json +20 -0
  30. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected.gitlab-ci.yml +17 -0
  31. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/input.yml +12 -0
  32. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected-report.json +24 -0
  33. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected.gitlab-ci.yml +20 -0
  34. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/input.yml +16 -0
  35. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected-report.json +24 -0
  36. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected.gitlab-ci.yml +27 -0
  37. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/input.yml +20 -0
  38. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected-report.json +24 -0
  39. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected.gitlab-ci.yml +15 -0
  40. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/input.yml +13 -0
  41. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected-report.json +20 -0
  42. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected.gitlab-ci.yml +17 -0
  43. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/input.yml +11 -0
  44. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected-report.json +21 -0
  45. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected.gitlab-ci.yml +15 -0
  46. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/input.yml +11 -0
  47. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected-report.json +20 -0
  48. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected.gitlab-ci.yml +16 -0
  49. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/input.yml +12 -0
  50. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected-report.json +13 -0
  51. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected.gitlab-ci.yml +31 -0
  52. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/input.yml +16 -0
  53. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected-report.json +13 -0
  54. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected.gitlab-ci.yml +20 -0
  55. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/input.yml +10 -0
  56. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected-report.json +13 -0
  57. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected.gitlab-ci.yml +18 -0
  58. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/input.yml +11 -0
  59. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected-report.json +13 -0
  60. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected.gitlab-ci.yml +24 -0
  61. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/input.yml +12 -0
  62. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected-report.json +13 -0
  63. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected.gitlab-ci.yml +18 -0
  64. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/input.yml +13 -0
  65. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected-report.json +20 -0
  66. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected.gitlab-ci.yml +17 -0
  67. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/input.yml +13 -0
  68. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected-report.json +13 -0
  69. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected.gitlab-ci.yml +14 -0
  70. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/input.yml +7 -0
  71. package/src/migrate/from-github/fixtures.test.ts +92 -0
  72. package/src/migrate/from-github/index.ts +128 -0
  73. package/src/migrate/from-github/provenance.ts +68 -0
  74. package/src/migrate/from-github/rules.ts +82 -0
  75. package/src/migrate/from-github/stages.test.ts +99 -0
  76. package/src/migrate/from-github/stages.ts +177 -0
  77. package/src/migrate/from-github/transformer.test.ts +278 -0
  78. package/src/migrate/from-github/transformer.ts +719 -0
  79. package/src/migrate.mcp.test.ts +69 -0
  80. package/src/plugin.test.ts +7 -3
  81. package/src/plugin.ts +105 -1
  82. package/src/skills/chant-gitlab-migrate.md +117 -0
@@ -0,0 +1,11 @@
1
+ on: push
2
+ env:
3
+ GREETING: Hello
4
+ jobs:
5
+ greet:
6
+ runs-on: ubuntu-latest
7
+ env:
8
+ NAME: World
9
+ steps:
10
+ - run: echo "$GREETING $NAME"
11
+ - run: echo "Token is ${{ secrets.API_TOKEN }}"
@@ -0,0 +1,13 @@
1
+ {
2
+ "totals": {
3
+ "error": 0,
4
+ "warning": 0,
5
+ "info": 0
6
+ },
7
+ "ruleIds": [],
8
+ "needsReview": [],
9
+ "provenance": {
10
+ "literalRewrites": 6,
11
+ "actionMappings": []
12
+ }
13
+ }
@@ -0,0 +1,24 @@
1
+ # Generated by chant migrate from input.yml
2
+
3
+ stages:
4
+ - build
5
+ - deploy
6
+ workflow:
7
+ rules:
8
+ - if: '$CI_PIPELINE_SOURCE == "push"'
9
+
10
+ deploy:
11
+ image: ubuntu:24.04
12
+ rules:
13
+ - if: '$CI_COMMIT_REF_NAME == ''refs/heads/main'' && $CI_PIPELINE_SOURCE == ''push'''
14
+ script:
15
+ - make deploy
16
+ stage: deploy
17
+
18
+ notify:
19
+ image: ubuntu:24.04
20
+ rules:
21
+ - when: on_failure
22
+ script:
23
+ - ./notify-slack.sh
24
+ stage: build
@@ -0,0 +1,12 @@
1
+ on: push
2
+ jobs:
3
+ deploy:
4
+ runs-on: ubuntu-latest
5
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
6
+ steps:
7
+ - run: make deploy
8
+ notify:
9
+ runs-on: ubuntu-latest
10
+ if: failure()
11
+ steps:
12
+ - run: ./notify-slack.sh
@@ -0,0 +1,13 @@
1
+ {
2
+ "totals": {
3
+ "error": 0,
4
+ "warning": 0,
5
+ "info": 0
6
+ },
7
+ "ruleIds": [],
8
+ "needsReview": [],
9
+ "provenance": {
10
+ "literalRewrites": 4,
11
+ "actionMappings": []
12
+ }
13
+ }
@@ -0,0 +1,18 @@
1
+ # Generated by chant migrate from input.yml
2
+
3
+ stages:
4
+ - test
5
+ workflow:
6
+ rules:
7
+ - if: '$CI_PIPELINE_SOURCE == "push"'
8
+
9
+ test:
10
+ image: node:20-alpine
11
+ services:
12
+ - name: postgres:15
13
+ alias: postgres
14
+ variables:
15
+ POSTGRES_PASSWORD: secret
16
+ script:
17
+ - npm test
18
+ stage: test
@@ -0,0 +1,13 @@
1
+ on: push
2
+ jobs:
3
+ test:
4
+ runs-on: ubuntu-latest
5
+ container:
6
+ image: node:20-alpine
7
+ services:
8
+ - name: postgres:15
9
+ alias: postgres
10
+ variables:
11
+ POSTGRES_PASSWORD: secret
12
+ steps:
13
+ - run: npm test
@@ -0,0 +1,20 @@
1
+ {
2
+ "totals": {
3
+ "error": 0,
4
+ "warning": 1,
5
+ "info": 0
6
+ },
7
+ "ruleIds": [
8
+ "MIG-PERMISSIONS-001"
9
+ ],
10
+ "needsReview": [
11
+ {
12
+ "line": 1,
13
+ "ruleId": "MIG-PERMISSIONS-001"
14
+ }
15
+ ],
16
+ "provenance": {
17
+ "literalRewrites": 6,
18
+ "actionMappings": []
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ # Generated by chant migrate from input.yml
2
+
3
+ stages:
4
+ - build
5
+ workflow:
6
+ rules:
7
+ - if: '$CI_PIPELINE_SOURCE == "push"'
8
+
9
+ flaky-test:
10
+ image: ubuntu:24.04
11
+ timeout: '10 minutes'
12
+ allow_failure: true
13
+ resource_group: deploy-$CI_COMMIT_REF_NAME
14
+ interruptible: true
15
+ script:
16
+ - make test
17
+ stage: build
@@ -0,0 +1,13 @@
1
+ on: push
2
+ jobs:
3
+ flaky-test:
4
+ runs-on: ubuntu-latest
5
+ timeout-minutes: 10
6
+ continue-on-error: true
7
+ concurrency:
8
+ group: deploy-${{ github.ref }}
9
+ cancel-in-progress: true
10
+ permissions:
11
+ contents: read
12
+ steps:
13
+ - run: make test
@@ -0,0 +1,13 @@
1
+ {
2
+ "totals": {
3
+ "error": 0,
4
+ "warning": 0,
5
+ "info": 0
6
+ },
7
+ "ruleIds": [],
8
+ "needsReview": [],
9
+ "provenance": {
10
+ "literalRewrites": 3,
11
+ "actionMappings": []
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ # Generated by chant migrate from input.yml
2
+
3
+ stages:
4
+ - build
5
+ workflow:
6
+ name: CI Pipeline
7
+ rules:
8
+ - if: '$CI_PIPELINE_SOURCE == "push"'
9
+
10
+ build:
11
+ image: ubuntu:24.04
12
+ script:
13
+ - make
14
+ stage: build
@@ -0,0 +1,7 @@
1
+ name: CI Pipeline
2
+ on: push
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - run: make
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Fixture-driven migration tests.
3
+ *
4
+ * Each fixture directory under `fixtures/` contains:
5
+ * - input.yml (GitHub Actions workflow)
6
+ * - expected.gitlab-ci.yml (expected GitLab CI output)
7
+ * - expected-report.json (expected diagnostic + provenance shape)
8
+ *
9
+ * The test asserts canonical-YAML equality on the output and shape
10
+ * equality on the report. Adding a new fixture requires no manifest
11
+ * edit — just drop the three files into a new directory.
12
+ */
13
+
14
+ import { describe, test, expect } from "vitest";
15
+ import { readFileSync, readdirSync, statSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { parseYAML } from "@intentius/chant/yaml";
18
+ import { transform } from "./index";
19
+
20
+ const FIXTURE_ROOT = join(__dirname, "fixtures");
21
+
22
+ interface Fixture { name: string; dir: string }
23
+
24
+ function* walkFixtures(root: string, prefix = ""): Generator<Fixture> {
25
+ for (const entry of readdirSync(root)) {
26
+ const p = join(root, entry);
27
+ if (!statSync(p).isDirectory()) continue;
28
+ const name = prefix ? `${prefix}/${entry}` : entry;
29
+ const files = readdirSync(p);
30
+ if (files.includes("input.yml") && files.includes("expected.gitlab-ci.yml")) {
31
+ yield { name, dir: p };
32
+ } else {
33
+ yield* walkFixtures(p, name);
34
+ }
35
+ }
36
+ }
37
+
38
+ function canonicalYaml(text: string): unknown {
39
+ // Strip leading comments so the "Generated by chant migrate from ..."
40
+ // banner doesn't have to match byte-for-byte when sourceFile differs.
41
+ const stripped = text.replace(/^#[^\n]*\n+/g, "");
42
+ if (!stripped.trim()) return {};
43
+ return parseYAML(stripped);
44
+ }
45
+
46
+ interface ReportShape {
47
+ totals: { error: number; warning: number; info: number };
48
+ ruleIds: string[];
49
+ needsReview: Array<{ line: number; ruleId: string }>;
50
+ provenance: { literalRewrites: number; actionMappings: Array<{ action: string; tier: number | undefined }> };
51
+ }
52
+
53
+ function reportShape(diagnostics: Array<Record<string, unknown>>, provenance: Array<Record<string, unknown>>): ReportShape {
54
+ const totals = { error: 0, warning: 0, info: 0 };
55
+ for (const d of diagnostics) {
56
+ const sev = d.severity as string;
57
+ if (sev in totals) totals[sev as "error" | "warning" | "info"]++;
58
+ }
59
+ const ruleIds = Array.from(new Set(diagnostics.map((d) => d.ruleId as string))).sort();
60
+ const needsReview = diagnostics
61
+ .filter((d) => d.severity === "warning" || d.severity === "error")
62
+ .map((d) => ({ line: d.line as number, ruleId: d.ruleId as string }));
63
+ const literalRewrites = provenance.filter((p) => p.category === "literal" || p.category === "rewrite").length;
64
+ const actionMappings = provenance
65
+ .filter((p) => p.category === "action-map")
66
+ .map((p) => ({ action: p.actionRef as string, tier: p.mappingTier as number | undefined }));
67
+ return { totals, ruleIds, needsReview, provenance: { literalRewrites, actionMappings } };
68
+ }
69
+
70
+ describe("github → gitlab fixtures", () => {
71
+ for (const f of walkFixtures(FIXTURE_ROOT)) {
72
+ test(f.name, async () => {
73
+ const input = readFileSync(join(f.dir, "input.yml"), "utf-8");
74
+ const expectedYaml = readFileSync(join(f.dir, "expected.gitlab-ci.yml"), "utf-8");
75
+ const expectedReport: ReportShape = JSON.parse(
76
+ readFileSync(join(f.dir, "expected-report.json"), "utf-8"),
77
+ );
78
+
79
+ const result = await transform(input, { sourceFile: "input.yml" });
80
+
81
+ // Canonical YAML compare (parse + re-stringify absorbs whitespace/key order)
82
+ expect(canonicalYaml(result.output)).toEqual(canonicalYaml(expectedYaml));
83
+
84
+ // Shape compare
85
+ const actualShape = reportShape(
86
+ result.diagnostics as unknown as Array<Record<string, unknown>>,
87
+ result.provenance as unknown as Array<Record<string, unknown>>,
88
+ );
89
+ expect(actualShape).toEqual(expectedReport);
90
+ });
91
+ }
92
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Public entry point for the GitHub Actions → GitLab CI migration tool.
3
+ *
4
+ * Lazy-imports `@intentius/chant-lexicon-github` so users who don't need
5
+ * migration don't pay the install cost (the github lexicon is an optional
6
+ * peer dependency of `@intentius/chant-lexicon-gitlab`).
7
+ */
8
+
9
+ import type { TemplateIR } from "@intentius/chant/import/parser";
10
+ import type { LintDiagnostic } from "@intentius/chant/lint/rule";
11
+ import { ProvenanceAccumulator, type ProvenanceRecord } from "./provenance";
12
+ import { transformIR } from "./transformer";
13
+ import { emitGitlabYaml } from "./emit-yaml";
14
+ import { provenanceToDiagnostics } from "./diagnostics";
15
+ import { GitLabGenerator } from "../../import/generator";
16
+ import { applyComposites } from "./composites/rewriter";
17
+ import type { ActionMappingRegistry } from "./actions/registry";
18
+ // Importing `./actions/index` triggers auto-registration of Tier 1
19
+ // marketplace action mappings into the default registry. This is the
20
+ // single chokepoint where the registry is wired up.
21
+ import "./actions/index";
22
+
23
+ export interface MigrateOptions {
24
+ /** Output format. */
25
+ emit?: "yaml" | "ts";
26
+ /** Enable composite-pattern recognition (Node patterns in v1). */
27
+ useComposites?: boolean;
28
+ /** Source file path for provenance (display only). */
29
+ sourceFile?: string;
30
+ /** Inject a custom action mapping registry for testing. */
31
+ registry?: ActionMappingRegistry;
32
+ /** Escalate needs-review diagnostics to errors. */
33
+ strict?: boolean;
34
+ }
35
+
36
+ export interface MigrationResult {
37
+ /** GitLab IR (consumed by `chant import`-style tools downstream). */
38
+ ir: TemplateIR;
39
+ /** Rendered output (YAML by default, TS when emit: "ts"). */
40
+ output: string;
41
+ /** Per-key provenance records. */
42
+ provenance: ProvenanceRecord[];
43
+ /** SARIF-shaped diagnostics derived from provenance. */
44
+ diagnostics: LintDiagnostic[];
45
+ /** Inferred stage list. */
46
+ stages: string[];
47
+ }
48
+
49
+ /**
50
+ * Migrate a GitHub Actions workflow YAML into GitLab CI.
51
+ *
52
+ * @param yamlContent raw .github/workflows/*.yml content
53
+ * @param opts migration options
54
+ */
55
+ export async function transform(
56
+ yamlContent: string,
57
+ opts: MigrateOptions = {},
58
+ ): Promise<MigrationResult> {
59
+ // Lazy-import the GitHub parser to keep the github lexicon dep optional.
60
+ let GitHubActionsParser: typeof import("@intentius/chant-lexicon-github/import/parser").GitHubActionsParser;
61
+ try {
62
+ ({ GitHubActionsParser } = await import("@intentius/chant-lexicon-github/import/parser"));
63
+ } catch {
64
+ throw new Error(
65
+ "chant migrate from github requires @intentius/chant-lexicon-github. " +
66
+ "Install it: npm install --save-dev @intentius/chant-lexicon-github",
67
+ );
68
+ }
69
+
70
+ const ghIR = new GitHubActionsParser().parse(yamlContent);
71
+ const provAcc = new ProvenanceAccumulator();
72
+ const transformed = await transformIR(ghIR, {
73
+ sourceFile: opts.sourceFile,
74
+ registry: opts.registry,
75
+ provenance: provAcc,
76
+ });
77
+ let { ir } = transformed;
78
+ const stages = transformed.stages;
79
+
80
+ // --use-composites: opt-in IR rewrite that turns recognised shapes
81
+ // (NodePipeline / NodeCI) into composite calls.
82
+ if (opts.useComposites) {
83
+ const r = applyComposites(ir);
84
+ ir = r.ir;
85
+ provAcc.pushAll(r.provenance);
86
+ }
87
+
88
+ let output: string;
89
+ if (opts.emit === "ts") {
90
+ const generator = new GitLabGenerator();
91
+ const files = generator.generate(ir);
92
+ // For single-file emit (default), concatenate. The migration banner
93
+ // + per-resource provenance comments are interleaved.
94
+ const banner = `// Migrated from ${opts.sourceFile ?? "(stdin)"} by chant migrate.
95
+ // Source tool: github-actions. Edit freely — chant build will pick this up.\n\n`;
96
+ output = banner + files.map((f) => f.content).join("\n");
97
+ // Append NeedsReview TODOs at the bottom for visibility.
98
+ const todos = provAcc.byCategory("needs-review");
99
+ if (todos.length > 0) {
100
+ output += "\n// TODO(migration): items needing manual review:\n";
101
+ for (const t of todos) {
102
+ output += `// - ${t.rule}: ${t.note ?? ""}\n`;
103
+ }
104
+ }
105
+ } else {
106
+ output = emitGitlabYaml(ir);
107
+ }
108
+
109
+ const provenance = provAcc.all();
110
+ const diagnostics = provenanceToDiagnostics(provenance, { strict: opts.strict });
111
+
112
+ return { ir, output, provenance, diagnostics, stages };
113
+ }
114
+
115
+ /**
116
+ * Lightweight detector: does this YAML look like a GitHub Actions workflow?
117
+ * Used by the plugin's `migrationSource("github")` extension.
118
+ */
119
+ export function detectGitHubWorkflow(content: string): boolean {
120
+ // Cheap detection: top-level `jobs:` + `on:` or `runs-on:` appearing nested.
121
+ if (!/^\s*jobs\s*:/m.test(content)) return false;
122
+ return /^\s*on\s*:/m.test(content) || /^\s*runs-on\s*:/m.test(content);
123
+ }
124
+
125
+ export { ProvenanceAccumulator } from "./provenance";
126
+ export type { ProvenanceRecord, ProvenanceCategory } from "./provenance";
127
+ export type { ActionMapping, ActionMapCtx, ActionMappedResult, ActionMappingRegistry } from "./actions/registry";
128
+ export { createRegistry, getDefaultRegistry, lookupAction } from "./actions/registry";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Per-node provenance for GitHub Actions → GitLab CI migration.
3
+ *
4
+ * Returned as a side channel from `transform()` so the IR stays clean
5
+ * (re-usable by `chant import`) while still capturing per-key migration
6
+ * history for SARIF/Markdown reporting.
7
+ */
8
+
9
+ export type ProvenanceCategory =
10
+ | "literal" // direct YAML key rename (env → variables, runs-on → image)
11
+ | "rewrite" // expression/identifier substitution (github.ref → $CI_COMMIT_REF_NAME)
12
+ | "synthesis" // emitted GitLab construct with no GH original (inferred stage)
13
+ | "needs-review" // could not translate; emitted comment + diagnostic
14
+ | "skipped" // intentionally dropped
15
+ | "action-map"; // a marketplace action mapping fired
16
+
17
+ export interface ProvenanceRecord {
18
+ /** Where this lives in the output (GitLab IR) — e.g. "jobs.test.script[2]" */
19
+ gitlabPath: string;
20
+ /** Resource logicalId in the GitLab IR, if applicable */
21
+ gitlabLogicalId?: string;
22
+ /** Source file name (for SARIF) */
23
+ sourceFile?: string;
24
+ /** 1-based source line (best effort) */
25
+ sourceLine?: number;
26
+ /** 1-based source column (best effort) */
27
+ sourceColumn?: number;
28
+ /** YAML path in the source (e.g. "jobs.test.steps[1].uses") */
29
+ sourceKey?: string;
30
+ /** What category of translation happened */
31
+ category: ProvenanceCategory;
32
+ /** Rule ID (e.g. "MIG-TRIGGER-001", "ACT-actions-checkout") */
33
+ rule: string;
34
+ /** Free-form explanation for human readers */
35
+ note?: string;
36
+ /** Original action reference (e.g. "actions/checkout@v4") for action-map records */
37
+ actionRef?: string;
38
+ /** Tier 1/2/3 for action-map records */
39
+ mappingTier?: 1 | 2 | 3;
40
+ }
41
+
42
+ /**
43
+ * Accumulator passed through the transformer pipeline so any step can
44
+ * append to the same provenance list without threading return values.
45
+ */
46
+ export class ProvenanceAccumulator {
47
+ private records: ProvenanceRecord[] = [];
48
+
49
+ push(record: ProvenanceRecord): void {
50
+ this.records.push(record);
51
+ }
52
+
53
+ pushAll(records: ProvenanceRecord[]): void {
54
+ this.records.push(...records);
55
+ }
56
+
57
+ all(): ProvenanceRecord[] {
58
+ return [...this.records];
59
+ }
60
+
61
+ byCategory(category: ProvenanceCategory): ProvenanceRecord[] {
62
+ return this.records.filter((r) => r.category === category);
63
+ }
64
+
65
+ needsReviewCount(): number {
66
+ return this.records.filter((r) => r.category === "needs-review").length;
67
+ }
68
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Static LintRule metadata for SARIF enrichment.
3
+ *
4
+ * Each rule fired by the transformer (provenance.rule field) gets a
5
+ * corresponding entry here so SARIF output carries description + helpUri.
6
+ *
7
+ * These rules don't actually run a `check()` — the work has already been
8
+ * done by the transformer. They're metadata-only.
9
+ */
10
+
11
+ import type { LintRule } from "@intentius/chant/lint/rule";
12
+
13
+ const noopCheck = () => [];
14
+
15
+ function rule(id: string, severity: "error" | "warning" | "info", description: string): LintRule {
16
+ return {
17
+ id,
18
+ severity,
19
+ category: "correctness",
20
+ description,
21
+ helpUri: `https://intentius.io/chant/lexicons/gitlab/migration#${id.toLowerCase()}`,
22
+ check: noopCheck,
23
+ };
24
+ }
25
+
26
+ export const MIGRATION_RULES: LintRule[] = [
27
+ // Trigger translations
28
+ rule("MIG-ON-PUSH", "info", "GitHub on: push → workflow.rules push"),
29
+ rule("MIG-ON-PR", "info", "GitHub on: pull_request → workflow.rules merge_request_event"),
30
+ rule("MIG-ON-SCHEDULE", "warning", "GitHub on: schedule requires GitLab CI/CD > Schedules UI configuration"),
31
+ rule("MIG-ON-DISPATCH", "warning", "GitHub on: workflow_dispatch inputs require spec:inputs (GitLab 17+) with defaults"),
32
+ rule("MIG-ON-NON-GIT", "warning", "GitHub event has no GitLab equivalent — GitLab pipelines run on git events only"),
33
+ rule("MIG-ON-UNKNOWN", "warning", "Unknown GitHub trigger event"),
34
+
35
+ // Job-level translations
36
+ rule("MIG-WORKFLOW-NAME", "info", "name → workflow.name"),
37
+ rule("MIG-WORKFLOW-ENV", "info", "Workflow env → top-level variables"),
38
+ rule("MIG-JOB-ENV", "info", "Job env → variables"),
39
+ rule("MIG-TIMEOUT", "info", "timeout-minutes → timeout"),
40
+ rule("MIG-ALLOW-FAILURE", "info", "continue-on-error → allow_failure"),
41
+ rule("MIG-NEEDS", "info", "needs: passthrough"),
42
+ rule("MIG-CONCURRENCY", "info", "concurrency.group/cancel-in-progress → resource_group/interruptible"),
43
+ rule("MIG-SERVICES", "info", "services: passthrough"),
44
+ rule("MIG-CONTAINER", "info", "container.image → image"),
45
+ rule("MIG-MATRIX", "info", "strategy.matrix → parallel.matrix"),
46
+
47
+ // runs-on
48
+ rule("MIG-RUNS-ON-001", "info", "runs-on: linux runner → Docker image"),
49
+ rule("MIG-RUNS-ON-NON-LINUX", "warning", "Non-Linux runs-on requires self-hosted runner with tag"),
50
+ rule("MIG-RUNS-ON-TAG", "info", "Custom runs-on label → tags:"),
51
+
52
+ // Expressions
53
+ rule("MIG-EXPR-CONTEXT", "info", "github.*/runner.*/job.* → predefined $CI_* variable"),
54
+ rule("MIG-EXPR-USERVAR", "info", "env.*/vars.*/secrets.*/inputs.*/matrix.* → $NAME"),
55
+ rule("MIG-EXPR-FUNCTION", "info", "Boolean function → when: clause"),
56
+ rule("MIG-EXPR-NO-EQUIV", "warning", "GitHub expression has no GitLab equivalent"),
57
+ rule("MIG-EXPR-UNKNOWN", "warning", "Could not translate expression"),
58
+
59
+ // Rule conversions
60
+ rule("MIG-IF-WHEN", "info", "if: boolean_function() → when:"),
61
+
62
+ // Permissions and outputs
63
+ rule("MIG-PERMISSIONS-001", "warning", "GitHub permissions: has no per-job equivalent — configure CI/CD token at project level"),
64
+ rule("MIG-JOB-OUTPUTS", "warning", "GitHub job outputs require artifacts:reports:dotenv pattern in GitLab"),
65
+ rule("MIG-NEEDS-OUTPUTS-001", "warning", "steps/needs outputs require artifacts:reports:dotenv pattern"),
66
+
67
+ // Matrix
68
+ rule("MIG-MATRIX-INCLUDE-001", "warning", "matrix.include/exclude has no direct GitLab equivalent"),
69
+ rule("MIG-FAIL-FAST", "warning", "strategy.fail-fast has no GitLab equivalent (default behaviour is non-fail-fast)"),
70
+
71
+ // Reusable workflow + step env
72
+ rule("MIG-REUSABLE-WORKFLOW", "warning", "GitHub reusable workflow → GitLab include: with variable substitution (no typed inputs)"),
73
+ rule("MIG-STEP-ENV-CONFLICT", "warning", "Step-level env var conflict; using last value"),
74
+
75
+ // Stage inference
76
+ rule("MIG-STAGE-TOPO", "info", "Stage assigned by needs: depth"),
77
+ rule("MIG-STAGE-HEURISTIC", "info", "Stage assigned by job name heuristic"),
78
+ rule("MIG-NEEDS-CYCLE-001", "warning", "needs: cycle detected; each cycle member in its own stage"),
79
+
80
+ // Action mapping fallback
81
+ rule("MIG-ACTION-UNKNOWN", "warning", "Marketplace action has no registered mapping"),
82
+ ];