jobs → GitHub jobs, orbs → marketplace actions,
executors → runs-on: / container:.gh actions-importer migrate circle-ci for automated
conversion, or manually move .circleci/config.yml logic into
.github/workflows/ci.yml.cimg/* Docker images in GitHub Actions — they set
USER to circleci which causes permission errors on GitHub-hosted runners.
Use official images or setup actions. [src1]ubuntu-latest) provide only 2 vCPU / 7 GB RAM — do
not assume CircleCI resource_class: xlarge (8 CPU / 16 GB) performance without requesting
larger runners. [src4, src6]no_output_timeout per step. [src2]| 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] |
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
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.
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 }}" }
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' }
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-
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 }}
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
# 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
# GOOD -- official action, maintained, cached [src5]
steps:
- uses: actions/setup-node@v4
with: { node-version: '20' }
# BAD -- installs dependencies from scratch every time (slow)
steps:
- run: npm ci # 2-3 minutes every run
# GOOD -- cache restores in seconds [src1]
steps:
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' } # Built-in caching
- run: npm ci
# 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
# 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
# BAD -- secrets visible in logs and repo history
steps:
- run: curl -H "Authorization: Bearer ghp_abc123" https://api.example.com
# GOOD -- secrets are masked in logs, scoped to environments [src2]
steps:
- run: curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" https://api.example.com
persist_to_workspace passes files
within one workflow. GitHub uses upload/download-artifact with different size limits (500 MB
free, 10 GB max) and 90-day default retention. [src1,
src2]{{ checksum "file" }}; GitHub uses
${{ hashFiles('file') }}. Forgetting to convert silently breaks cache restoration. [src1]
.yml file IS a workflow with its own triggers. [src1]
resource_class: xlarge (8 CPU, 16 GB)
→ GitHub's ubuntu-latest is 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/build-push-action with cache-from/cache-to using GitHub
cache or registry backend. [src4]matrix strategy or third-party actions
like chaosaffe/split-tests. [src4]circleci ssh has no native GitHub Actions
equivalent. Use mxschmitt/action-tmate for interactive debugging. [src4]# 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)]"'
| 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% |
| 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 |
mxschmitt/action-tmate for similar functionality, but it requires a public SSH key.workflow_call + matrix strategies or
dorny/paths-filter to replicate in GitHub Actions.pull_request_target behavior, anchoring
execution to trusted default-branch workflow definitions. Review security settings after migration.