Migrate CircleCI to GitHub Actions: Step by Step
Last updated
Migrating from CircleCI to GitHub Actions is mostly a translation job: jobs stay jobs, workflows become needs, orbs become marketplace actions, and save_cache becomes actions/cache. A small pipeline takes an afternoon. The hard parts are secrets, contexts, and anything that relied on CircleCI-specific features such as resource_class or test splitting. This guide covers the mapping, a real translation, the automated Importer, and a cutover plan that avoids downtime.
For the product-level comparison, see CircleCI vs GitHub Actions and CircleCI alternatives.
Concept mapping
| CircleCI | GitHub Actions |
|---|---|
.circleci/config.yml | .github/workflows/*.yml (one file per workflow) |
workflows: with requires | jobs.<id>.needs |
jobs: | jobs: |
docker: / machine: executor | runs-on: plus container: if you want a job image |
Secondary docker: images | services: |
resource_class | Runner label, for example a larger runner or a self-hosted label |
steps: - run | steps: - run |
checkout | actions/checkout |
save_cache / restore_cache | actions/cache, or cache: in setup-* actions |
persist_to_workspace / attach_workspace | actions/upload-artifact / actions/download-artifact |
store_artifacts | actions/upload-artifact |
store_test_results | A test reporter action, or upload-artifact of the JUnit files |
parallelism: N + test splitting | strategy.matrix; see test sharding |
| Orbs | Marketplace actions, composite actions, reusable workflows |
| Contexts | Environments, organization secrets and variables |
| Project environment variables | Repository secrets and variables |
filters: branches | on.push.branches / if: conditions |
Scheduled workflows (triggers: schedule) | on.schedule; see cron schedules |
when: / unless: | if: expressions |
A realistic pipeline, side by side
CircleCI, a Node app with Postgres, build and test, then deploy from main:
# .circleci/config.yml
version: 2.1
orbs:
node: circleci/node@5
jobs:
test:
docker:
- image: cimg/node:22.11
- image: cimg/postgres:16.4
environment:
POSTGRES_PASSWORD: postgres
resource_class: large
parallelism: 4
steps:
- checkout
- restore_cache:
keys:
- npm-v1-{{ checksum "package-lock.json" }}
- npm-v1-
- run: npm ci
- save_cache:
key: npm-v1-{{ checksum "package-lock.json" }}
paths: [~/.npm]
- run:
command: |
TESTS=$(circleci tests glob "test/**/*.test.js" | circleci tests split --split-by=timings)
npm test -- $TESTS
environment:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
- store_test_results:
path: reports
deploy:
docker:
- image: cimg/node:22.11
steps:
- checkout
- run: ./scripts/deploy.sh
workflows:
ci:
jobs:
- test
- deploy:
requires: [test]
context: production
filters:
branches:
only: main
GitHub Actions equivalent:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest # or [self-hosted, Linux, compiler-ci]
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
services:
postgres:
image: postgres:16.4
env:
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-retries 10
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx jest --shard=${{ matrix.shard }}/4 --ci --reporters=default --reporters=jest-junit
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.shard }}
path: reports
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v6
- run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
What changed, and why:
- The primary Docker image becomes the runner itself. Use
setup-nodeinstead ofcimg/node, or keep an image withcontainer:. - Secondary images become
services:. The service is reachable onlocalhostwith the port mapping when the job runs on the runner directly, and by service name when the job itself usescontainer:. Add a health check so tests do not start before Postgres is ready. parallelism: 4pluscircleci tests splitbecomes a matrix. CircleCI splits by recorded timings; Jest's built-in--shardsplits by file order, which is not timing-aware. If timing balance matters, see test sharding.- The context
productionbecomes anenvironment: production, which can also require approvals and scope secrets.
Secrets and environment variables
CircleCI project variables become repository secrets. Contexts shared across projects become organization secrets (restrict by repository) or environment secrets. You cannot read secret values back from CircleCI's UI, so export them from wherever you originally stored them; plan for that early. Non-sensitive values go in repository or organization variables, referenced as ${{ vars.NAME }}.
Prefer short-lived credentials over stored keys for cloud access; see GitHub Actions OIDC with AWS and securing GitHub Actions. Secrets are not passed to workflows triggered from forks by default.
Caching and artifacts
restore_cache with a key list maps directly onto key plus restore-keys. Most setups simplify to cache: on setup-node, setup-python, setup-go and similar. See the per-ecosystem guides: npm, pip, Gradle, Go, and the general caching guide. The repository cache limit is 10 GB; see cache limits.
For persist_to_workspace, upload the files in one job and download them in the next:
- uses: actions/upload-artifact@v4
with: { name: build, path: dist }
# in the next job
- uses: actions/download-artifact@v4
with: { name: build }
Artifacts are stored per run, with a retention period you can set.
Automated conversion: GitHub Actions Importer
GitHub provides an Importer as a gh extension that supports CircleCI, Jenkins, GitLab CI, Azure DevOps, Bamboo, Bitbucket Pipelines and Travis CI (per GitHub's docs, checked October 2026). It needs Docker and the GitHub CLI.
gh extension install github/gh-actions-importer
gh actions-importer configure # sets up tokens for GitHub and CircleCI
gh actions-importer audit circle-ci --output-dir audit
gh actions-importer dry-run circle-ci --output-dir out ...
The documented commands are audit (analyse your pipelines), forecast (estimate Actions usage from past runs), dry-run (write converted workflows locally) and migrate (open a pull request). Run audit first: it tells you which constructs convert and which need manual work, which makes the project plan realistic. Treat output as a first draft. Orbs and custom commands often need hand conversion, and the converted YAML is verbose. Check the current Importer docs for exact flags for your version.
Cutover plan
- Audit with the Importer and list orbs, contexts, and scheduled pipelines.
- Port one workflow (usually the main test job) and run it on a branch.
- Run both in parallel for a week or two. Compare duration and pass rate; fix flaky differences.
- Move secrets and environments, then deployment jobs. Test deploys against a staging environment first.
- Update branch protection to require the GitHub checks instead of the CircleCI ones.
- Disable CircleCI triggers, keep the config in git history, and remove it after a release cycle.
After the move: cost and speed
Default GitHub-hosted runners are 2 vCPU. If your CircleCI jobs used large or xlarge resource classes, expect GitHub's default runners to feel slower. Compare GitHub's larger runners with alternatives in GitHub Actions pricing. compiler.dev runs the same workflow on 8 vCPU / 16 GB machines for $0.012 per minute by changing runs-on to [self-hosted, Linux, compiler-ci], with automatic fallback to your original runners if anything fails; see pricing.
FAQ
How long does a CircleCI to GitHub Actions migration take?
A single pipeline with standard orbs: hours. A dozen pipelines with custom orbs, contexts and deployment logic: weeks, mostly for validation and secrets.
What replaces CircleCI orbs?
Marketplace actions for common tools, composite actions for reusable step sequences, and reusable workflows for whole jobs.
Can I keep CircleCI test splitting by timings?
Not natively. Jest and Playwright shard by file or test count; for timing-based balancing, use a splitting script or a tool built for it.
Do I lose CircleCI's SSH debugging?
There is no built-in equivalent. Community actions open a temporary SSH or tmate session; evaluate their security before using them.
Can I run both for a while?
Yes, and you should. Run both until results match, then switch required checks.
Related
- Migrate GitLab CI to GitHub Actions: Step by StepMigrate GitLab CI to GitHub Actions with a .gitlab-ci.yml mapping table, side-by-side translation, variables, cache, services, Importer and cutover.
- Migrate Jenkins to GitHub Actions: Step by StepMigrate Jenkins to GitHub Actions with a Jenkinsfile mapping table, side-by-side translation, credentials, caching, agents, Importer and a cutover plan.
- CircleCI vs GitHub Actions with compiler.dev runnersCircleCI vs GitHub Actions on compiler.dev runners: credit pricing, config differences, where CircleCI wins, and a migration sketch with real YAML.
- Best CircleCI Alternatives in 2026Best CircleCI alternatives: GitHub Actions, GitLab CI, Buildkite, Semaphore, Jenkins and compiler.dev compared on pricing model, migration effort and fit.