How to Migrate from CircleCI to GitHub Actions

Type: Software Reference Confidence: 0.92 Sources: 8 Verified: 2026-02-23 Freshness: monthly

TL;DR

Constraints

Quick Reference

CircleCI GitHub Actions Notes
.circleci/config.yml .github/workflows/*.yml Both YAML; GHA uses one file per workflow [src1]
jobs: jobs: Nearly identical structure [src1]
steps: - run: steps: - run: Same keyword [src1]
executors: runs-on: or container: VM or Docker [src1]
orbs: Marketplace actions (uses:) e.g., circleci/nodeactions/setup-node [src1, src5]
workflows: on: triggers + needs: Trigger-based instead of named workflows [src1]
when: / unless: if: expressions Conditional execution [src1]
persist_to_workspace actions/upload-artifact@v4 Artifact passing [src1]
attach_workspace actions/download-artifact@v4 Restore artifacts [src1]
save_cache / restore_cache actions/cache@v4 Single action handles both [src1]
store_test_results dorny/test-reporter@v1 No built-in test insights [src5]
store_artifacts actions/upload-artifact@v4 Reports, binaries [src2]
parameters: workflow_dispatch: inputs: Manual trigger params [src2]
Contexts Environments + secrets Environment-scoped secrets with approval gates [src2]
resource_class: runs-on: labels xlarge → larger runner label [src1, src6]
Pipeline variables github context << pipeline.git.branch >>${{ github.ref_name }} [src1]
matrix: (via orbs) strategy: matrix: Built-in matrix support [src2]
Service containers services: key in job Explicit services with health checks [src1]

Decision Tree

START
|-- Is code hosted on GitHub?
|   |-- YES -> GitHub Actions is natural fit [src2]
|   +-- NO -> Consider staying on CircleCI or migrating to GitHub first
|-- Want automated migration?
|   |-- YES -> Install gh actions-importer, run audit + migrate [src7]
|   +-- NO -> Manual YAML conversion (more control)
|-- How many orbs are used?
|   |-- 0-3 orbs -> Simple migration (1-2 days) [src1]
|   |-- 4-8 orbs -> Moderate (find marketplace replacements) [src5]
|   +-- 8+ or custom orbs -> Complex (build composite actions) [src1]
|-- Do you use CircleCI contexts?
|   |-- YES -> Map to GitHub environments + secrets [src2]
|   +-- NO -> Use repository-level secrets
|-- Need macOS, Windows, or arm64 runners?
|   |-- YES -> Available on both (pricing restructured Jan 2026) [src8]
|   +-- NO -> Standard Linux runners (cheaper on GitHub for public repos)
|-- Need CircleCI test splitting?
|   |-- YES -> Use matrix strategy + third-party splitting action [src4]
|   +-- NO -> Standard job parallelism via needs: + matrix
+-- DEFAULT -> Start with simplest workflow, migrate incrementally

Step-by-Step Guide

1. Audit existing CircleCI configuration

Assess migration complexity before writing any code. [src7]

# Install GitHub Actions Importer
gh extension install github/gh-actions-importer
gh actions-importer update

# Configure credentials (CircleCI token + GitHub token)
gh actions-importer configure

# Audit all CircleCI projects in your org
gh actions-importer audit circle-ci --output-dir tmp/audit

# Dry-run a single project (preview without creating PR)
gh actions-importer dry-run circle-ci \
  --output-dir tmp/dry-run \
  --circle-ci-project my-project

Verify: Check tmp/audit/ for conversion rate report — successful, partially successful, unsupported, and failed conversions.

2. Map CircleCI config to GitHub Actions YAML

Side-by-side conversion of common patterns. [src1]

# BEFORE: .circleci/config.yml
version: 2.1
orbs:
  node: circleci/node@5
executors:
  default:
    docker:
      - image: cimg/node:20.0
jobs:
  test:
    executor: default
    steps:
      - checkout
      - node/install-packages
      - run: { name: Run tests, command: npm test }
      - store_test_results: { path: test-results }
  build:
    executor: default
    steps:
      - checkout
      - node/install-packages
      - run: npm run build
      - persist_to_workspace: { root: ., paths: [dist] }
  deploy:
    executor: default
    steps:
      - attach_workspace: { at: . }
      - run: ./deploy.sh
workflows:
  main:
    jobs:
      - test
      - build: { requires: [test] }
      - deploy: { requires: [build], filters: { branches: { only: main } } }
# AFTER: .github/workflows/ci.yml
name: CI/CD Pipeline
on:
  push: { branches: [main, develop] }
  pull_request: { branches: [main] }

jobs:
  test:
    runs-on: ubuntu-latest
    container: node:20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm test
      - uses: dorny/test-reporter@v1
        if: always()
        with: { name: Tests, path: 'test-results/*.xml', reporter: jest-junit }

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with: { name: dist, path: dist/ }

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with: { name: dist }
      - run: ./deploy.sh
        env: { DEPLOY_TOKEN: "${{ secrets.DEPLOY_TOKEN }}" }

3. Replace CircleCI orbs with GitHub Actions

Map each orb to its marketplace equivalent. [src1, src5]

# circleci/node@5 -> actions/setup-node@v4
- uses: actions/setup-node@v4
  with: { node-version: '20', cache: 'npm' }

# circleci/docker@2 -> docker/build-push-action@v6
- uses: docker/build-push-action@v6
  with: { push: true, tags: 'myapp:latest' }

# circleci/aws-cli@4 -> aws-actions/configure-aws-credentials@v4
- uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-1

# circleci/slack@4 -> slackapi/slack-github-action@v2
- uses: slackapi/slack-github-action@v2
  with: { channel-id: '#deploys', slack-message: 'Deployed!' }

# circleci/python@2 -> actions/setup-python@v5
- uses: actions/setup-python@v5
  with: { python-version: '3.12', cache: 'pip' }

# circleci/terraform@3 -> hashicorp/setup-terraform@v3
- uses: hashicorp/setup-terraform@v3
  with: { terraform_version: '1.7' }

4. Migrate caching

CircleCI uses separate save/restore; GitHub Actions combines them. [src1]

# CircleCI:
- save_cache:
    key: node-deps-{{ checksum "package-lock.json" }}
    paths: [node_modules]
- restore_cache:
    keys:
      - node-deps-{{ checksum "package-lock.json" }}
      - node-deps-

# GitHub Actions -- single action handles both:
- uses: actions/cache@v4
  with:
    path: node_modules
    key: node-deps-${{ hashFiles('package-lock.json') }}
    restore-keys: node-deps-

5. Migrate contexts to GitHub environments

Map CircleCI contexts to GitHub environments with optional approval gates. [src2]

# Create environment in GitHub repo Settings > Environments
gh secret set DEPLOY_TOKEN --env production --body "prod-token"
gh secret set DEPLOY_TOKEN --env staging --body "staging-token"
# Use in workflow with optional approval gate:
deploy:
  runs-on: ubuntu-latest
  environment: production  # Uses production secrets + approval rules
  steps:
    - run: ./deploy.sh
      env:
        TOKEN: ${{ secrets.DEPLOY_TOKEN }}

6. Migrate service containers (databases, Redis, etc.)

CircleCI uses additional Docker images alongside the primary; GitHub Actions uses the services: key. [src1]

# CircleCI:
jobs:
  test:
    docker:
      - image: cimg/node:20.0    # primary
      - image: cimg/postgres:15  # service

# GitHub Actions:
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env: { POSTGRES_PASSWORD: test }
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm test
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/postgres

Anti-Patterns

Wrong: Duplicating orb logic inline

# BAD -- reimplementing what a marketplace action already does
steps:
  - run: |
      curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
      sudo apt-get install -y nodejs
      node --version

Correct: Use marketplace actions

# GOOD -- official action, maintained, cached [src5]
steps:
  - uses: actions/setup-node@v4
    with: { node-version: '20' }

Wrong: No caching

# BAD -- installs dependencies from scratch every time (slow)
steps:
  - run: npm ci  # 2-3 minutes every run

Correct: Cache dependencies

# GOOD -- cache restores in seconds [src1]
steps:
  - uses: actions/setup-node@v4
    with: { node-version: '20', cache: 'npm' }  # Built-in caching
  - run: npm ci

Wrong: Using CircleCI pre-built images in GitHub Actions

# BAD -- cimg/* images set USER=circleci, causing permission errors [src1]
jobs:
  test:
    runs-on: ubuntu-latest
    container: cimg/node:20.0
    steps:
      - uses: actions/checkout@v4  # Permission denied errors

Correct: Use official images or setup actions

# GOOD -- use official Docker images or setup actions
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm test

Wrong: Hardcoding secrets in workflow files

# BAD -- secrets visible in logs and repo history
steps:
  - run: curl -H "Authorization: Bearer ghp_abc123" https://api.example.com

Correct: Use GitHub secrets and environments

# GOOD -- secrets are masked in logs, scoped to environments [src2]
steps:
  - run: curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" https://api.example.com

Common Pitfalls

Diagnostic Commands

# Analyze CircleCI config complexity
grep -c "jobs\|orbs\|executors\|workflows" .circleci/config.yml

# List all orbs used
grep -oP "circleci/[a-z-]+@\d+" .circleci/config.yml

# Audit migration feasibility with GitHub Actions Importer
gh actions-importer audit circle-ci --output-dir tmp/audit

# Preview converted workflow without creating PR
gh actions-importer dry-run circle-ci \
  --output-dir tmp/dry-run \
  --circle-ci-project my-project

# Validate GitHub Actions workflow syntax
actionlint .github/workflows/ci.yml

# Check workflow runs after migration
gh run list --limit 10
gh run view <run-id> --log

# Compare CI run times before/after migration
gh run list --workflow=ci.yml --json databaseId,conclusion,createdAt,updatedAt \
  --jq '.[] | "\(.createdAt) -> \(.updatedAt) [\(.conclusion)]"'

Version History & Compatibility

Feature CircleCI GitHub Actions
Config format YAML (config.yml v2.0/2.1) YAML (workflow files)
Extensibility Orbs (registry.circleci.com) Marketplace actions (github.com/marketplace)
Runners Cloud or self-hosted GitHub-hosted or self-hosted
Free tier (Linux) 6,000 min/mo 2,000 min/mo (private) / unlimited (public)
Docker layer caching Built-in External action required
Test insights Built-in dashboard Third-party actions (dorny/test-reporter)
Reusable config Orbs + commands Reusable workflows + composite actions
Matrix builds Via orbs Built-in strategy: matrix:
Manual approvals N/A (use contexts) Environment protection rules
Pricing (Jan 2026) Per-credit model Per-minute; hosted runners reduced up to 39%

When to Use / When Not to Use

Migrate When Don't Migrate When Use Instead
Code is hosted on GitHub Heavily invested in custom CircleCI orbs Stay on CircleCI; refactor orbs incrementally
Want unified GitHub workflow (code + CI in one UI) Need CircleCI's built-in test insights dashboard CircleCI + GitHub integration
Simplifying DevOps toolchain Complex resource class requirements (8+ CPU) CircleCI or self-hosted runners
Public repos (unlimited free CI) Team relies on CircleCI SSH debugging daily Stay on CircleCI
Need GitHub environment protection rules CI budget is tight (CircleCI 3x free minutes) CircleCI free tier
Actions Importer can convert >80% of config Migration would take >2 weeks of engineering time Gradual migration or stay

Important Caveats

Related Units