How to Migrate from CircleCI to GitHub Actions
How do I migrate from CircleCI to GitHub Actions?
TL;DR
- Bottom line: CircleCI and GitHub Actions share similar YAML config. Map CircleCI
jobs→ GitHubjobs,orbs→ marketplace actions,executors→runs-on:/container:. - Key tool/command: Use
gh actions-importer migrate circle-cifor automated conversion, or manually move.circleci/config.ymllogic into.github/workflows/ci.yml. - Watch out for: CircleCI orbs bundle complex functionality (commands + executors + jobs) — each needs individual replacement with marketplace actions.
- Works with: Any GitHub repo; GitHub-hosted (Linux, macOS, Windows, arm64) or self-hosted runners. Actions Importer supports CircleCI config v2.0 and v2.1.
Constraints
- Do not use CircleCI's pre-built
cimg/*Docker images in GitHub Actions — they setUSERtocircleciwhich causes permission errors on GitHub-hosted runners. Use official images or setup actions. [src1] - GitHub-hosted standard runners (
ubuntu-latest) provide only 2 vCPU / 7 GB RAM — do not assume CircleCIresource_class: xlarge(8 CPU / 16 GB) performance without requesting larger runners. [src4, src6] - GitHub Actions Importer cannot automatically migrate CircleCI contexts, project-level environment variables, or custom orbs — these require manual conversion. [src7]
- Each GitHub Actions workflow has a 6-hour job timeout (configurable) and 35-day workflow run limit.
CircleCI allows configurable
no_output_timeoutper step. [src2] - GitHub Actions free tier for private repos is 2,000 min/mo vs CircleCI's 6,000 min/mo (Linux). As of Jan 2026, hosted-runner prices reduced up to 39% but $0.002/min platform charge introduced. [src8]
- Artifact size limits: GitHub Actions allows up to 10 GB per artifact (500 MB free tier). Always verify artifact sizes before migration. [src2]
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/node → actions/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
Decision Logic
Structured if/then rules an agent can apply once it knows the project's CI shape. Each rule maps a condition to a concrete recommendation.
If the repo is already on GitHub and the CircleCI config uses 0-3 standard orbs
→ Run gh actions-importer dry-run circle-ci then manually finish the workflow; expect a
1-2 day migration with >80% auto-conversion. [src1,
src7]
If the config relies on custom orbs, CircleCI contexts, or project-level env vars
→ Do not trust the importer for these — migrate contexts to GitHub environments +
secrets and rebuild custom orbs as composite actions by hand; the importer flags them as unsupported. [src7]
If a job declares resource_class: large/xlarge (8+ CPU)
→ Do not map to ubuntu-latest (2 vCPU / 7 GB); request a GitHub larger runner label or
self-hosted runner, and model cost using the Jan 2026 per-minute rates (hosted reduced up to 39%,
$0.002/min platform charge). [src4, src6, src8]
If the pipeline uses cimg/* Docker images as the execution environment
→ Replace them with official base images plus actions/setup-* actions —
cimg/* images set USER=circleci and cause permission errors on GitHub-hosted
runners. [src1]
If the project depends on CircleCI automatic test splitting across parallel containers
→ Plan extra effort: GitHub Actions has no built-in equivalent — use strategy: matrix
plus a third-party splitting action (e.g. chaosaffe/split-tests) keyed on timing data. [src4]
If the repo is public and CI cost is the main driver
→ Migrate: GitHub-hosted standard runners are free on public repos (vs CircleCI's metered credits), so the move is almost always net-positive. [src2, src8]
If the team relies daily on CircleCI SSH debugging or the built-in test-insights dashboard
→ Weigh staying on CircleCI; GitHub Actions only approximates these via
mxschmitt/action-tmate and dorny/test-reporter, neither of which is a 1:1
replacement. [src4]
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
- Orbs are not 1:1 with actions: A single CircleCI orb may bundle multiple commands, executors, jobs. Each piece may need a separate GitHub Action or composite action. [src1]
- Workspace vs. artifacts: CircleCI's
persist_to_workspacepasses files within one workflow. GitHub usesupload/download-artifactwith different size limits (500 MB free, 10 GB max) and 90-day default retention. [src1, src2] - Cache key syntax differs: CircleCI uses
{{ checksum "file" }}; GitHub uses${{ hashFiles('file') }}. Forgetting to convert silently breaks cache restoration. [src1] - Pipelines vs. workflows: CircleCI can have multiple named workflows in one config.yml.
In GitHub Actions, each
.ymlfile IS a workflow with its own triggers. [src1] - Resource classes: CircleCI's
resource_class: xlarge(8 CPU, 16 GB) → GitHub'subuntu-latestis only 2 CPU / 7 GB. Use larger runner labels or self-hosted. Prices reduced up to 39% as of Jan 2026. [src4, src6, src8] - Docker layer caching: CircleCI has built-in DLC. GitHub needs explicit
docker/build-push-actionwithcache-from/cache-tousing GitHub cache or registry backend. [src4] - Test splitting: CircleCI has automatic test splitting across parallel containers with
timing-based distribution. GitHub Actions requires
matrixstrategy or third-party actions likechaosaffe/split-tests. [src4] - SSH debugging: CircleCI's
circleci sshhas no native GitHub Actions equivalent. Usemxschmitt/action-tmatefor interactive debugging. [src4]
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
- CircleCI has better built-in test splitting across parallel containers with automatic timing-based distribution; GitHub Actions requires manual splitting with third-party actions.
- CircleCI's SSH debugging has no native GitHub Actions equivalent — use
mxschmitt/action-tmatefor similar functionality, but it requires a public SSH key. - GitHub free tier (2,000 min/mo private) may be less than CircleCI (6,000 min/mo). As of Jan 2026, hosted runners reduced up to 39% but $0.002/min platform charge introduced (self-hosted charge postponed). [src8]
- CircleCI's dynamic config requires
workflow_call+ matrix strategies ordorny/paths-filterto replicate in GitHub Actions. - GitHub Actions Importer audit reports may overstate conversion success — always manually review partially-successful conversions for correctness. [src7]
- As of 2025, GitHub Actions defaults to more secure
pull_request_targetbehavior, anchoring execution to trusted default-branch workflow definitions. Review security settings after migration.