Understanding the Migration
Migrating from GitHub Actions to GitLab CI means moving your automated workflows—tests, builds, deployments, and releases—from GitHub’s YAML-based CI/CD platform to GitLab’s built-in pipeline system. Both platforms let you define jobs as code in a repository, but they differ significantly in syntax, concepts, and available features.
Teams typically consider this migration when consolidating tooling around a single GitLab instance (source control + CI/CD), seeking more control over self-hosted runners, leveraging GitLab’s integrated container registry and Kubernetes integration, or reducing costs by moving away from GitHub’s paid action minutes. It also makes sense when an organization adopts GitLab as its primary DevOps platform and wants to standardize pipelines.
Key Differences at a Glance
Before diving into the migration, it’s helpful to understand the fundamental differences that will shape your conversion:
- Configuration file: GitHub Actions uses
.github/workflows/*.ymlfiles (multiple workflows allowed). GitLab CI uses a single.gitlab-ci.ymlat the repository root, which can define multiple pipelines via conditions. - Triggers: GitHub uses the
on:key with events likepush,pull_request,schedule,workflow_dispatch. GitLab CI usesonly/except(legacy) or the more powerfulrules:andworkflow:rules. - Runners: GitHub has hosted runners (ubuntu-latest, etc.) and self-hosted runners identified by labels. GitLab uses “tags” to select runners, and offers shared runners on GitLab.com or self-managed runners.
- Actions vs Steps: GitHub Actions heavily relies on marketplace “actions” (reusable units). GitLab CI steps are typically shell scripts, but you can reuse logic via
include:(templates) or custom Docker images. - Artifacts and Cache: Both platforms support passing data between jobs and caching dependencies, but the syntax and behavior differ.
- Environments & Approvals: GitLab has first-class environment support with manual approval gates, which replaces GitHub’s environment protection rules and workflow_dispatch manual triggers.
Preparing for the Migration
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →A successful migration starts with a thorough inventory of your existing GitHub Actions workflows. Don’t try to translate everything in one go—plan to migrate incrementally and validate each pipeline.
Audit Your GitHub Actions Workflows
List every workflow file under .github/workflows/. For each, record:
- Triggers: push, pull_request, tags, schedules, manual dispatch.
- All jobs and their
runs-onenvironment. - Secrets and environment variables used.
- External actions (e.g.,
actions/checkout,actions/setup-node). - Steps that upload/download artifacts or use cache.
- Any service containers (PostgreSQL, Redis, etc.).
This audit gives you a clear mapping of what needs to be recreated in GitLab CI. Keep the list handy—it becomes your migration checklist.
Map Concepts Between Platforms
Use this conceptual mapping to guide your translation:
- Workflow file → .gitlab-ci.yml pipeline definition
- Job → Job (defined under top-level keys, each with a stage)
- on: push → workflow:rules or only: - push
- on: pull_request → only: - merge_requests or rules with
$CI_PIPELINE_SOURCE == 'merge_request_event' - runs-on: ubuntu-latest → tags: [docker, linux] (or use a Docker image directly)
- actions/checkout → GitLab automatically clones the repo; no step needed
- actions/setup-node → Use a Docker image with Node.js pre-installed (e.g.,
image: node:18) - Secrets → GitLab CI Variables (protected, masked, or CI/CD settings)
- Artifacts →
artifacts:with paths, anddependenciesto control flow - Cache →
cache:with key and paths - Services →
services:blocks with Docker images
Step-by-Step Migration Guide
Let’s walk through converting a typical GitHub Actions workflow into a GitLab CI pipeline. We’ll use a common Node.js application pipeline as our example, then expand to more complex patterns.
1. Create a GitLab CI Configuration File
Start by creating a file named .gitlab-ci.yml in the root of your repository. GitLab will detect it automatically on the next push. Unlike GitHub, you have one main configuration file, but you can split logic using include: to reference external YAML files if needed.
# .gitlab-ci.yml - starting point
stages:
- test
- build
- deploy
Define stages that match your workflow’s logical phases. Every job must belong to a stage.
2. Translate Triggers and Events
Suppose your GitHub workflow triggers on push to main and pull requests:
# GitHub Actions trigger
on:
push:
branches: [main]
pull_request:
branches: [main]
In GitLab CI, use the modern rules: syntax for precise control:
# GitLab CI equivalent using rules
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
For scheduled runs (cron), map on: schedule to:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
Manual dispatch (workflow_dispatch) becomes a manual job with when: manual or a rule that checks a variable, often combined with GitLab’s “Run pipeline” web button.
3. Convert Jobs and Runners
A GitHub job like this:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
becomes a GitLab CI job that uses a Docker image containing Node.js, and parallel matrix jobs:
# GitLab CI equivalent with parallel matrix
test:
stage: test
image: node:${NODE_VERSION}
parallel:
matrix:
- NODE_VERSION: ['16', '18']
script:
- npm ci
- npm test
tags: [docker] # only needed if you use specific runners
Notice that GitLab automatically checks out the repository, so no explicit checkout step is required. The parallel:matrix syntax creates one job per variable value, similar to GitHub’s strategy matrix.
4. Adapt Environment Variables and Secrets
GitHub secrets like ${{ secrets.NPM_TOKEN }} become GitLab CI Variables. You can set them in the project’s CI/CD settings (Settings → CI/CD → Variables) or at group/instance level. In the YAML, reference them as $NPM_TOKEN (or $env:NPM_TOKEN for PowerShell).
# GitHub step using secret
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
GitLab CI equivalent:
# In GitLab CI
deploy_package:
stage: deploy
image: node:18
script:
- npm publish
variables:
NODE_AUTH_TOKEN: $NPM_TOKEN # Variable defined in project settings
only:
- tags
Protect variables by marking them as “protected” (only for protected branches/tags) or “masked” (hidden in logs). You can also use file-type variables for certificates.
5. Translate Steps and Actions
GitHub Actions marketplace provides ready-made actions for setup, deployment, etc. In GitLab CI, you replace them with:
- Using the appropriate Docker image (e.g.,
node:18instead ofsetup-node). - Running explicit shell commands (
apt-get install,curl, etc.). - Using GitLab’s
include:to import pre-defined CI templates (e.g.,Node.gitlab-ci.yml). - Leveraging custom Docker images that bundle dependencies.
For example, a GitHub workflow deploying to AWS with aws-actions/configure-aws-credentials can be replaced with a job that uses an AWS CLI Docker image and script steps that export variables from GitLab CI Variables.
# GitHub Actions deploy job
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- run: aws s3 sync ./build s3://my-bucket
GitLab CI translation:
deploy_s3:
stage: deploy
image: amazon/aws-cli:latest
script:
- aws s3 sync ./build s3://my-bucket --region us-east-1
variables:
AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION: us-east-1
rules:
- if: $CI_COMMIT_BRANCH == "main"
6. Handle Artifacts and Caching
GitHub’s actions/upload-artifact and download-artifact become GitLab’s artifacts: declaration. Artifacts are automatically passed between jobs in the same pipeline if the jobs are in later stages, but you can control it with dependencies.
# GitHub: build job uploads artifact
- uses: actions/upload-artifact@v3
with:
name: build-output
path: dist/
In GitLab CI:
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
For caching, GitHub’s actions/cache maps to GitLab’s cache: block, using a cache key to share dependencies across pipelines:
# GitHub cache step
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
GitLab CI equivalent:
cache:
key: ${CI_COMMIT_REF_SLUG}-node-${CI_JOB_NAME}
paths:
- node_modules/
Note that GitLab’s cache is shared between pipelines on the same branch by default, but you can customize the key strategy.
7. Services and Containers
If your GitHub workflow uses a service container like PostgreSQL:
services:
postgres:
image: postgres:14
env:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
ports: [5432]
In GitLab CI, you define services at the job level:
test:
stage: test
image: node:18
services:
- postgres:14
variables:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
# GitLab automatically sets PGHOST, PGPORT etc.
script:
- npm test
GitLab automatically connects the service to the job using hostname aliases based on the image name (e.g., postgres). Docker-in-Docker setups for building images are also supported via the docker service and DOCKER_HOST variable.
8. Manual Approvals and Environments
GitHub’s workflow_dispatch and environment approval checks translate to GitLab’s manual actions and environment deployment approvals. For a manual deployment to production:
# GitHub manual trigger
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
In GitLab CI, you can use a job with when: manual and environment settings:
deploy_production:
stage: deploy
when: manual
environment:
name: production
url: https://myapp.example.com
script:
- deploy-script.sh
only:
- main
You can also protect environments so that only certain users can approve the manual job, mimicking GitHub’s environment protection rules.
Best Practices for GitLab CI after Migration
- Use stages wisely: Group jobs into logical stages (test, build, security, deploy) to create a clear pipeline flow.
- Leverage
rules:instead ofonly/except: Rules give fine-grained control and are more readable. Useworkflow:rulesto control entire pipeline creation. - Pin Docker image versions: Avoid floating tags like
node:latest. Use specific versions (e.g.,node:18.17.1) for reproducibility. - Centralize variables: Define CI/CD variables in GitLab’s project/group settings rather than hardcoding them in YAML. Use protected and masked flags for secrets.
- Cache intelligently: Use a cache key that includes
CI_COMMIT_REF_SLUGand the job name to avoid collisions. Fall back to a global cache for common dependencies. - Include templates: For repetitive tasks (e.g., building a Docker image), use
include:to reference community or custom templates, reducing duplication. - Monitor pipeline efficiency: Use GitLab’s pipeline analytics and job trace to identify slow stages. Consider using
needs:to run independent jobs in parallel without stage constraints. - Test your pipelines: Use GitLab’s CI lint tool (in the repository’s CI/CD settings or via the API) to validate YAML before pushing.
Testing and Validation
After writing your .gitlab-ci.yml, push it to a feature branch (not main) to trigger a pipeline without affecting production. Observe the pipeline in GitLab’s CI/CD → Pipelines interface. Use the job log to debug failures. GitLab offers a built-in lint tool: navigate to CI/CD → Pipeline editor in your project to validate the YAML interactively.
Iterate on the conversion one workflow at a time. Start with your simplest workflow (e.g., a linter or unit test) to build confidence. Once that pipeline succeeds, gradually add more jobs. Keep both the GitHub Actions workflow and GitLab CI pipeline running in parallel until you’re ready to switch completely. This dual-running approach reduces risk and allows you to compare results.
Conclusion
Migrating from GitHub Actions to GitLab CI is a structured process that rewards careful planning. By auditing your existing workflows, mapping concepts, and methodically translating triggers, jobs, actions, artifacts, and secrets, you can build robust GitLab pipelines that match or exceed your previous automation. The key is to embrace GitLab’s strengths—integrated container registry, powerful rules syntax, manual approvals, and a single configuration file—while following best practices for maintainability. With a gradual, validated approach, your team will soon enjoy a unified DevOps experience directly within GitLab, simplifying your toolchain and accelerating delivery.