Migration
Show the whole prompt
Read https://intentius.io/chant/lexicons/gitlab/migration/.
Run `npx chant migrate <workflow> --output .gitlab-ci.yml --validate --report migration.sarif` for each GitHub Actions workflow I name, or `--emit ts` when I want chant source to keep.
List every NeedsReview item (MIG-...) and security finding it reports, with the change each needs, then run `npx chant lint src` on any emitted source.
Open a pull request with the output and that list. Keep the GitHub workflows, and do not set GitLab project settings or schedules yourself.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicons/gitlab/migration/.Run `npx chant migrate <workflow> --output .gitlab-ci.yml --validate --report migration.sarif` for each GitHub Actions workflow I name, or `--emit ts` when I want chant source to keep.List every NeedsReview item (MIG-...) and security finding it reports, with the change each needs, then run `npx chant lint src` on any emitted source.Open a pull request with the output and that list. Keep the GitHub workflows, and do not set GitLab project settings or schedules yourself.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.chant migrate translates .github/workflows/*.yml into .gitlab-ci.yml or chant TypeScript source. It applies the translation rules of the upstream gitlab-org/ci-cd/github-actions-to-gitlab-ci Agent Skill as a deterministic compiler you can test and re-run.
Install
Section titled “Install”npm install --save-dev @intentius/chant @intentius/chant-lexicon-gitlabThe migration code lives in the GitLab lexicon and is invoked via the core chant CLI. The GitHub lexicon is loaded as an optional peer at runtime, so install it too if you want the parser available immediately:
npm install --save-dev @intentius/chant-lexicon-githubQuickstart
Section titled “Quickstart”After installing locally:
npx chant migrate .github/workflows/ci.yml --output .gitlab-ci.ymlThat writes .gitlab-ci.yml and prints a Markdown summary to stderr. For CI-friendly machine-readable output, add --report migration.sarif.
If you want to run without installing into a project, use npx with explicit package resolution:
npx -p @intentius/chant -p @intentius/chant-lexicon-gitlab \ chant migrate .github/workflows/ci.yml --output .gitlab-ci.ymlWhen to use migrate vs writing chant source from scratch
Section titled “When to use migrate vs writing chant source from scratch”- Migrate: you have an existing GitHub Actions workflow and want the equivalent GitLab pipeline (or chant source) without manual translation. Best for one-time conversions and for evaluating GitLab CI/CD.
- Write from scratch: you’re starting greenfield. Use
chant init --lexicon gitlab --template node-pipelineand write your pipeline directly in typed chant.
| Flag | Default | Purpose |
|---|---|---|
--from <name> | github | Source lexicon (only github supported in v1) |
--to <name> | gitlab | Target lexicon |
--output <path> | stdout | Where to write the rendered output |
--emit <fmt> | yaml | yaml (one-shot) or ts (own-it-going-forward chant source) |
--report <path> | — | SARIF v2.1.0 report file path |
--strict | off | Exit non-zero if any needs-review diagnostic is emitted |
--validate | off | Run glci or glab ci lint against the emitted YAML |
--use-composites | off | Recognise Node patterns and emit NodePipeline({...}) calls |
Two emit modes
Section titled “Two emit modes”--emit yaml writes .gitlab-ci.yml directly. Best when you want the YAML and you’re done with chant going forward.
--emit ts writes typed chant source. Best when you want to maintain the pipeline as code: edit the TypeScript, rebuild with chant build src/ --output .gitlab-ci.yml to refresh.
What translates literally
Section titled “What translates literally”These GitHub keys map directly to GitLab equivalents:
| GitHub | GitLab | Notes |
|---|---|---|
name | workflow.name | Pipeline display name |
on: push | workflow.rules with $CI_PIPELINE_SOURCE == "push" | |
on: pull_request | workflow.rules with $CI_PIPELINE_SOURCE == "merge_request_event" | |
runs-on: ubuntu-* | image: ubuntu:<v> | Linux only |
runs-on: [self-hosted, gpu] | tags: [self-hosted, gpu] | Multi-label maps to tags |
env: (workflow/job) | variables: | |
if: | rules: [{ if: ... }] | Identifiers substituted to $CI_* |
needs: | needs: | Passthrough |
strategy.matrix | parallel.matrix | Keys uppercased |
timeout-minutes | timeout | Stringified "N minutes" |
continue-on-error | allow_failure | |
concurrency.group | resource_group | |
concurrency.cancel-in-progress | interruptible | |
container.image | image | |
services | services | Passthrough |
Mapped marketplace actions
Section titled “Mapped marketplace actions”33 actions are mapped out of the box (lifted from the upstream skill’s references/marketplace-actions.md):
| Tier | Examples | Translation pattern |
|---|---|---|
| 1 (essential) | actions/checkout, actions/setup-{node,python,java,go,ruby}, actions/{cache,upload-artifact,download-artifact}, docker/{login,build-push,setup-buildx,setup-qemu}-action, actions/github-script | image substitution, native cache/artifacts, or skipped |
| 2 (common) | actions/setup-dotnet, shivammathur/setup-php, aws-actions/configure-aws-credentials, google-github-actions/auth, azure/login, hashicorp/setup-terraform, codecov/codecov-action, softprops/action-gh-release, peter-evans/create-pull-request, JamesIves/github-pages-deploy-action, pnpm/action-setup, oven-sh/setup-bun, gradle/gradle-build-action, cypress-io/github-action | image, script, or needs-review with scaffolding |
| 3 (niche) | tj-actions/changed-files, dorny/paths-filter, nick-fields/retry, pre-commit/action, slackapi/slack-github-action | inline script or hint to native GitLab keyword |
Unknown uses: references emit MIG-ACTION-UNKNOWN with a TODO comment in the output.
NeedsReview rules
Section titled “NeedsReview rules”Items the transformer flags for manual review (severity: warning, escalates to error under --strict):
| Rule | What it catches |
|---|---|
MIG-PERMISSIONS-001 | GitHub permissions: has no per-job equivalent. Configure CI/CD token access at project level. |
MIG-ON-SCHEDULE | Cron schedules live in GitLab UI under CI/CD > Schedules, not YAML. |
MIG-ON-DISPATCH | workflow_dispatch inputs need spec:inputs (GitLab 17+) with defaults on every input. |
MIG-ON-NON-GIT | GitLab pipelines only run on git events; issues/release/discussion events have no equivalent. |
MIG-NEEDS-OUTPUTS-001 | steps.X.outputs / needs.X.outputs require the artifacts:reports:dotenv pattern. |
MIG-MATRIX-INCLUDE-001 | matrix.include / matrix.exclude have no direct GitLab equivalent. |
MIG-FAIL-FAST | strategy.fail-fast: true has no GitLab equivalent (default is non-fail-fast). |
MIG-REUSABLE-WORKFLOW | GitHub uses: org/repo/.github/workflows/x.yml maps to GitLab include: (no typed inputs). |
MIG-RUNS-ON-NON-LINUX | macOS/Windows runners need self-hosted GitLab runners with appropriate tags. |
MIG-EXPR-NO-EQUIV | Specific GitHub expressions have no GitLab predefined variable (runner.os, github.run_attempt, etc.). |
MIG-NEEDS-CYCLE-001 | needs: cycle detected; each member assigned its own stage. |
MIG-ACTION-UNKNOWN | Marketplace action has no registered mapping. |
--use-composites (the upgrade moment)
Section titled “--use-composites (the upgrade moment)”When the transformer recognises a Node-shaped pipeline, --use-composites collapses it into a single composite call:
Before (literal, 25 lines of TypeScript):
export const build = new Job({ image: "node:22", script: ["npm ci", "npm run build"], stage: "build", // ...});export const test = new Job({ image: "node:22", script: ["npm ci", "npm test"], stage: "test", needs: ["build"],});After (with --use-composites, 4 lines):
export const app = NodePipeline({ nodeVersion: "22", packageManager: "npm",});v1 recognises:
- NodePipeline: 2 jobs (build + test), both
image: node:*, package-manager autodetected from install commands. - NodeCI: single job with
image: node:*+ install + (build or test).
Other shapes (Python, Docker, Go) are deferred follow-ups in the umbrella issue.
Validating the output
Section titled “Validating the output”--validate runs two backends against the migrated pipeline:
- Syntax: a local validator,
glci lint(preferred: offline, no auth), falling back toglab ci lint(requiresglab auth login). If neither tool is on PATH, syntax validation is skipped with a warning. - Security — the GitLab security post-synth checks (WGL029–048) run against the emitted
.gitlab-ci.yml. Anything lost in translation that leaves a weakness in the GitLab pipeline is caught here, by the same checks that run on any emitted GitLab YAML.
Pair with --strict to hard-fail on missing tools, syntax errors, or error-severity security findings.
Install glci:
curl -fsSL https://gitlab.com/gitlab-org/ci-cd/runner-tools/glci/-/raw/main/install.sh | bashInstall glab: see GitLab CLI install docs.
Diagnostics and the SARIF report
Section titled “Diagnostics and the SARIF report”Every translation decision generates a provenance record. Records categorised as needs-review (or action-map tier 3) become SARIF diagnostics:
chant migrate workflow.yml --output .gitlab-ci.yml --report migration.sarifThe SARIF is v2.1.0 and ingests by GitHub’s codeql-action/upload-sarif or GitLab’s CI integrations. Each rule carries description + helpUri linking back to this page.
Security posture
Section titled “Security posture”Migration sits between the github and gitlab supply-chain checks. Security properties do not translate 1:1: some carry across silently, some are lost, some need re-establishing on the GitLab side. Alongside the functional provenance (Translated / Approximated / NeedsReview / Lost), chant migrate classifies each security-relevant property’s fate across the boundary and emits a Security posture section in the Markdown report.
| Property (GH → GL) | Fate | Rule | Why |
|---|---|---|---|
| Pinned action SHA | Lost | MIG-PIN-LOST | A GitHub SHA pin has no meaning once the action maps to a GitLab include/image — re-pin the target (WGL029 / WGL031). |
Least-privilege permissions: | NeedsReview | MIG-PERMISSIONS-001 | No per-job YAML equivalent; lives in project token settings + id_tokens scoping (WGL033). |
| Secret reference | NeedsReview | MIG-SECRET-UNSCOPED | ${{ secrets.X }} → $X assumes a masked + protected GitLab variable, set outside the YAML (WGL038). |
| Untrusted-input injection | Translated (silently) | MIG-INJECTION-CARRIED | An injection site translates verbatim — the risk carries across (WGL035). |
| Trigger / fork trust boundary | NeedsReview | MIG-TRUST-BOUNDARY | pull_request_target → MR/fork pipelines that fork contributors can trigger; re-establish protected refs / approvals (WGL034 / WGL036). |
Because both lexicons organize their security checks around the same core problems, migration maps each problem to its counterpart (pin to pin, least-privilege to least-privilege), preserving the invariant rather than the syntax. Where a problem has a primitive on one side but not the other, that gap is a NeedsReview. Run --validate (above) to also check that the migrated YAML passes the GitLab security checks.
Limitations (v1)
Section titled “Limitations (v1)”- Composite actions (
.github/actions/*/action.yml): not recursively translated. Replace withinclude:orextends:after migration. - Reusable workflows with typed
inputs:/outputs:: partial.include:is emitted but typed inputs require manualspec:inputsrewrite. - Multi-file workflow trees:
chant migrateoperates on one file at a time. Shell-loop for multiple files. - Reverse direction (GitLab to GitHub): not implemented.
- Custom action-mapping plugins: the registry is internal;
--action-mapping <file>is a follow-up.
Follow-up tracking lives on umbrella issue #97.
Why chant migrate over the upstream skill
Section titled “Why chant migrate over the upstream skill”The upstream gitlab-org/ci-cd/github-actions-to-gitlab-ci skill (MIT) is excellent for one-shot translation inside an AI agent. chant migrate applies the same translation rules as a typed compiler. Use chant when you need any of these:
| You need… | The skill | chant migrate |
|---|---|---|
| Reproducible output — same input → same YAML every run | freehand LLM each invocation; output varies | deterministic typed compiler; byte-stable |
| Testable translations — assert against goldens | no test infrastructure | 16 fixture-driven tests + roundtrip harness against real public repos |
| Machine-readable report for CI ingestion | Markdown only | SARIF v2.1.0 + Markdown |
| CI exit-code gating on translation quality | not available | --strict exits non-zero on any needs-review item |
| Maintain the pipeline in chant going forward (typed source you edit) | YAML output only | --emit ts produces typed chant TS that rebuilds via chant build |
| Composite-shape recognition | suggests in prose | --use-composites rewrites IR; emits NodePipeline({...}) instead of raw jobs |
| Programmatic invocation from CI scripts | requires an agent host | plain chant migrate CLI; also an MCP tool (gitlab:migrate) and companion skill |
| Extend the action mapping table | edit the prompt, redeploy | typed ActionMapping[] registry; user-supplied mappings on roadmap (--action-mapping <file>) |
| Lint the generated YAML against chant’s own rules | not applicable | 23 WGL lint rules + 19 post-synth checks run on the output via chant lint |
| Quick translation in an agent context with no install | ✅ ideal fit — paste workflow, get answer | requires npm install --save-dev @intentius/chant first |
The skills are complementary: if you’re inside an AI agent helping a user evaluate GitLab CI/CD from a GitHub Actions background, use the upstream skill; it’s optimised for that conversation. If you’re committing the translation to a repository or running it from CI, use chant migrate.
Coverage vs the upstream skill
Section titled “Coverage vs the upstream skill”chant migrate covers every capability documented in the upstream skill’s SKILL.md plus several net-new ones. The matrix:
| Skill capability | chant equivalent |
|---|---|
| Single-file translation | chant migrate <file> |
Marketplace action mapping (33 actions in upstream references/marketplace-actions.md) | ActionMapping[] registry covers all 33; auto-registered |
| Caching & artifacts | Native cache: and artifacts: keywords via Tier 1 action mappings |
| Matrix builds | strategy.matrix → parallel.matrix with uppercased variable names |
Conditional execution (if: → rules:if:) | translateIfCondition with expression substitution |
| Migration report (4 categories) | Markdown report + SARIF v2.1.0 with same Translated/Approximated/NeedsReview/Lost shape |
| Docker workflows | Tier 1 docker/* mappings emit docker:dind service + inline docker commands |
| Secrets & variables (substitution) | ${{ secrets.X }} / ${{ matrix.X }} / ${{ github.X }} all substituted across script:, if:, image:, and variables: |
| Validator integration (glci/glab) | --validate shells out with the same backend preference (glci → glab fallback) |
| Workflow classification (real-CI vs repo-automation) | Repo-automation detection emits an upfront banner; mixed-trigger workflows are flagged |
| Aggregated manual setup steps | Numbered list at the end of the Markdown report; per-rule actionable instructions |
Suggested GitLab improvements (DAG, rules:changes:, child pipelines, include:, templates, resource groups, protected environments) | Heuristic detection based on workflow shape; surfaced in the Markdown report’s “Suggested GitLab improvements” section |
| Caveats | ”Caveats” section at the end of every report |
| Composite actions | ❌ same limitation in both |
workflow_call typed inputs/outputs | ❌ same limitation in both |
| Repo automation events (issues, labels) | Surfaced as MIG-ON-NON-GIT needs-review with the same gitlab-triage recommendation |
Inspired by github-actions-to-gitlab-ci
Section titled “Inspired by github-actions-to-gitlab-ci”This tool is a typed-compiler port of gitlab-org/ci-cd/github-actions-to-gitlab-ci, an MIT-licensed Agent Skill published by the GitLab CI team. Specifically derived:
- The four-category provenance model (Translated / Approximated / NeedsReview / Lost), used verbatim.
- The marketplace-action mapping table, ported as a typed
ActionMapping[]registry insrc/migrate/from-github/actions/. All 33 entries from the upstreamreferences/marketplace-actions.mdare present. - The syntax-mapping reference: the table from
github.*/runner.*to$CI_*has the same shape. - The validator-as-final-stage pattern (
glci/glab ci lint), kept as the last stage rather than replaced.
What’s different: the upstream skill is a stateless LLM prompt over a curated table; chant migrate is a stateful compiler with those mappings encoded as typed, testable TypeScript modules. The skills are complementary: use the upstream skill for one-shot read-and-respond; use chant migrate when the translation has to be reproducible, testable, and re-runnable.
Full attribution: see lexicons/gitlab/ATTRIBUTIONS.md.
Sample roundtrip harness
Section titled “Sample roundtrip harness”For maintainers: lexicons/gitlab/scripts/migrate-samples.sh clones a handful of public GitHub repos and asserts a 70% pass-rate on real workflows. Out of CI by design (network-bound, slow); run locally:
bash lexicons/gitlab/scripts/migrate-samples.sh --verbose