@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
package/README.md CHANGED
@@ -27,6 +27,10 @@ npm install --save-dev @intentius/chant @intentius/chant-lexicon-gitlab
27
27
 
28
28
  If a concrete observation use case surfaces, file a focused issue rather than retrofitting `listArtifacts()` to fit.
29
29
 
30
+ ## GitHub Actions migration
31
+
32
+ `chant migrate` translates `.github/workflows/*.yml` into `.gitlab-ci.yml` or typed chant TypeScript. The transformer, action-mapping registry, and provenance model are ported from `gitlab-org/ci-cd/github-actions-to-gitlab-ci` (MIT). See [Migration](https://intentius.io/chant/lexicons/gitlab/migration/) and [`ATTRIBUTIONS.md`](./ATTRIBUTIONS.md).
33
+
30
34
  ## License
31
35
 
32
36
  See the main project LICENSE file.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "bf526b6f9f73e10cebc3f09f09a5f47a4209f0f227b2cdab8a937fadb490441a",
4
+ "manifest.json": "1121cc5d536544ee7de9b5326c5aa47929f4ac40591d4269e1e50e428f7c32dc",
5
5
  "meta.json": "931fc3246a55645b1493080bbeb160e5d205e42349e8bdca96ca243adb5f0da3",
6
6
  "types/index.d.ts": "5cd2e99f135a929b72bdd822d00d780d39b1cd407ba0cfb3d511c7ac9d667b58",
7
7
  "rules/artifact-no-expiry.ts": "3f3cabf9792cbf8207e53a25f506715466b19ec25e9c3b4d0d77fed6b2eb4542",
@@ -29,7 +29,8 @@
29
29
  "rules/wgl028.ts": "f1c64eda25e2ad8da634d64a397f877a46ae8327713db5cf3193882bb9b96453",
30
30
  "rules/yaml-helpers.ts": "f2322530a7fd812e482c5a7df05d0d05787b207574b67b811776786e00fd3e18",
31
31
  "skills/chant-gitlab.md": "1e26a0c50ea891423479bd7fec3bc133f9185727c4a3eaadd525d7e7436056b6",
32
+ "skills/chant-gitlab-migrate.md": "9402ed4fcfd25971cf72f8f02a93c43aadfed3b83876081b92711562ce1bdc34",
32
33
  "skills/chant-gitlab-patterns.md": "cefda1b656f222293d16381dfbf452b2b376b545a69f62e55dc38907fe8aa7fa"
33
34
  },
34
- "composite": "9e350e4b9ef3118fd4eec7e9b3eaef2c5477f825cbffbae60aecf24ca7fd731a"
35
+ "composite": "95153640312955ba3ebf46f26199a5c749cf81ae007ffdaa1b38f23148ca6464"
35
36
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitLab",
6
6
  "intrinsics": [
@@ -0,0 +1,117 @@
1
+ ---
2
+ skill: chant-gitlab-migrate
3
+ description: Translate GitHub Actions workflows into GitLab CI/CD pipelines via chant migrate
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Chant GitHub Actions → GitLab CI/CD Migration
8
+
9
+ ## When to invoke this skill
10
+
11
+ The user wants to translate a `.github/workflows/*.yml` workflow into a `.gitlab-ci.yml` pipeline. Trigger phrases include:
12
+
13
+ - "migrate this GitHub workflow to GitLab"
14
+ - "convert .github/workflows/ci.yml to GitLab CI"
15
+ - pasting a GitHub workflow and asking "how would this look in GitLab"
16
+ - evaluating GitLab CI/CD from a GitHub Actions background
17
+ - "what's the GitLab equivalent of <action>"
18
+
19
+ This skill is the operational glue around `chant migrate`. The translation logic lives in `@intentius/chant-lexicon-gitlab`; this skill knows how to invoke it, surface the report, and suggest GitLab-only upgrade moments.
20
+
21
+ ## Distinction from the upstream GitLab skill
22
+
23
+ The upstream `gitlab-org/ci-cd/github-actions-to-gitlab-ci` Agent Skill (MIT) translates workflows by direct LLM prompting — stateless, freehand each run. `chant migrate` ports the same translation rules into a typed compiler so the output is reproducible, testable, and re-runnable. The skills are complementary:
24
+
25
+ - Use the upstream skill for one-shot read-and-respond ("here's my YAML, paste the answer").
26
+ - Use `chant migrate` when the translation has to survive evolution of the source workflow.
27
+
28
+ ## Step 1: Detect
29
+
30
+ Confirm the input is a GitHub Actions workflow. Heuristic: top-level `jobs:` plus either `on:` or per-job `runs-on:`. If you're given a path, read the file. If pasted inline, work from the paste.
31
+
32
+ ## Step 2: Dry-run migrate
33
+
34
+ Run with no `--strict`, no `--validate` first so the user sees the full translation including any NeedsReview items:
35
+
36
+ ```bash
37
+ npx chant migrate path/to/workflow.yml --output /tmp/proposed.gitlab-ci.yml --report /tmp/migration.sarif
38
+ ```
39
+
40
+ Or for inline content, pipe via a temp file:
41
+
42
+ ```bash
43
+ TMPF=$(mktemp --suffix=.yml) && cat > "$TMPF" <<'EOF'
44
+ <paste workflow here>
45
+ EOF
46
+ npx chant migrate "$TMPF" --output /tmp/proposed.gitlab-ci.yml --report /tmp/migration.sarif
47
+ ```
48
+
49
+ The Markdown summary always prints to stderr. The SARIF v2.1.0 report goes to `--report`.
50
+
51
+ ## Step 3: Review NeedsReview items
52
+
53
+ The transformer surfaces categorised provenance. The categories are:
54
+
55
+ | Category | Severity (default) | Meaning |
56
+ |---|---|---|
57
+ | literal | (none, no diagnostic) | Direct key rename (env → variables) |
58
+ | rewrite | (none, no diagnostic) | Expression substitution (github.ref → $CI_COMMIT_REF_NAME) |
59
+ | synthesis | (none, no diagnostic) | Emitted construct with no GH original (inferred stage) |
60
+ | action-map tier 3 | warning | Marketplace action mapping that requires manual review |
61
+ | skipped | info | Intentionally dropped (actions/checkout) |
62
+ | needs-review | warning | No clean translation — user must decide |
63
+
64
+ Common needs-review rules and the right response:
65
+
66
+ - **MIG-PERMISSIONS-001**: GitHub `permissions:` has no per-job equivalent. Configure CI/CD token access at project level (Settings > CI/CD > Token Access).
67
+ - **MIG-ON-SCHEDULE**: GitLab cron schedules live in the UI under CI/CD > Schedules, not in YAML. The workflow rule (`$CI_PIPELINE_SOURCE == "schedule"`) is emitted; the schedule itself needs UI setup.
68
+ - **MIG-ON-DISPATCH**: `workflow_dispatch` inputs require `spec:inputs` (GitLab 17+) with defaults on every input (auto-triggered pipelines can't prompt).
69
+ - **MIG-NEEDS-OUTPUTS-001**: GitHub job outputs require the `artifacts:reports:dotenv` pattern in GitLab. Manual rewire needed.
70
+ - **MIG-ACTION-UNKNOWN**: A marketplace action has no registered mapping. Either replace with an inline script, or add an `--action-mapping <file>` extension (future flag).
71
+
72
+ ## Step 4: Validate (optional)
73
+
74
+ If the user has `glci` or `glab` installed locally, run validation:
75
+
76
+ ```bash
77
+ npx chant migrate path/to/workflow.yml --output .gitlab-ci.yml --validate
78
+ ```
79
+
80
+ The CLI picks `glci` (offline, no auth) first; `glab ci lint` second. If neither is on PATH, `--validate` warns and skips. Pair with `--strict` to make validation failures hard.
81
+
82
+ ## Step 5: Decide the emit mode
83
+
84
+ Two modes serve different intents:
85
+
86
+ - `--emit yaml` (default): produces `.gitlab-ci.yml` directly. Right when the user wants the YAML and is done with chant going forward.
87
+ - `--emit ts`: produces chant TypeScript source the user owns. Right when the user wants to maintain the pipeline in chant — they can edit the typed source and rebuild via `chant build` to refresh the YAML.
88
+
89
+ When in doubt, do `--emit yaml` first to validate the translation, then offer `--emit ts` as the long-term ownership option.
90
+
91
+ ## Step 6: Suggest the upgrade moments
92
+
93
+ After surfacing the literal translation, opportunistically suggest GitLab-native features that improve on the GitHub original. Only suggest when they actually apply:
94
+
95
+ - `--use-composites` recognises Node-shaped pipelines and emits `NodePipeline({...})` or `NodeCI({...})` instead of raw `Job` constructors. The output is 5-10x shorter and easier to maintain.
96
+ - DAG `needs:` removes stage barriers — jobs run as soon as deps complete (already handled by the transformer for explicit `needs:`).
97
+ - `rules:changes:` enables monorepo path-based job filtering — no GitHub equivalent.
98
+ - `resource_group:` serialises deploys to the same environment (covered when GH `concurrency.group:` is translated).
99
+ - `include:` shares config across projects and workflows.
100
+ - GitLab CI templates (`Auto-DevOps`, `Security/SAST`, `Terraform/Base`) — no GitHub Actions equivalent.
101
+
102
+ ## Self-check rubric
103
+
104
+ Before reporting "done" to the user, verify:
105
+
106
+ - [ ] Was `actions/checkout` removed (GitLab clones automatically)?
107
+ - [ ] Was `runs-on:` translated to `image:` (Linux runners) or `tags:` (self-hosted)?
108
+ - [ ] Did every `uses:` step either map cleanly or get flagged with NeedsReview?
109
+ - [ ] Did `if:` conditions translate to `rules:if:` (NOT a string substitution; semantics differ)?
110
+ - [ ] Were `permissions:` items documented as manual project-level setup?
111
+ - [ ] Was the migration report (Markdown + SARIF) surfaced to the user?
112
+ - [ ] Were `NeedsReview` items called out explicitly?
113
+ - [ ] Did I suggest `--use-composites` if Node patterns were detected?
114
+
115
+ ## Inspired by
116
+
117
+ `gitlab-org/ci-cd/github-actions-to-gitlab-ci` — MIT. The trigger-phrase patterns, the four-category report shape, and the "suggest GitLab improvements" step are direct ports. The difference: this skill *calls* a compiler instead of *being* one.
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "description": "GitLab CI lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/intentius/chant.git",
9
+ "url": "https://github.com/INTENTIUS/chant.git",
10
10
  "directory": "lexicons/gitlab"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/intentius/chant/issues"
13
+ "url": "https://github.com/INTENTIUS/chant/issues"
14
14
  },
15
15
  "keywords": [
16
16
  "infrastructure-as-code",
@@ -44,9 +44,16 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@intentius/chant": "*",
47
+ "@intentius/chant-lexicon-github": "*",
47
48
  "typescript": "^5.9.3"
48
49
  },
49
50
  "peerDependencies": {
50
- "@intentius/chant": "^0.1.0"
51
+ "@intentius/chant": "^0.1.0",
52
+ "@intentius/chant-lexicon-github": "^0.1.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@intentius/chant-lexicon-github": {
56
+ "optional": true
57
+ }
51
58
  }
52
59
  }
@@ -17,6 +17,17 @@ const TYPE_TO_CLASS: Record<string, string> = {
17
17
  "GitLab::CI::Workflow": "Workflow",
18
18
  };
19
19
 
20
+ /**
21
+ * Composite IR types emit as `FunctionName({...})` factory calls rather
22
+ * than `new ClassName({...})` constructors. The `chant migrate --use-
23
+ * composites` rewriter rewrites recognised IR shapes into these sentinel
24
+ * types (see `lexicons/gitlab/src/migrate/from-github/composites/`).
25
+ */
26
+ const COMPOSITE_TYPE_TO_FN: Record<string, string> = {
27
+ "GitLab::Composite::NodePipeline": "NodePipeline",
28
+ "GitLab::Composite::NodeCI": "NodeCI",
29
+ };
30
+
20
31
  /**
21
32
  * Properties that reference known property entities.
22
33
  */
@@ -45,6 +56,8 @@ export class GitLabGenerator implements TypeScriptGenerator {
45
56
  for (const resource of ir.resources) {
46
57
  const cls = TYPE_TO_CLASS[resource.type];
47
58
  if (cls) usedConstructors.add(cls);
59
+ const fn = COMPOSITE_TYPE_TO_FN[resource.type];
60
+ if (fn) usedConstructors.add(fn);
48
61
 
49
62
  // Check properties for nested constructors
50
63
  this.collectNestedConstructors(resource.properties, usedConstructors);
@@ -78,12 +91,17 @@ export class GitLabGenerator implements TypeScriptGenerator {
78
91
  // Emit resources
79
92
  for (const resource of ir.resources) {
80
93
  const cls = TYPE_TO_CLASS[resource.type];
81
- if (!cls) continue;
94
+ const fn = COMPOSITE_TYPE_TO_FN[resource.type];
95
+ if (!cls && !fn) continue;
82
96
 
83
97
  const varName = resource.logicalId;
84
98
  const propsStr = this.emitProps(resource.properties, 1);
85
99
 
86
- lines.push(`export const ${varName} = new ${cls}(${propsStr});`);
100
+ if (fn) {
101
+ lines.push(`export const ${varName} = ${fn}(${propsStr});`);
102
+ } else {
103
+ lines.push(`export const ${varName} = new ${cls}(${propsStr});`);
104
+ }
87
105
  lines.push("");
88
106
  }
89
107
 
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Action mapping registry — public surface.
3
+ *
4
+ * Tier 1 mappings auto-register into the default registry on import of
5
+ * this module. Tier 2 and Tier 3 are added in #88.
6
+ */
7
+
8
+ export type {
9
+ ActionMapping,
10
+ ActionMapCtx,
11
+ ActionMappedResult,
12
+ ActionMappingRegistry,
13
+ } from "./registry";
14
+ export { createRegistry, getDefaultRegistry, lookupAction, setDefaultRegistry } from "./registry";
15
+
16
+ import { registerTier1 } from "./tier-1";
17
+ import { registerTier2 } from "./tier-2";
18
+ import { registerTier3 } from "./tier-3";
19
+ import { getDefaultRegistry } from "./registry";
20
+
21
+ // Auto-register Tiers 1/2/3 into the default registry the first time
22
+ // this module is imported.
23
+ registerTier1(getDefaultRegistry());
24
+ registerTier2(getDefaultRegistry());
25
+ registerTier3(getDefaultRegistry());
26
+
27
+ export { registerTier1, registerTier2, registerTier3 };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * ActionMapping registry for GitHub Actions → GitLab CI translation.
3
+ *
4
+ * Each marketplace action gets a typed mapping that returns the GitLab-side
5
+ * effects (script lines, image substitution, services, cache, artifacts,
6
+ * job-level variables) plus provenance records.
7
+ *
8
+ * v1 ships a built-in registry covering 33 actions (Tier 1/2/3, see
9
+ * `tier-1.ts`/`tier-2.ts`/`tier-3.ts`). Users can inject their own
10
+ * mappings via `MigrateOptions.registry`.
11
+ */
12
+
13
+ import type { ProvenanceRecord } from "../provenance";
14
+
15
+ export interface ActionMapCtx {
16
+ /** GitLab IR job logicalId */
17
+ logicalId: string;
18
+ /** Original GitHub job name (kebab-case) */
19
+ jobName: string;
20
+ /** Source file (for provenance) */
21
+ sourceFile?: string;
22
+ /** 0-based step index within the job */
23
+ stepIndex: number;
24
+ }
25
+
26
+ export interface ActionMappedResult {
27
+ /** Lines appended to the GitLab job `script:` */
28
+ scriptLines: string[];
29
+ /** Lines prepended to the GitLab job `before_script:` */
30
+ beforeScript?: string[];
31
+ /** Job-level image override (last write wins) */
32
+ image?: string;
33
+ /** Job-level services to add */
34
+ services?: unknown[];
35
+ /** Job-level cache config */
36
+ cache?: Record<string, unknown>;
37
+ /** Job-level artifacts config */
38
+ artifacts?: Record<string, unknown>;
39
+ /** Job-level variables to merge */
40
+ variables?: Record<string, unknown>;
41
+ /** Provenance records for this mapping firing */
42
+ provenance: ProvenanceRecord[];
43
+ }
44
+
45
+ export interface ActionMapping {
46
+ /** Action name without version, e.g. "actions/checkout" */
47
+ actionName: string;
48
+ /** Optional version filter (matches against major version) */
49
+ majorVersions?: string[];
50
+ /** Tier (1: must-have, 2: common, 3: niche) */
51
+ tier: 1 | 2 | 3;
52
+ /** Apply the mapping to a step */
53
+ translate(step: Record<string, unknown>, ctx: ActionMapCtx): ActionMappedResult;
54
+ }
55
+
56
+ export interface ActionMappingRegistry {
57
+ register(mapping: ActionMapping): void;
58
+ lookup(actionRef: string): ActionMapping | undefined;
59
+ }
60
+
61
+ class DefaultRegistry implements ActionMappingRegistry {
62
+ private byName = new Map<string, ActionMapping[]>();
63
+
64
+ register(mapping: ActionMapping): void {
65
+ const arr = this.byName.get(mapping.actionName) ?? [];
66
+ arr.push(mapping);
67
+ this.byName.set(mapping.actionName, arr);
68
+ }
69
+
70
+ lookup(actionRef: string): ActionMapping | undefined {
71
+ // actionRef is e.g. "actions/checkout@v4" or "docker/build-push-action@v5"
72
+ const [name, version] = actionRef.split("@");
73
+ const candidates = this.byName.get(name);
74
+ if (!candidates || candidates.length === 0) return undefined;
75
+ if (!version) return candidates[0];
76
+ // Find a mapping whose majorVersions includes this version (best effort)
77
+ const major = version.replace(/^v/, "").split(".")[0];
78
+ for (const c of candidates) {
79
+ if (!c.majorVersions || c.majorVersions.length === 0) return c;
80
+ if (c.majorVersions.includes(`v${major}`) || c.majorVersions.includes(major)) {
81
+ return c;
82
+ }
83
+ }
84
+ // No version match — fall back to first registered
85
+ return candidates[0];
86
+ }
87
+ }
88
+
89
+ let defaultRegistryInstance: ActionMappingRegistry | undefined;
90
+
91
+ export function getDefaultRegistry(): ActionMappingRegistry {
92
+ if (!defaultRegistryInstance) {
93
+ defaultRegistryInstance = new DefaultRegistry();
94
+ }
95
+ return defaultRegistryInstance;
96
+ }
97
+
98
+ /**
99
+ * Test-only: replace the default registry. Used by `lookupAction` callers
100
+ * that pass `opts.registry` to inject a stub. Reset to undefined to restore.
101
+ */
102
+ export function setDefaultRegistry(registry: ActionMappingRegistry | undefined): void {
103
+ defaultRegistryInstance = registry;
104
+ }
105
+
106
+ export function createRegistry(): ActionMappingRegistry {
107
+ return new DefaultRegistry();
108
+ }
109
+
110
+ export function lookupAction(actionRef: string, registry?: ActionMappingRegistry): ActionMapping | undefined {
111
+ return (registry ?? getDefaultRegistry()).lookup(actionRef);
112
+ }
@@ -0,0 +1,128 @@
1
+ import { describe, test, expect, beforeAll } from "vitest";
2
+ import { createRegistry, lookupAction } from "./registry";
3
+ import { registerTier1 } from "./tier-1";
4
+ import type { ActionMapCtx } from "./registry";
5
+
6
+ const ctx: ActionMapCtx = { logicalId: "build", jobName: "build", stepIndex: 0 };
7
+
8
+ const reg = createRegistry();
9
+ beforeAll(() => registerTier1(reg));
10
+
11
+ function callAction(uses: string, withProps: Record<string, unknown> = {}) {
12
+ const m = lookupAction(uses, reg);
13
+ if (!m) throw new Error(`No mapping for ${uses}`);
14
+ return m.translate({ uses, with: withProps }, ctx);
15
+ }
16
+
17
+ describe("Tier 1 mappings", () => {
18
+ test("actions/checkout removes step + may set GIT_DEPTH", () => {
19
+ const r = callAction("actions/checkout@v4");
20
+ expect(r.scriptLines).toEqual([]);
21
+ expect(r.provenance[0].category).toBe("skipped");
22
+ });
23
+
24
+ test("actions/checkout with fetch-depth sets GIT_DEPTH", () => {
25
+ const r = callAction("actions/checkout@v4", { "fetch-depth": 0 });
26
+ expect(r.variables?.GIT_DEPTH).toBe(0);
27
+ });
28
+
29
+ test("actions/setup-node sets image: node:<v>", () => {
30
+ const r = callAction("actions/setup-node@v4", { "node-version": "20" });
31
+ expect(r.image).toBe("node:20");
32
+ });
33
+
34
+ test("actions/setup-node with cache: npm sets cache paths", () => {
35
+ const r = callAction("actions/setup-node@v4", { "node-version": "20", cache: "npm" });
36
+ expect(r.cache?.paths).toEqual([".npm/"]);
37
+ });
38
+
39
+ test("actions/setup-python sets image: python:<v>", () => {
40
+ const r = callAction("actions/setup-python@v5", { "python-version": "3.11" });
41
+ expect(r.image).toBe("python:3.11");
42
+ });
43
+
44
+ test("actions/setup-java sets eclipse-temurin", () => {
45
+ const r = callAction("actions/setup-java@v3", { "java-version": "17" });
46
+ expect(r.image).toBe("eclipse-temurin:17");
47
+ });
48
+
49
+ test("actions/setup-go sets image: golang:<v>", () => {
50
+ const r = callAction("actions/setup-go@v5", { "go-version": "1.21" });
51
+ expect(r.image).toBe("golang:1.21");
52
+ });
53
+
54
+ test("actions/setup-ruby sets image: ruby:<v>", () => {
55
+ const r = callAction("actions/setup-ruby@v1", { "ruby-version": "3.3" });
56
+ expect(r.image).toBe("ruby:3.3");
57
+ });
58
+
59
+ test("actions/cache → cache: keyword", () => {
60
+ const r = callAction("actions/cache@v3", { path: "node_modules\n.npm", key: "v1-${{ runner.os }}" });
61
+ expect(r.cache?.paths).toEqual(["node_modules", ".npm"]);
62
+ expect(r.cache?.key).toContain("v1-");
63
+ });
64
+
65
+ test("actions/upload-artifact → artifacts: keyword", () => {
66
+ const r = callAction("actions/upload-artifact@v4", { name: "out", path: "dist", "retention-days": 7 });
67
+ expect(r.artifacts?.paths).toEqual(["dist"]);
68
+ expect(r.artifacts?.name).toBe("out");
69
+ expect(r.artifacts?.expire_in).toBe("7 days");
70
+ });
71
+
72
+ test("actions/download-artifact → no-op (GitLab auto-passes)", () => {
73
+ const r = callAction("actions/download-artifact@v4");
74
+ expect(r.scriptLines).toEqual([]);
75
+ });
76
+
77
+ test("docker/login-action emits docker login", () => {
78
+ const r = callAction("docker/login-action@v3", {
79
+ registry: "ghcr.io",
80
+ username: "${{ github.actor }}",
81
+ password: "${{ secrets.GH_TOKEN }}",
82
+ });
83
+ expect(r.scriptLines.join("\n")).toContain("docker login");
84
+ expect(r.scriptLines.join("\n")).toContain("ghcr.io");
85
+ });
86
+
87
+ test("docker/build-push-action emits docker build/push + dind", () => {
88
+ const r = callAction("docker/build-push-action@v5", {
89
+ tags: "ghcr.io/me/img:latest",
90
+ push: true,
91
+ });
92
+ expect(r.image).toBe("docker:latest");
93
+ expect(r.services?.[0]).toMatchObject({ name: "docker:dind" });
94
+ expect(r.scriptLines.some((l) => l.startsWith("docker build"))).toBe(true);
95
+ expect(r.scriptLines.some((l) => l.startsWith("docker push"))).toBe(true);
96
+ });
97
+
98
+ test("docker/setup-buildx-action emits buildx create", () => {
99
+ const r = callAction("docker/setup-buildx-action@v3");
100
+ expect(r.scriptLines.some((l) => l.includes("buildx create"))).toBe(true);
101
+ expect(r.image).toBe("docker:latest");
102
+ });
103
+
104
+ test("docker/setup-qemu-action emits binfmt", () => {
105
+ const r = callAction("docker/setup-qemu-action@v3", { platforms: "linux/arm64" });
106
+ expect(r.scriptLines.some((l) => l.includes("tonistiigi/binfmt"))).toBe(true);
107
+ });
108
+
109
+ test("actions/github-script → needs-review with TODO comment", () => {
110
+ const r = callAction("actions/github-script@v7");
111
+ expect(r.scriptLines.some((l) => l.startsWith("# TODO"))).toBe(true);
112
+ expect(r.provenance[0].category).toBe("needs-review");
113
+ });
114
+
115
+ test("registry contains all 14 Tier 1 actions", () => {
116
+ const names = [
117
+ "actions/checkout", "actions/setup-node", "actions/setup-python",
118
+ "actions/setup-java", "actions/setup-go", "actions/setup-ruby",
119
+ "actions/cache", "actions/upload-artifact", "actions/download-artifact",
120
+ "docker/login-action", "docker/build-push-action",
121
+ "docker/setup-buildx-action", "docker/setup-qemu-action",
122
+ "actions/github-script",
123
+ ];
124
+ for (const n of names) {
125
+ expect(lookupAction(`${n}@v1`, reg)).toBeDefined();
126
+ }
127
+ });
128
+ });