Migrate GitLab CI to GitHub Actions: Step by Step
Last updated
Migrating GitLab CI to GitHub Actions is a fairly direct translation. Stages become needs, rules become on plus if, cache and artifacts map to actions/cache and upload-artifact, and services map to services. The sticking points are include and extends (use reusable workflows and composite actions), CI/CD variables (secrets do not export), and GitLab-only features such as the built-in container registry and Review Apps.
Related pages: GitLab CI vs GitHub Actions and GitLab CI alternatives.
Concept mapping
| GitLab CI | GitHub Actions |
|---|---|
.gitlab-ci.yml | .github/workflows/*.yml |
stages: | Job ordering with needs: |
job: | jobs.<id> |
image: | container: on the job, or use setup-* actions |
services: | services: |
script: | steps: - run: |
before_script: / after_script: | Earlier steps, or steps with if: always() |
variables: | env: |
| Project / group CI/CD variables | Repository, organisation or environment secrets and variables |
rules: / only / except | on: triggers and if: expressions |
needs: (DAG) | needs: |
parallel: matrix | strategy.matrix |
cache: with key:files | actions/cache with hashFiles() |
artifacts: | actions/upload-artifact |
artifacts: reports: junit | Test reporter action or upload |
dependencies: (artifact passing) | actions/download-artifact |
extends: / include: / templates | Composite actions, reusable workflows (workflow_call) |
environment: | environment: |
when: manual | Environment required reviewers, or workflow_dispatch |
schedule pipelines | on.schedule; see cron schedules |
| GitLab Runner (tags) | Runner labels |
| Container registry | GitHub Packages (ghcr.io) or another registry |
$CI_COMMIT_SHA, $CI_COMMIT_BRANCH | ${{ github.sha }}, ${{ github.ref_name }} |
A realistic pipeline, side by side
GitLab:
# .gitlab-ci.yml
stages: [build, test, deploy]
default:
image: python:3.13
cache:
key:
files: [poetry.lock]
paths: [.cache/pip]
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
build:
stage: build
script:
- pip install poetry
- poetry build
artifacts:
paths: [dist/]
expire_in: 1 week
test:
stage: test
parallel:
matrix:
- PYVER: ["3.12", "3.13"]
image: python:$PYVER
services:
- postgres:16
variables:
POSTGRES_PASSWORD: postgres
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
script:
- pip install -r requirements.txt
- pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
deploy:
stage: deploy
script: ./scripts/deploy.sh
environment: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
needs: [test]
GitHub Actions:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest # or [self-hosted, Linux, compiler-ci]
steps:
- uses: actions/checkout@v6
- run: pipx install poetry
- uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: poetry
- run: poetry build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.12", "3.13"]
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-retries 10
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
cache: pip
- run: pip install -r requirements.txt
- run: pytest --junitxml=report.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: junit-${{ matrix.python }}
path: report.xml
deploy:
needs: [build, test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v6
- run: ./scripts/deploy.sh
What to notice:
- GitLab runs each job in
image:. In Actions the job runs on the runner VM, and you install the language withsetup-*. Usecontainer:when you need a specific image. - Services: in GitLab the service host is the image name (
postgres). In Actions with a job on the runner, it islocalhostwith a mapped port; withcontainer:it is the service key. Add a health check. cache: key: files: [poetry.lock]becomescache: poetryinsetup-python, which hashespoetry.lock. See Poetry caching, pip caching and the general caching guide.rules: if: $CI_COMMIT_BRANCH == "main"becomes theontrigger plus anif.artifacts: reports: junithas no first-class equivalent; upload the file or use a JUnit reporter action.
Variables, secrets and environments
GitLab variables marked masked or protected become GitHub secrets. Protected variables, available only on protected branches, map to environment secrets with branch protection rules on the environment. You must copy values manually; there is no export of secret values. File-type variables become secrets you write to a file in a step. Predefined CI_ variables map to GitHub contexts (github., runner.*).
For cloud credentials, OIDC replaces stored keys: GitHub Actions OIDC with AWS. More hardening is in securing GitHub Actions.
Include, extends and templates
GitLab pipelines often share YAML through include: and extends:. Pick the Actions mechanism by size:
- A shared sequence of steps: a composite action in a shared repo.
- A whole shared job or pipeline: a reusable workflow with
on: workflow_callandinputs/secrets. - Templates with many overrides: reusable workflows with inputs, or a generated YAML approach.
Reusable workflows cannot do everything extends can (for example, deep merge of arbitrary keys), so expect to restructure rather than translate.
What GitLab bundles that GitHub does not
- Container registry: use GitHub Packages (ghcr.io) or an external registry.
- Review Apps and environments dashboard: GitHub Environments and deployments cover part of it; preview environments need your own setup.
- Built-in security scanning templates: GitHub has code scanning and Dependabot; see Dependabot setup.
- Merge trains: GitHub's equivalent is the merge queue.
- Child pipelines and dynamic pipelines: use workflow calls and matrix strategies.
Automated conversion: GitHub Actions Importer
GitHub's Importer is a gh extension. Per GitHub's docs (checked October 2026) it supports GitLab (cloud and self-hosted), Jenkins, CircleCI, Azure DevOps, Bamboo, Bitbucket Pipelines and Travis CI, and requires Docker and the GitHub CLI.
gh extension install github/gh-actions-importer
gh actions-importer configure
gh actions-importer audit gitlab --output-dir audit --namespace my-group
The documented commands are audit, forecast, dry-run and migrate. Run audit for a summary of what converts, forecast to estimate Actions minutes from your past GitLab pipeline history, then dry-run to write workflows locally. Check GitHub's Importer docs for the flags your GitLab setup needs. Treat the output as a draft: include/extends structures in particular need review.
Cutover plan
- Audit and forecast with the Importer.
- Port the main test pipeline and run it on a branch.
- Mirror for a while: keep GitLab CI running and compare results. If you keep the code on GitLab during this phase, note that Actions needs the repository on GitHub; plan the repository move as its own step.
- Move secrets, environments and deploy jobs. Test against staging.
- Update branch protection to require GitHub checks.
- Turn off GitLab CI, archive the old config.
Speed after the move
GitLab shared runners and GitHub's default 2 vCPU runners differ in size; compare before you judge. Larger GitHub runners cost more; see GitHub Actions pricing. compiler.dev runs the same workflows on 8 vCPU / 16 GB machines at $0.012 per minute through runs-on: [self-hosted, Linux, compiler-ci], with automatic fallback to your original runners; see pricing and faster runners.
FAQ
Do I have to move my repository from GitLab to GitHub?
GitHub Actions runs on repositories hosted on GitHub, so yes. Plan the code migration alongside the CI migration.
How do I replace GitLab include and extends?
Composite actions for shared steps, reusable workflows for shared jobs.
Do GitLab CI variables migrate automatically?
No. Secret values cannot be exported; re-enter them as GitHub secrets.
What replaces GitLab artifacts passing between stages?
upload-artifact in one job and download-artifact in the next, with needs.
Can I run both CI systems during the move?
Yes, on a mirrored or duplicated repo, and compare results before switching required checks.
Related
- Migrate CircleCI to GitHub Actions: Step by StepMigrate CircleCI to GitHub Actions with a concept mapping table, side-by-side config translation, secrets, caching, the Importer and a cutover plan.
- Migrate Jenkins to GitHub Actions: Step by StepMigrate Jenkins to GitHub Actions with a Jenkinsfile mapping table, side-by-side translation, credentials, caching, agents, Importer and a cutover plan.
- GitLab CI vs GitHub Actions with compiler.dev runnersGitLab CI vs GitHub Actions on compiler.dev runners: per-seat pricing, compute minutes, all-in-one DevOps tradeoffs, and a .gitlab-ci.yml migration.
- Best GitLab CI Alternatives in 2026Best GitLab CI alternatives in 2026: GitHub Actions, CircleCI, Buildkite, Semaphore, Jenkins and compiler.dev, with pricing and migration notes.