We’ve migrated a dozen projects from CircleCI to GitHub Actions over the past two years. The reasons vary—cost optimization, simplification, GitHub integration—but the process has consistent patterns and predictable friction points.
This isn’t a philosophical comparison of which platform is better. It’s a practical guide for teams who’ve decided to migrate and need to know what’s actually involved.
Why Teams Migrate
Before diving into how, it’s worth understanding why this migration happens. The most common reasons we see:
Cost consolidation: If you’re already paying for GitHub Enterprise, Actions is included. Why maintain a separate CircleCI subscription?
Simpler workflow management: Keeping CI configuration in the same place as code means one fewer system to manage, one fewer permission model to maintain.
Better GitHub integration: Actions sees pull requests, issues, and releases natively. CircleCI requires webhooks and API calls for the same visibility.
Self-hosted runner requirements: GitHub Actions makes self-hosted runners straightforward. CircleCI can do this, but the experience is rougher. If you’re evaluating runner options, our comparison of self-hosted vs cloud CI/CD runners covers the tradeoffs.
These aren’t universal truths—plenty of teams stay on CircleCI and are happy. But if you’re reading this, you’ve probably already made the decision.
The Core Mapping
CircleCI and GitHub Actions share the same fundamental concept: run jobs in containers or VMs based on repository events. The syntax differs, but the models align.
CircleCI .circleci/config.yml:
version: 2.1
workflows:
build-and-test:
jobs:
- build
- test:
requires:
- build
jobs:
build:
docker:
- image: node:18
steps:
- checkout
- run: npm ci
- run: npm run build
GitHub Actions .github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: npm run build
The translation is straightforward for simple pipelines. Complexity emerges with orbs, contexts, and workflow orchestration.
Step 1: Inventory Your CircleCI Configuration
Start by cataloging what your CircleCI setup actually does. Don’t just read the config file—understand the runtime behavior.
Key questions to answer:
- Which orbs are you using? CircleCI orbs are reusable configuration packages. GitHub Actions uses marketplace actions instead.
- What contexts provide secrets? CircleCI contexts are named secret collections. GitHub uses repository secrets, environment secrets, and organization secrets.
- Do you use dynamic configuration? CircleCI’s setup workflows generate config at runtime. Actions handles this differently.
- Are there matrix builds? Both platforms support matrices, but the syntax differs.
- What triggers are configured? Branch filters, tag filters, scheduled runs—map these to GitHub event filters.
- Self-hosted runners? If you’re using CircleCI’s resource classes for self-hosted executors, plan the GitHub runner migration.
For each workflow, document:
- Trigger conditions
- Environment variables and secrets used
- Docker images or machine executors
- Job dependencies and parallelism
- Artifacts and caching patterns
This inventory prevents surprises mid-migration.
Step 2: Map Orbs to Actions
CircleCI orbs bundle reusable steps. GitHub Actions Marketplace provides similar functionality but with different packages.
Common CircleCI orbs and their GitHub equivalents:
| CircleCI Orb | GitHub Action |
|---|---|
circleci/node@5.0 | actions/setup-node@v4 |
circleci/python@2.0 | actions/setup-python@v5 |
circleci/aws-cli@4.0 | aws-actions/configure-aws-credentials@v4 |
circleci/docker@2.0 | docker/setup-buildx-action@v3 |
codecov/codecov@3.0 | codecov/codecov-action@v4 |
For custom orbs or less common ones, you may need to rewrite the logic directly in workflow steps. This is tedious but not complicated—most orb logic is just wrapped shell commands.
Example: Slack notifications
CircleCI’s Slack orb:
- slack/notify:
event: fail
mentions: '@devops-team'
template: basic_fail_1
GitHub Actions equivalent:
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Build failed on ${{ github.repository }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
The functionality is the same. The syntax is different. Budget time for these translations.
Step 3: Migrate Secrets and Contexts
CircleCI contexts group secrets for reuse across projects. GitHub Actions uses repository secrets (scoped to one repo), environment secrets (scoped to deployment environments), and organization secrets (scoped to all repos).
Migration approach:
- List all contexts in CircleCI and note which workflows use which contexts.
- Decide on GitHub secret scope: If secrets are project-specific, use repository secrets. If shared across repos, use organization secrets. For detailed best practices, see our GitHub Actions secrets management guide.
- Create corresponding secrets in GitHub: Settings → Secrets and variables → Actions.
- Update workflows to reference them:
${{ secrets.SECRET_NAME }}in GitHub Actions.
Environment-based secrets:
CircleCI restricts contexts to specific jobs. GitHub Actions uses environments for similar control.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }}
The production environment can require manual approval and has its own secrets separate from staging.
Step 4: Handle Caching and Artifacts
Both platforms cache dependencies and store build artifacts. The mechanisms are similar but not identical.
CircleCI caching:
- restore_cache:
keys:
- v1-deps-{{ checksum "package-lock.json" }}
- save_cache:
key: v1-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
GitHub Actions caching:
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
The logic is equivalent. Cache keys in Actions use hashFiles() instead of CircleCI’s template syntax.
Artifacts:
CircleCI’s store_artifacts becomes GitHub’s actions/upload-artifact:
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
Artifacts are available in the Actions UI and downloadable via API.
Step 5: Convert Workflow Logic
CircleCI workflows define job dependencies and parallelism. GitHub Actions handles this through needs and matrix strategies.
Job dependencies:
CircleCI:
workflows:
version: 2
build-test-deploy:
jobs:
- build
- test:
requires:
- build
- deploy:
requires:
- test
GitHub Actions:
jobs:
build:
runs-on: ubuntu-latest
steps: [...]
test:
runs-on: ubuntu-latest
needs: build
steps: [...]
deploy:
runs-on: ubuntu-latest
needs: test
steps: [...]
Matrix builds:
CircleCI matrices:
jobs:
test:
docker:
- image: node:18
parallelism: 3
parameters:
version:
type: enum
enum: ["14", "16", "18"]
GitHub Actions matrices:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16, 18]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
Both run tests across multiple versions in parallel. The GitHub syntax is arguably clearer.
Step 6: Handle Edge Cases
Dynamic configuration:
CircleCI supports setup workflows that generate config YAML dynamically. GitHub Actions doesn’t have direct equivalents. Workarounds:
- Use workflow conditionals and matrix strategies to cover variation
- Generate configuration outside Actions and commit it (less dynamic, more predictable)
- Use workflow templates and repository dispatch events for complex scenarios
SSH debugging:
CircleCI’s rerun with SSH lets you SSH into failed jobs. GitHub Actions doesn’t offer this natively. Alternatives:
- Use
actions/tmate@v3to start an SSH session in the workflow - Add debugging steps with verbose output
- Use self-hosted runners where you have direct access
Scheduled workflows:
CircleCI schedules are defined in the workflow. GitHub Actions uses on.schedule with cron syntax:
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC
Both support standard cron expressions.
Step 7: Test in Parallel
Don’t cut over all at once. Run both CircleCI and GitHub Actions simultaneously for a few weeks.
- Add
.github/workflows/files to your repository - Keep
.circleci/config.ymlactive - Compare results between platforms
- Fix discrepancies in the Actions workflows
- Once confident, disable CircleCI workflows
This de-risks the migration. If Actions has issues, CircleCI is still running.
What Usually Goes Wrong
Differences in environment variables:
CircleCI automatically provides CIRCLE_BRANCH, CIRCLE_SHA1, etc. GitHub Actions provides GITHUB_REF, GITHUB_SHA. Scripts relying on CircleCI-specific variables break. Grep your codebase for CIRCLE_ and replace accordingly.
Docker layer caching:
CircleCI’s Docker layer caching is a paid feature that speeds up image builds. GitHub Actions doesn’t have direct equivalents. Use Docker’s buildx with cache backends:
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
This caches layers in GitHub’s cache storage. For more optimization strategies, see our guide on Docker build caching.
Permissions and token scopes:
GitHub Actions runs with a GITHUB_TOKEN that has limited permissions by default. If workflows push commits, create releases, or modify issues, you may need to expand token permissions:
permissions:
contents: write
pull-requests: write
Or use a personal access token with broader scopes.
Cost Considerations
GitHub Actions is free for public repositories and includes 2,000 minutes/month for private repos on the free plan. Paid plans increase this significantly.
CircleCI has different pricing tiers with credits. For some workloads, CircleCI is cheaper. For others, Actions is cheaper. Run the numbers based on your actual usage.
Self-hosted runners change the equation entirely—you pay for compute infrastructure but not per-minute charges.
When Not to Migrate
Migration makes sense if GitHub Actions genuinely simplifies your setup. It doesn’t always.
Reasons to stay on CircleCI:
- Your CircleCI config is stable and complex. Migration is high-effort, low-value.
- You rely heavily on CircleCI-specific features (certain orbs, execution environments).
- Cost analysis shows CircleCI is cheaper for your workload.
- Your team is deeply familiar with CircleCI and not with Actions.
Don’t migrate for the sake of migrating. Migrate when there’s clear benefit.
The Bottom Line
Migrating from CircleCI to GitHub Actions is straightforward for simple pipelines and tedious for complex ones. The platforms are similar enough that the concepts translate, but different enough that you can’t automate the conversion.
Budget time for:
- Mapping orbs to actions (1-2 hours per orb)
- Translating workflow logic (2-4 hours per complex workflow)
- Migrating and testing secrets (1-2 hours)
- Parallel testing period (1-2 weeks)
Most teams complete migration in days to weeks, not months. The result is tighter GitHub integration and one fewer external service to manage.
If you’re mid-migration and stuck, the friction is almost always in orb equivalents or environment-specific behavior. Test in isolation, compare outputs, and adjust. The platforms are close enough that persistence beats cleverness.
Need help with your CI/CD migration? We’ve guided teams through dozens of CircleCI to GitHub Actions migrations—from simple pipelines to complex multi-repo setups with custom orbs and deployment orchestration. See how we helped one team achieve 80% faster deployments and 95% fewer failures with a modern CI/CD pipeline, or learn more about our cloud platform engineering services.