Cirrus CI shutting down: moving the macOS jobs first
With Cirrus CI shutting down on 1 June 2026 and Cirrus Runners closed to new customers, every task in your .cirrus.yml needs a new home, and this is not the one-line label swap that a runner migration usually is. Cirrus is a platform rather than a runner, so the Linux half is a rewrite into a workflow file and the macOS half is a rewrite plus a bill you should price before you commit to it.

Two products are ending on two timetables, and mixing them up is the first mistake. Cirrus Labs announced that "Cirrus CI will shut down effective Monday, June 1, 2026", which is the hosted platform that reads your .cirrus.yml. Cirrus Runners, the separate product that attaches macOS and Linux runners to GitHub Actions, is a slower wind-down: the same announcement says the company is "no longer accepting new customers" and will "continue supporting the service for existing customers through their existing contract periods".
The documentation has gone ahead of the service. Checked on 2026-09-20, cirrus-ci.org no longer resolves and cirrus-ci.com does not answer, so the only authoritative description of what your pipeline does is the .cirrus.yml in your own repository. Export anything you need from the Cirrus dashboard now rather than later.
One piece of good news: Tart, Vetu and Orchard are being relicensed "under a more permissive license" with the licensing fees dropped, so the macOS virtualization stack underneath Cirrus survives even though the hosted service does not.
The pre-flight checklist
A Cirrus migration is a translation, not a relabel, so the inventory matters more than usual. Work through all eight before you write a workflow file.
- Instance types. Every
macos_instance,container,windows_containerandpersistent_workerin the file is a different destination. The macOS ones are the expensive ones and should be listed first. - Runner sizes. Cirrus tasks declare CPU and memory on the instance. Write those numbers down: the GitHub-hosted default is 2 vCPU with 8 GB on private repositories, and a task that asked for more will be slower without saying so.
- Secrets. Cirrus encrypted variables and
CIRRUS_*environment values have no equivalent. Every one needs a repository or environment secret on the GitHub side, created before the first run rather than after the first failure. - Cache keys. A
*_cacheblock carriesfolder,fingerprint_scriptandpopulate_script. The folder becomespath, the fingerprint becomes akeybuilt withhashFiles, and the populate script becomes an ordinary step guarded bycache-hit. - Matrix. Cirrus
matrixexpands inside a task. GitHubstrategy.matrixexpands into jobs, so the fan-out is the same but the concurrency ceiling is not: GitHub allows 20 to 500 concurrent jobs by plan and only 5 concurrent macOS jobs on Free, Pro and Team. - Artifacts. An
artifactsinstruction becomesactions/upload-artifact, and anything that was reading artifacts from the Cirrus API needs a new source. - Concurrency. Cirrus
auto_cancellationbecomes aconcurrency:block withcancel-in-progress: true. - Anything that assumed a persistent worker. A
persistent_workertask ran on a machine you controlled, with state between runs. On ephemeral runners that state is gone every time.
Before and after: a macOS task
The shape below is the one most repositories used: a macos_instance with a Tart image, then named script instructions. Translating it is mechanical once the secrets and the cache are in place.
# before: .cirrus.yml
macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sonoma
test_task:
deps_cache:
folder: ~/.cache/pods
fingerprint_script: cat Podfile.lock
populate_script: pod install
test_script: ./scripts/test.sh
# after: .github/workflows/ci.yml
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: actions/cache@v6
id: pods
with:
path: ~/.cache/pods
key: pods-${{ hashFiles('Podfile.lock') }}
- if: steps.pods.outputs.cache-hit != 'true'
run: pod install
- run: ./scripts/test.shWhat macOS costs after the move
This is the number that decides the migration, because GitHub bills macOS at ten times its Linux rate and rounds every job up to the whole minute. Price it before you move, not after the first invoice.
| Runner | Shape | Per minute |
|---|---|---|
| Namespace, prepaid | 4 vCPU, 7 GB | $0.04 |
| Namespace, prepaid | 6 vCPU, 14 GB | $0.06 |
| GitHub-hosted | 3-core or 4-core, M1 or Intel | $0.062 |
| Blacksmith | macOS M4, 6 vCPU | $0.08 |
| WarpBuild | M4 Pro, 6 vCPU, 14 GB | $0.08 |
| Depot | depot-macos-latest, 8 CPU, 24 GB | $0.08 |
| Namespace, overage | 6 vCPU, 14 GB | $0.09 |
| Namespace, prepaid | 12 vCPU, 28 GB | $0.12 |
| Blacksmith | macOS M4, 12 vCPU | $0.16 |
| Latchkey | None | No macOS runner at any price |
What changes in cache behavior
A Cirrus cache was declarative: you gave it a folder and a fingerprint script, and it decided when to repopulate. actions/cache is two things instead of one, a restore and a save, and the repopulate step is yours to guard with cache-hit. Forget the guard and the populate step runs on every job, which is the most common way a migrated pipeline gets slower while the cache reports a hit.
The limits are different too. GitHub allows 10 GB per repository and removes entries that have not been accessed for 7 days, and an entry is restorable only from the branch that wrote it or from the default branch. A Cirrus pipeline that shared one cache across every branch will see a cold restore on every new branch after the move.
What to test first
Move one macOS task before anything else. It is the expensive one, the slow one, and the one most likely to depend on something the Tart image provided that macos-latest does not.
- Translate a single task, push to a branch, and compare the wall clock against the last Cirrus run of the same task.
- Check the Xcode version. The Cirrus image carried the last three Xcode versions; a GitHub-hosted macOS image carries its own set, so pin the one you need with
xcode-selectrather than assuming. - Confirm the cache step logs a restore and that your populate step is skipped on the second run of the same commit.
- Only then translate the Linux tasks, which are cheaper to get wrong, and delete
.cirrus.ymllast.
If you are moving to Latchkey
The concession comes first, because it is decisive here: Latchkey has no macOS runner, so the macOS half of a Cirrus migration cannot come to Latchkey at all. Those jobs go to GitHub-hosted, Namespace, WarpBuild or Blacksmith, priced in the table above and in GitHub Actions runner alternatives.
For the Linux half, Latchkey runners are $0.0025 a minute for 2 vCPU with 8 GB, with a dependency cache that is a one-line swap for actions/cache and no published size. The label resolves to a 2 vCPU Ubuntu 24.04 runner with 7,734 MB of memory, 43 GB free and Docker 29.7.2, measured on 2026-09-20 in job cli-02917f33-a216-4255-9f21-7043ddccad19; the full output is quoted in migrate from self-hosted runners. Size it against your container: tasks before you translate them, because a Cirrus container that asked for 8 GB will not fit. The containerized tasks are also the ones that fail on registry problems such as a Docker Hub rate limit, which the runner repairs inside the run rather than billing twice.
Frequently asked questions
When does Cirrus CI stop running jobs?
What is the best Cirrus CI alternative for macOS builds?
Why is my GitHub Actions bill mostly macOS minutes?
Why do macOS minutes cost 10x Linux minutes?
Related guides
References
- Cirrus Labs: Cirrus CI shutdown announcement and Cirrus Runners wind-down (verified 2026-09-20)
- scipy/scipy#24990: CI, Cirrus Runners will disappear soon (verified 2026-09-20)
- GitHub Actions runner per-minute rates and rounding (verified 2026-09-20)
- Namespace pricing: macOS shapes and the platform multiplier (verified 2026-09-20)
- Depot runner types: macOS runners at 8 CPUs and 24 GB (verified 2026-09-20)
- GitHub Actions documentation