Migrate CircleCI to GitHub Actions: orbs, executors, contexts and test splitting
To migrate CircleCI to GitHub Actions, split each entry under workflows: into its own workflow file, turn requires: into needs:, replace orbs with actions, executors and resource_class with a runs-on label, contexts with environment or organization secrets, and persist_to_workspace with the artifact actions. The two things with no direct equivalent are timing-based test splitting and Docker Layer Caching, so plan those before the cutover.
CircleCI and GitHub Actions both keep YAML in the repository with jobs made of steps, so the job bodies translate quickly. The structure around them does not: one config.yml holds every workflow, orbs package jobs and commands you never see, and parallelism with circleci tests run gives you timing-balanced shards from one line of config.
This page maps each construct, converts a config that uses an orb, an executor, a context and four-way test splitting, and gives a resource_class to runner size table so the converted jobs keep the CPU and memory they had.

The concept map
GitHub's guide covers jobs, caching, workspaces and services. The orbs, contexts, resource classes and test splitting rows are where most of the migration time goes.
| CircleCI | GitHub Actions | What changes |
|---|---|---|
workflows: (several in one file) | One file per workflow in .github/workflows/ | Triggers move into each file's on: |
requires: | needs: | None |
filters: branches: | on: push: branches: or if: | None |
orbs: | Actions from the Marketplace | An orb job or command has to be found or rewritten |
commands: | Composite action | Lives in its own action.yml |
executors: with docker: | runs-on:, optionally container: | CircleCI images set USER circleci, which conflicts with Actions file permissions |
Second docker: image | services: | Named service with its own env: |
resource_class: | A runs-on: label | The label is the size |
context: | Environment secrets or organization secrets | Access rules move from security groups to environments and repository access |
parallelism: with circleci tests run | strategy: matrix: with your own split | No built-in timing-based splitting |
restore_cache / save_cache | actions/cache with key and restore-keys | Immutable keys in both; {{ checksum }} becomes hashFiles() |
persist_to_workspace / attach_workspace | upload-artifact / download-artifact | Named artifacts instead of one workspace |
store_test_results | No built-in equivalent | Upload JUnit XML as an artifact or use an action |
| Docker Layer Caching | No equivalent | Pick a build cache separately |
type: approval | A job on an environment with required reviewers | Approval is a property of the environment |
Before: a .circleci/config.yml
One workflow, three jobs. The node orb installs packages, a reusable executor sets the image and size, test splits files across four containers by historical timing, and deploy reads a context and only runs on main.
version: 2.1
orbs:
node: circleci/node@7.2.1
executors:
app:
docker:
- image: cimg/node:22.11
resource_class: large
jobs:
build:
executor: app
steps:
- checkout
- node/install-packages
- run: npm run build
- persist_to_workspace:
root: .
paths: [dist]
test:
executor: app
parallelism: 4
environment:
JEST_JUNIT_OUTPUT_DIR: reports
steps:
- checkout
- node/install-packages
- run: |
npx jest --listTests | circleci tests run \
--command "xargs npx jest --ci --reporters=default --reporters=jest-junit" \
--split-by=timings
- store_test_results:
path: reports
deploy:
executor: app
steps:
- checkout
- attach_workspace:
at: .
- run: ./scripts/deploy.sh
workflows:
build-test-deploy:
jobs:
- build
- test:
requires: [build]
- deploy:
requires: [test]
context: production-deploy
filters:
branches:
only: mainAfter: the converted workflow
The orb's install-packages command became setup-node with its npm cache plus npm ci. GitHub recommends moving off CircleCI's prebuilt images, so the jobs run on the runner directly instead of in cimg/node. The context became the production environment, whose secrets only the deploy job can read.
The four test containers became a four-cell matrix using Jest's own --shard flag. That splits by test file, not by recorded timing, so shards will be less even than CircleCI's. To get timing balance back, upload each shard's JUnit XML as an artifact and feed past durations into a splitter, as sharding tests across GitHub Actions runners describes.
name: build-test-deploy
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest # was: resource_class: large
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist
test:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4] # was: parallelism: 4
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
- run: npm ci
- run: npx jest --ci --shard=${{ matrix.shard }}/4
deploy:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # was: context: production-deploy
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist
- run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Resource classes and runner sizes
A resource_class is a size, and on Actions the size is whatever the runs-on label resolves to. Match on vCPU first, then check memory: CircleCI's Docker classes give 2 GB per vCPU, so a job that fit there fits in anything with more. Moving down a size to save money is a separate decision from the migration; make it after the timings are known.
If a job never set resource_class, CircleCI's reference says it ran on a default that can change, so check which class its recent runs actually used before you pick a label.
| CircleCI Docker class | vCPU and RAM | Latchkey label at the same vCPU |
|---|---|---|
medium | 2 vCPU, 4 GB | latchkey-small: 2 vCPU, 8 GB |
large | 4 vCPU, 8 GB | latchkey-medium: 4 vCPU, 16 GB |
xlarge | 8 vCPU, 16 GB | latchkey-large: 8 vCPU, 32 GB |
2xlarge | 16 vCPU, 32 GB | latchkey-xlarge: 16 vCPU, 64 GB |
Orbs, test timing and layer caching: the three to plan
Orbs. An orb call is one line in your config and dozens in the orb, so you cannot replace it until you know what it ran. The CircleCI CLI prints the YAML source of any orb version. Read the commands you call, note every step that installs a tool or writes a cache, and find the action or the two lines of shell that do the same. For circleci/node, install-packages is a lockfile install with a cache around it, which setup-node with cache: npm already covers. Orbs that wrap a vendor CLI usually have an official action from the same vendor; check the Marketplace before rewriting one.
Test timing. circleci tests run --split-by=timings balances shards from the JUnit results CircleCI stored for earlier runs. Actions stores nothing between runs unless you do, so the converted workflow starts with file-based shards. Keep the JUnit reporter, and when shard times drift apart, add the timing loop described in parallelizing tests by timing.
Layer caching. CircleCI's Docker Layer Caching was a checkbox on the job. GitHub states there is no equivalent, so a Docker build that relied on it will rebuild every layer on the first runs. Docker layer caching in GitHub Actions compares the BuildKit cache backends you can use instead.
circleci orb source circleci/node@7.2.1 > node-orb.yml
grep -n -A12 'install-packages:' node-orb.ymlThe migration checklist
Work through it per workflow in config.yml, plus every orb it imports.
- Runner labels. Every executor and
resource_classbecomes aruns-onlabel from the table above. Self-hosted CircleCI runner classes become self-hosted runners; the Importer marks those executors unsupported. - Secrets. Project environment variables and contexts are both manual tasks in GitHub Actions Importer. Recreate context values as environment secrets where approval mattered, organization secrets where it did not.
- Cache keys.
{{ checksum "package-lock.json" }}becomeshashFiles('package-lock.json'). A list of fallback keys becomeskeyplusrestore-keys. Both systems refuse to overwrite an existing key, so the versioning habit (v1-) carries over. - Matrix.
parallelismbecomes a matrix over a shard index; CircleCImatrixparameters becomestrategy.matrix. - Artifacts.
persist_to_workspaceandstore_artifactsboth becomeupload-artifact; give each a name, because there is no single workspace. - Concurrency. A
serial-groupbecomes aconcurrency:group. Addqueue: maxso every waiting deploy keeps its place; without it, GitHub keeps one pending run and cancels the one already waiting when a newer one arrives.
Key takeaways
- One
config.ymlbecomes one workflow file per entry underworkflows:. - Orbs and contexts are manual work in GitHub Actions Importer; budget time for them.
- Timing-based test splitting is not built into Actions: a matrix with
--shardsplits by file, not by duration. - Map
resource_classto a runner label by vCPU, then confirm memory.
Frequently asked questions
What is the GitHub Actions equivalent of CircleCI orbs?
How do I split tests across parallel jobs in GitHub Actions?
--shard=1/4 or with a script. GitHub states that CircleCI-style timing-based splitting is not built in, so balancing by duration means storing past timings yourself.What replaces CircleCI contexts in GitHub Actions?
Can I keep using the cimg Docker images on GitHub Actions?
container:, but GitHub advises against it: the images set USER circleci, which conflicts with the runner's file permissions on the workspace. GitHub's own converted example needs a chmod step to work around it. Use the runner image and setup actions instead.Related guides
References
- GitHub Docs: Migrating from CircleCI to GitHub Actions (read 2026-11-12)
- GitHub Docs: Migrating from CircleCI with GitHub Actions Importer, manual tasks and supported syntax
- CircleCI Docs: Configuration reference, resource classes and executors
- CircleCI Docs: Test splitting and parallelism
- Jest CLI options: --shard
- GitHub Actions documentation