← Back to DevBytes

Migrating from GitHub Actions to GitLab CI: Step-by-Step Guide

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:

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:

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:

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:

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles