GitLab CI to GitHub Actions Migration: The Enterprise Guide

Blog post

Enterprise scale migrations are tough - they don't need to be

CodeCargo Team
September 25, 2026

What is a GitLab CI to GitHub Actions migration?

A GitLab CI to GitHub Actions migration is the process of moving pipeline definitions out of .gitlab-ci.yml files and into GitHub Actions workflows under .github/workflows/, so that building, testing and deploying happens on GitHub alongside the code. In an enterprise the migration is rarely one file. It is hundreds of pipelines, a runner fleet, a set of CI/CD variables that nobody has audited in three years, and a delivery schedule that cannot stop while the work happens.
The good news is that GitLab CI and GitHub Actions are conceptually close. Both are YAML, both run jobs on runners, both build a dependency graph between jobs, both support service containers and matrices. Most of the translation is mechanical. The work that is not mechanical is the part worth planning for, and it is mostly about runners, secrets and the handful of GitLab features that have no one-to-one equivalent.

When should you move from GitLab CI to GitHub Actions?

The strongest reason is consolidation. If your code already lives on GitHub, or is moving there, running CI somewhere else means two permission models, two audit trails, two sets of runners to patch, and a pull request that cannot tell you whether it is safe to merge without leaving the page. Teams also move to reach the GitHub Actions ecosystem, to use GitHub's own security and compliance surface around the same repositories, and to stop paying to operate a second platform.
The weakest reason is that GitLab CI is bad, because it is not. It is a capable system with a genuinely elegant single-file model. Migrations that are sold internally as an escape from a broken tool tend to stall when engineers discover the old tool worked fine. Migrations sold as consolidation onto one platform survive contact with reality better.

GitLab CI vs GitHub Actions: what actually changes?

The mapping below covers the keywords that appear in almost every real pipeline and will help you develop a mental map of how a migration might work.
GitLab CI
GitHub Actions
What to watch
script:
run:
Direct swap. GitLab runs scripts through a shell per job; Actions runs each run: step in its own shell invocation
stages: plus stage:
needs:
Actions has no global stage list. Ordering is expressed per job as an explicit dependency graph, which is more verbose and more precise
image:
container:
Per-job container. Many pipelines do not need it at all once they use runs-on plus a setup action
services:
services:
Same concept and nearly the same shape
variables:
env:
Scope carefully: workflow, job and step level all exist
rules:, only:, except:
on: plus if:
Trigger conditions move up to on:, per-job conditions become if: expressions. This is the row that takes the most thought
artifacts:
actions/upload-artifact
Artifacts become an explicit upload step, and a matching download step in the consuming job
cache:
actions/cache
Explicit key and path, rather than a cache block on the job
parallel: matrix:
strategy: matrix:
Close equivalent, including include and exclude style refinements
needs:
needs:
Same name, same idea
allow_failure: true
continue-on-error: true
Same behaviour
timeout:
timeout-minutes:
Job level in both
when: manual
workflow_dispatch, or an environment with required reviewers
A manual gate becomes either a separately triggered workflow or a deployment approval
environment:
environment:
Both support named environments; GitHub attaches protection rules and secrets to them
include:, extends:
Reusable workflows (workflow_call) and composite actions
The most important row for a large estate, see below
Runner tags
runs-on: labels
Your self-hosted fleet is re-registered against GitHub and addressed by label
CI_COMMIT_SHA
github.sha
Every predefined variable has an equivalent in the github context, but the names all change
Two rows deserve expanding.
include: and extends: are where the real estate savings are. A large GitLab estate usually has a shared templates repository that every .gitlab-ci.yml includes. The equivalent on GitHub is a reusable workflow called with workflow_call, or a composite action for a reusable block of steps. Porting the shared templates first, then pointing converted pipelines at them, is what keeps a migration from producing hundreds of copies of the same YAML. Converting pipeline by pipeline without doing this first is the single most common way a migration creates more maintenance than it removes.
rules: is the row that hides the work. A mature GitLab pipeline often has rules that combine branch, file path, pipeline source and variable state. On GitHub, part of that expression belongs in on: (which events start the workflow, and with which branch or path filters) and part belongs in if: on the job. Splitting one rule set across two places is easy to get subtly wrong, and the failure mode is a job that quietly never runs.

A worked example

A small but realistic GitLab pipeline:
stages: [build, test]
variables:  NODE_ENV: test
build:  stage: build  image: node:20  script:    - npm ci    - npm run build  artifacts:    paths: [dist/]
test:  stage: test  image: node:20  needs: [build]  script:    - npm test  rules:    - if: $CI_PIPELINE_SOURCE == "merge_request_event"Copy
The same thing as a GitHub Actions workflow:
name: build-and-test
on:  pull_request:  push:    branches: [main]
env:  NODE_ENV: test
jobs:  build:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0        with:          node-version: '20'      - run: npm ci      - run: npm run build      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1        with:          name: dist          path: dist/
  test:    runs-on: ubuntu-latest    needs: [build]    if: github.event_name == 'pull_request'    steps:      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0        with:          node-version: '20'      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1        with:          name: dist          path: dist/      - run: npm testCopy
Note what changed beyond the keywords. The implicit checkout became an explicit step. The implicit artifact hand-off between stages became an upload and a download. The image: became a runner plus a setup action, which is usually faster. And the single rules: entry became a trigger in on: and a condition in if:.

What has no clean equivalent?

  • Predefined variables all change name. There is an equivalent for nearly everything in the github context, but CI_COMMIT_SHA, CI_COMMIT_REF_NAME and friends will not resolve. Grep for CI_ across the estate before you start; the count is usually higher than anyone expects.
  • CI_JOB_TOKEN is not GITHUB_TOKEN. Both are short-lived job credentials, but their permissions models differ, and anything that used the job token to reach another project needs deliberate redesign rather than a rename.
  • Auto DevOps has no counterpart. If any pipeline depends on it, that pipeline is a rewrite, not a conversion.
  • Runner scale and tags. Moving the fleet is usually the longest pole, not the YAML. Plan capacity before cutover, not after the first slow week.

How do you migrate hundreds of pipelines without stopping delivery?

Pipeline-by-pipeline hand conversion does not finish. The pattern that does:
  1. Inventory first. Which pipelines have run successfully in the last 90 days, which have not run at all, and which have no named owner. A meaningful share of any large estate is dead, and converting dead pipelines is pure cost.
  2. Port the shared templates before the pipelines. Reusable workflows and composite actions first, so converted pipelines call them instead of duplicating them.
  3. Convert in batches, by similarity. Pipelines cluster into a handful of shapes. Convert one of each shape carefully, then apply it across the cluster.
  4. Run both for one cycle. The old pipeline and the new workflow on the same commits, comparing results, before anything is switched off.
  5. Make the new workflows compliant by construction. If the converted workflows land without pinned actions, least-privilege permissions or egress control, you have moved the problem rather than fixed it.

Where does CodeCargo fit?

CodeCargo is the control plane for GitHub, and migration is one of its core motions. Its Pipeline Migration Assistant imports from Jenkins, Azure DevOps and GitLab, including both GitLab cloud and self-managed instances, and produces CodeCargo workspaces ready for AI-assisted conversion into GitHub Actions. Converted jobs can be worked individually in an editor, or at scale with migration batches and the migration agent, which opens pull requests across many repositories for review rather than committing changes unattended.
The part that matters after the migration is what the workflows land into. CodeCargo scores every workflow against policy on each edit, surfaces violations inline on the pull request, detects drift, and opens auto-remediation pull requests. CargoWall applies a kernel-level egress firewall to the runners so a converted pipeline cannot quietly reach destinations it should not. A migration that ends with thousands of ungoverned workflows has traded one problem for a larger one.

Key takeaways

  • The translation is mostly mechanical. script to run, image to container, rules to on plus if, artifacts to explicit upload and download steps.
  • Stages become an explicit graph. GitHub Actions has no global stage list; ordering is expressed per job with needs.
  • Port shared templates first. include: and extends: become reusable workflows and composite actions, and doing this first is what stops the estate duplicating itself.
  • rules: splits in two. Part becomes a trigger in on:, part becomes an if: on the job, and getting it wrong produces jobs that silently never run.
  • Every CI_ variable changes name. Grep the estate before you begin.
  • The runner fleet is usually the long pole, not the YAML.

Key terms glossary

  • .gitlab-ci.yml The single file defining a GitLab CI pipeline for a project.
  • Stage A named ordering group in GitLab CI. Jobs in one stage run in parallel, and the next stage waits.
  • Reusable workflow A GitHub Actions workflow another workflow can call with workflow_call, the closest equivalent to a GitLab include:.
  • Composite action A packaged sequence of steps reusable inside a job, the closest equivalent to a shared extends: block of steps.
  • Service container A supporting container, such as a database, available to a job. Both platforms call it services.
  • Matrix Running one job many times across a set of parameter combinations.
  • Pipeline Migration Assistant CodeCargo's importer, which brings Jenkins, Azure DevOps and GitLab pipelines into workspaces for AI-assisted conversion to GitHub Actions.

FAQ

How long does a GitLab CI to GitHub Actions migration take? For a single straightforward pipeline, under a day. For an enterprise estate, the timeline is driven by pipeline count, how much shared template logic exists, and how quickly the runner fleet can be stood up on GitHub, not by the YAML translation itself. The decisive variables are the number of distinct pipeline shapes and the number of owners you need agreement from, which is why an inventory pass comes first.
Can GitLab CI and GitHub Actions run side by side during a migration? Yes, and for anything larger than a handful of pipelines you should plan on it. Running the converted workflow next to the existing pipeline on the same commits for one delivery cycle is the cheapest way to find behavioural differences before you depend on the new one.
What is the GitHub Actions equivalent of GitLab stages? There is no global stage list. Each job declares the jobs it depends on with needs:, which builds an explicit dependency graph. It is more verbose than stages and more precise, because two jobs that never truly depended on each other no longer wait for each other.
How do GitLab CI/CD variables map to GitHub? Non-sensitive values become env: at workflow, job or step level. Masked and protected variables become GitHub secrets, and secrets scoped to a deployment target belong on a GitHub environment so they inherit its protection rules. The predefined CI_ variables do not carry over by name and must be replaced with their github context equivalents.
Does GitHub Actions support manual approval like when: manual? The equivalent depends on intent. A job someone triggers on demand becomes a workflow with a workflow_dispatch trigger. A gate before a deployment becomes a GitHub environment with required reviewers, which is the stronger control because the approval is recorded against the deployment.
What happens to our GitLab runners? Self-hosted runners are re-registered with GitHub and addressed by label in runs-on: instead of by GitLab tag. Capacity planning is the part teams underestimate: a migration that lands hundreds of workflows on an under-provisioned fleet looks like a performance regression in the new platform when it is really a queueing problem.
Can the conversion be automated? Substantially, yes. CodeCargo's Pipeline Migration Assistant imports GitLab pipelines, from cloud or self-managed instances, and produces workspaces for AI-assisted conversion, with migration batches opening pull requests across many repositories at once. Human review still matters, because the parts that need judgement, such as splitting rules: across triggers and conditions, are exactly the parts a converter cannot decide for you.
Should we convert every pipeline? No. Most large estates contain pipelines with no successful run in months, pipelines that never succeeded, and pipelines with no owner. Excluding those from scope is the cheapest decision available in the whole project.
C

CodeCargo Team

The CodeCargo team writes about GitHub workflow automation, developer productivity, and DevOps best practices.