type: runbook
status: active
timestamp: 2026-06-21
tags: [runbook, ci, migration, gitlab-ci, circleci, plan-b]

Migrate CI/CD from GitHub Actions to GitLab CI or CircleCI

Plan-B runbook when GitHub Actions unusable; translates CI to GitLab CI + CircleCI

Migrate CI/CD from GitHub Actions to GitLab CI or CircleCI

When to run

GitHub Actions is the family’s primary CI/CD (already free + unlimited public-repo minutes on Linux). Trigger this runbook when one of:

If only the FREE TIER changes (e.g. capped), the migration may be partial — heavy jobs move; cheap ones stay.

Pre-requisites

Translation table: GitHub Actions → GitLab CI → CircleCI

ConceptGitHub ActionsGitLab CICircleCI
Config file.github/workflows/<name>.yml.gitlab-ci.yml.circleci/config.yml
Linux runnerruns-on: ubuntu-latestimage: node:22 + default runnerexecutor: docker:cimg/node:22.0
Trigger on pushon: push: branches: [main]workflow: rules: - if: $CI_COMMIT_BRANCH == "main"workflows: build: jobs: - check: filters: branches: only: main
Trigger on tagon: push: tags: ['v*']rules: - if: $CI_COMMIT_TAGfilters: tags: only: /^v.*/
Trigger on PRon: pull_requestrules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"filters: branches: ignore: main (approximation)
Cron scheduleon: schedule: - cron: '...'GitLab Pipelines → Schedules UItriggers: schedule: cron: '...'
Secret${{ secrets.NPM_TOKEN }}$NPM_TOKEN (set in Settings → CI/CD → Variables)$NPM_TOKEN (set in Project Settings → Environment Variables)
OIDC for npm provenancepermissions: id-token: writeGitLab ID Tokens via id_tokens: fieldOIDC supported via cloud-config-only flow
Artifact uploadactions/upload-artifact@v4artifacts: paths: [...]store_artifacts: path: ...
Cache depsactions/setup-node@v4 with: cache: pnpmcache: paths: [.pnpm-store/]restore_cache: keys: [...] + save_cache:
Re-usable workflowuses: <org>/<repo>/.github/workflows/X.yml@maininclude: project: <path> file: <yml>orbs: <orb>@<ver>
Matrixstrategy: matrix:parallel: matrix: -parallelism: N + matrix: (limited)
Marketplace actionuses: <org>/<action>@v1run binary directly (no marketplace)orb or run binary

Standard family CI workflow translated

Source GH Actions (.github/workflows/ci.yml):

name: CI
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 10 }
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile=false
      - run: pnpm typecheck
      - run: pnpm test

Equivalent GitLab CI (.gitlab-ci.yml):

default:
  image: node:22
  cache:
    paths:
      - .pnpm-store/

check:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  before_script:
    - corepack enable
    - corepack prepare pnpm@10 --activate
    - pnpm config set store-dir .pnpm-store
  script:
    - pnpm install --frozen-lockfile=false
    - pnpm typecheck
    - pnpm test

Equivalent CircleCI (.circleci/config.yml):

version: 2.1
jobs:
  check:
    docker:
      - image: cimg/node:22.0
    steps:
      - checkout
      - restore_cache:
          keys:
            - pnpm-{{ checksum "pnpm-lock.yaml" }}
            - pnpm-
      - run: corepack enable && corepack prepare pnpm@10 --activate
      - run: pnpm config set store-dir ~/.pnpm-store
      - run: pnpm install --frozen-lockfile=false
      - save_cache:
          paths:
            - ~/.pnpm-store
          key: pnpm-{{ checksum "pnpm-lock.yaml" }}
      - run: pnpm typecheck
      - run: pnpm test

workflows:
  build:
    jobs:
      - check

Standard release workflow translated

Source GH Actions (.github/workflows/release.yml):

name: Release
on:
  push: { tags: ['v*.*.*'] }
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, registry-url: 'https://registry.npmjs.org' }
      - run: pnpm install --frozen-lockfile=false
      - run: npm publish --access public --provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

GitLab CI equivalent:

publish:
  image: node:22
  rules:
    - if: $CI_COMMIT_TAG =~ /^v.*/
  before_script:
    - corepack enable
    - corepack prepare pnpm@10 --activate
    - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
  script:
    - pnpm install --frozen-lockfile=false
    - npm publish --access public
  # NOTE: --provenance only works with GitHub Actions OIDC right now.
  # GitLab OIDC support for npm provenance is on npm's roadmap but not GA.
  # Until then, releases from GitLab CI are unsigned but functionally identical.

CircleCI equivalent: similar cimg/node image + npm publish step gated by filters: tags: only: /^v.*/.

Step-by-step emergency migration

  1. Verify mirror is current — check master cron’s last successful run. If it ran <7 days ago, the GitLab/Codeberg/Bitbucket/GitFlic mirror is fresh.

  2. Pick primary fallback — GitLab.com by default (broadest ecosystem). Fall back to CircleCI if GitLab is also affected.

  3. For each repo that needs CI today:

    • Visit GitLab.com mirror URL (e.g. gitlab.com/chirag127/<repo>)
    • Create .gitlab-ci.yml mirroring the local .github/workflows/ci.yml
    • Settings → CI/CD → Variables → add NPM_TOKEN, CODECOV_TOKEN, etc. (mirror from GH org secrets)
    • Push → pipeline runs
  4. For deployment workflows (CF Pages, npm publish, etc.):

    • CF Pages: deploy from GitLab via cloudflare/wrangler-action@v3 doesn’t exist on GitLab — use npx wrangler pages deploy dist directly with CF_API_TOKEN env var
    • npm publish: same npm publish --access public works — bump NPM_TOKEN
  5. Update master umbrella — write a .gitlab-ci.yml for the master itself that runs the matrix deploy + mirror cron (translated from GH Actions).

  6. Communicate:

    • Telegram channel: “GH Actions unusable, primary CI moved to GitLab.com”
    • Update README badges from github.com/.../actions/.../badge.svg to GitLab equivalents

Things that DON’T translate cleanly

GH Actions featureIssue elsewhere
--provenance for npmOnly works with GH OIDC; GitLab/CircleCI in flight
GitHub Marketplace actionsNo equivalent registry on GitLab/CircleCI — run binaries directly
Free macOS / Windows runnersWe don’t use them per [[linux-ci-only]] — no impact
${{ github.event.pull_request.head.sha }} etc.Different env var names ($CI_MERGE_REQUEST_SOURCE_BRANCH_SHA on GitLab)
repository_dispatchGitLab has trigger_pipeline API; CircleCI has API trigger
Reusable workflowsGitLab uses include:; CircleCI uses orbs:
GitHub App-installed bots (CodeRabbit, Mergify, etc.)All GitHub-only. No replacement on GitLab. Accept loss + use built-in MR features

Long-term plan

If GitHub Actions becomes unusable for >2 weeks, make GitLab CI the primary and run a reverse mirror (GitLab → GitHub) instead. Update [[decisions/ops/mirror-to-4-git-hosts]] accordingly.

Cross-refs


Edit on GitHub · Back to index