What this converter does
Paste a GitHub Actions workflow and you get a .gitlab-ci.yml plus a list of everything that did not map cleanly. It is the same idea as the other converters on this site, run the other way: for teams moving from GitHub to GitLab (gitlab.com or self-managed), or keeping both. Conversion runs entirely in your browser; nothing is uploaded.
The output is a starting point, not a finished migration. Marketplace actions become the shell commands or GitLab keywords that replace them; anything without a clean equivalent becomes a TODO step that fails loudly, and the notes list the CI/CD variables you need to create.
How GitHub Actions concepts map to GitLab CI
| GitHub Actions | GitLab CI | Notes |
|---|---|---|
on: push / pull_request / tags | workflow: rules | Branch and tag filters become $CI_COMMIT_BRANCH / $CI_COMMIT_TAG rules; paths becomes changes. Push + pull_request gets the standard duplicate-pipeline guard. |
schedule, workflow_dispatch, release | $CI_PIPELINE_SOURCE rules | Cron schedules are created in the UI; inputs become top-level variables with descriptions and options. |
jobs.<id> + needs | jobs + needs (DAG) | stages are derived from the dependency depth so the pipeline graph reads the same way. |
runs-on: ubuntu-* | default: image | Pick the image in the options. Windows and macOS jobs get the gitlab.com SaaS runner tags; self-hosted labels become tags. |
container, services | image, services | Service env becomes variables; localhost:port in the job is rewritten to the service alias. |
strategy.matrix | parallel: matrix | Keys become upper-case variables; include rows are appended; object-valued dimensions are flattened; runs-on: ${{ matrix.os }} on Linux-only matrices is dropped. |
if: on jobs | rules: if | github.ref, github.event_name, startsWith/contains, inputs, env and secrets comparisons; always() / failure() become when. |
if: on steps | shell if / after_script | always() and failure() steps move to after_script; other conditions become shell tests. |
${{ secrets.X }}, ${{ vars.X }}, ${{ env.X }} | $X | The notes list every CI/CD variable to create (secrets masked). |
${{ github.* }}, $GITHUB_* | $CI_* | 40+ context values mapped: sha, ref, repository, run id, actor, workspace, PR number, head/base refs, commit message… |
$GITHUB_OUTPUT, $GITHUB_ENV, job outputs | build.env + artifacts: reports: dotenv | Later jobs read the variables directly; $GITHUB_PATH becomes export PATH. |
actions/upload-artifact / download-artifact | artifacts: paths / needs | retention-days becomes expire_in; downloads become needs on the producing job. |
actions/cache | cache: key: files | hashFiles() keys become file-based keys; restore-keys become fallback_keys; restore/save become policy. |
actions/setup-node|python|go|java|dotnet, ruby/setup-ruby, Rust, PHP, Bun, Deno, Elixir, Flutter, Terraform… | image: | The job runs in the official image for that version; cache: npm|pip|maven|gradle becomes a cache block. |
docker/login-action, setup-buildx, build-push-action | docker:27 + dind service | ghcr.io/owner/repo becomes $CI_REGISTRY_IMAGE; GHA cache becomes a registry cache. |
aws-actions/configure-aws-credentials, google-github-actions/auth, azure/login | id_tokens + CLI login | OIDC roles become GITLAB_OIDC_TOKEN exchanges; static keys become CI/CD variables. |
softprops/action-gh-release, ncipollo/release-action | release: | With the release-cli image and a tag rule; assets need package-registry links. |
peaceiris/actions-gh-pages, actions/deploy-pages | pages job | The output directory is moved to public/. |
| JUnit reporters, Codecov, Sonar, Slack, SSH deploys, PyPI/npm publish, semantic-release, GoReleaser, pre-commit, golangci-lint… | shell commands / artifacts: reports | About 100 actions have a direct mapping; the rest become TODO steps with the inputs quoted. |
github/codeql-action, Trivy, TruffleHog, dependency review | include: template: | GitLab's SAST, Container Scanning, Secret Detection and Dependency Scanning templates. |
concurrency, timeout-minutes, continue-on-error, environment | interruptible / resource_group, timeout, allow_failure, environment | |
Reusable workflows (jobs.x.uses) | trigger: include: | Child pipeline; convert the called workflow separately. Local composite actions become TODOs. |
actions/stale, labeler, release-drafter, Dependabot helpers | dropped with a note | Repository automation, not CI. |
Things to check by hand
- The image. GitHub's
ubuntu-latesthas hundreds of tools preinstalled;ubuntu:24.04has almost none. Expect to addapt-get installlines, or pick a language image. - Merge request pipelines.
pull_requestbecomesmerge_request_event; the duplicate-pipeline guard is included when the workflow also ran on push. - Outputs between jobs travel as
dotenvartifacts. The producing job must writeNAME=valuelines tobuild.env; the converter does that for$GITHUB_OUTPUTwrites. - Windows and macOS. gitlab.com SaaS runner tags are emitted; self-managed GitLab needs runners with those tags, and Windows jobs run PowerShell.
FAQ
Is my workflow uploaded anywhere?
No. The page is static and the converter runs in your tab. Read actions-gitlab.js on GitHub.
Can I run it on every workflow in a repository?
Yes: npx jenkins-to-actions convert .github/workflows/ci.yml --to gitlab prints the .gitlab-ci.yml; convert each workflow and combine them with include: local:.
Why is the output valid but not identical in behaviour?
The two systems differ in places that no converter can paper over: caches must live inside the project directory, services are reached by alias rather than localhost, and matrix rows cannot be extended. Each of those is called out in the notes with the fix.
Going the other way?
Every other page on this site converts to GitHub Actions: Jenkinsfile, GitLab CI, Azure Pipelines, CircleCI, Bitbucket Pipelines and Travis CI.
Migrating a whole organisation?
The same converters ship as a command-line audit. Point it at a GitHub organisation, a GitLab group, an Azure DevOps organisation, a Bitbucket workspace or a Jenkins server and it inventories every CI configuration it finds, converts what it can, estimates the effort per repository and lists the secrets to create. With --open-prs it opens a pull request in each GitHub repository with the workflow and the notes. Nothing is sent anywhere except the platform you point it at.
GITHUB_TOKEN=… npx jenkins-to-actions audit --github-org acme # report + workflows GITHUB_TOKEN=… npx jenkins-to-actions audit --github-org acme --open-prs # one PR per repository GITLAB_TOKEN=… npx jenkins-to-actions audit --gitlab-group acme/platform AZURE_DEVOPS_TOKEN=… npx jenkins-to-actions audit --azure-org acme npx jenkins-to-actions audit --bitbucket-workspace acme --user me --token APP_PASSWORD npx jenkins-to-actions audit --url https://jenkins.example.com --user me --token APITOKEN
See a real result: this pull request was opened by one command on a repository holding one config per system. The README has the details, and the migration page describes the fixed-fee audit if you would rather have it done for you. Once you are on Actions, the cost calculator and the free CI Cost Report action show what the minutes cost.