GitHub Actions macOS 14 Runner Retirement: Survive the October Brownouts and Migrate Before November 2

GitHub Actions macOS 14 Runner Retirement: Survive the October Brownouts and Migrate Before November 2

Starting October 5, 2026 at 14:00 UTC, GitHub will deliberately fail every job that asks for a macOS 14 hosted runner, for ten hours at a time, on eight separate days this month. On November 2, 2026 the image goes away permanently. If any workflow in your organization still says runs-on: macos-14 (or macos-14-large / macos-14-xlarge), it has a scheduled outage on your calendar whether you put it there or not. The label swap itself is one line. The reason teams get burned is everything that line was silently pinning: Xcode 15, the iOS 17 simulators, and an x64 vs arm64 choice that the replacement labels make differently.

What GitHub announced, exactly

The official notice lives in the actions/runner-images repository (issue #13518). The facts that matter:

  • Deprecation started July 6, 2026. Since then, macOS 14 jobs may queue longer during peak hours.
  • Retirement is November 2, 2026, for both GitHub Actions and Azure DevOps (Microsoft-hosted agents are built from the same images).
  • Affected labels: macos-14, macos-14-large, macos-14-xlarge.
  • Brownouts. Jobs on those labels fail during these windows:
BrownoutStart (UTC)End (UTC)IST equivalent
1Oct 5, 14:00Oct 6, 00:00Oct 5, 19:30 to Oct 6, 05:30
2Oct 12, 14:00Oct 13, 00:00Oct 12, 19:30 to Oct 13, 05:30
3Oct 16, 14:00Oct 17, 00:00Oct 16, 19:30 to Oct 17, 05:30
4Oct 19, 14:00Oct 20, 00:00Oct 19, 19:30 to Oct 20, 05:30
5Oct 23, 14:00Oct 24, 00:00Oct 23, 19:30 to Oct 24, 05:30
6Oct 26, 14:00Oct 27, 00:00Oct 26, 19:30 to Oct 27, 05:30
7Oct 29, 14:00Oct 30, 00:00Oct 29, 19:30 to Oct 30, 05:30
8Oct 30, 14:00Oct 31, 00:00Oct 30, 19:30 to Oct 31, 05:30

The windows land on US business afternoons and Indian evenings. Nightly release builds and scheduled cron workflows are the likeliest to hit them without anyone watching.

Why this one is more than a label rename

GitHub supports the two latest stable macOS versions, so macOS 14 Sonoma ages out now that macOS 26 is the default. On paper you swap 14 for 15 and move on. In practice the macOS 14 image is the last one carrying an entire generation of Apple tooling. I compared the published software manifests for the current arm64 images:

macos-14 (arm64)macos-15 (arm64)macos-latest = macos-26 (arm64)
Default Xcode15.416.426.6
Xcode versions installed15.0.1 to 16.216.0 to 26.326.0.1 to 26.6
iOS simulator runtimes17.0 to 18.218.5 to 26.226.2 to 26.5
iPhone 15 simulator devicesYesNoNo
Clang (Apple)15.0.017.0.021.0.0
Node.js22.23.222.23.224.20.0
Ruby3.3.123.3.123.4.10

Three practical consequences:

  1. There is no hosted runner with Xcode 15 after November 2. If your project builds only with Xcode 15.x, the migration is an Xcode upgrade, not a YAML edit.
  2. Hard-coded simulator destinations break. -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' resolves on macOS 14 and fails with "Unable to find a destination matching the provided destination specifier" on macOS 15 and 26.
  3. macos-latest is a bigger jump than macos-15. It moves you to Xcode 26 and Node 24 at once. Pick it on purpose, not because it was the first label you saw.

Step 1: Find every workflow that uses macOS 14

Inside one repository:

1grep -rnE "macos-14(-large|-xlarge)?" .github/workflows/

Don't forget matrix definitions, which a plain runs-on: search misses:

1strategy:
2  matrix:
3    os: [ubuntu-24.04, macos-14, windows-2025]   # <- still affected
4runs-on: ${{ matrix.os }}

Across an entire organization, GitHub code search via the gh CLI is the fastest route:

1gh search code "macos-14" --owner your-org --filename "*.yml" \
2  --json repository,path --jq '.[] | "\(.repository.nameWithOwner)  \(.path)"'

Also check reusable workflows and composite actions that take the runner OS as an input. The macos-14 string might live in a caller's with: block rather than in the workflow that actually runs the job.

Azure DevOps users should search their pipelines for the equivalent image name:

1grep -rnE "vmImage:\s*'?macOS-14'?" --include="*.yml" .

Step 2: Pick the right replacement label

Match the architecture you actually use. The arm64 and Intel naming is not symmetrical:

You use todayArchitectureMove to (same arch)
macos-14arm64 (Apple silicon)macos-15 (safest) or macos-26 / macos-latest
macos-14-xlargearm64macos-15-xlarge or macos-26-xlarge / macos-latest-xlarge
macos-14-largex64 (Intel)macos-15-large / macos-15-intel, or macos-26-large / macos-26-intel / macos-latest-large

Note that macos-14-large is the Intel image. If you build x86_64-only native binaries there and swap to macos-15 because it "looks like the same thing", you just moved to arm64. Keep -large or -intel if you need x64.

For most teams the low-risk move is to pin to an explicit version, not latest:

 1jobs:
 2  build-ios:
 3    runs-on: macos-15
 4    steps:
 5      - uses: actions/checkout@v7
 6      - name: Select Xcode explicitly
 7        run: sudo xcode-select -s /Applications/Xcode_16.4.app
 8      - name: Show toolchain
 9        run: |
10          xcodebuild -version
11          xcrun simctl list runtimes          

Printing the toolchain costs two seconds and saves a lot of guesswork the first time a build behaves differently.

Step 3: Fix the Xcode and simulator assumptions

If you pinned Xcode with a path, check that the path still exists on the new image:

1# On macos-14 this worked; on macos-15/26 the directory does not exist
2sudo xcode-select -s /Applications/Xcode_15.4.app
3# xcode-select: error: invalid developer directory '/Applications/Xcode_15.4.app'

If you use maxim-lobanov/setup-xcode or a similar action with xcode-version: '15.4', it fails the same way. Bump to a version the image ships (16.x on macos-15, 26.x on macos-26).

For simulator destinations, stop hard-coding device names that tie you to one image generation. Either target a device that exists on the new image:

1xcodebuild test \
2  -scheme MyApp \
3  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6'

or resolve one at runtime so the next image retirement doesn't break you again:

1UDID=$(xcrun simctl list devices available -j \
2  | jq -r '[.devices | to_entries[] | select(.key | test("iOS")) | .value[]
3            | select(.name | startswith("iPhone"))][0].udid')
4xcodebuild test -scheme MyApp -destination "id=${UDID}"

If you must keep testing against iOS 17 specifically, the hosted macOS 15 image doesn't include that runtime. You can try downloading the runtime inside the job with xcodebuild -downloadPlatform iOS -buildVersion <version> (expect several extra minutes per run, and confirm your selected Xcode supports that runtime), or move that test lane to a self-hosted Mac.

Step 4: Test the migration before a brownout tests it for you

Run the new label on a branch alongside the old one before you delete anything:

1strategy:
2  fail-fast: false
3  matrix:
4    os: [macos-14, macos-15]
5runs-on: ${{ matrix.os }}

Once macos-15 is green, drop macos-14 from the matrix and merge. Do this before October 5, or at least outside the brownout windows above. A brownout failure looks like an infrastructure error, not a test failure, which wastes debugging time.

Best practices

  • Pin explicit versions (macos-15) for release pipelines and use macos-latest only in a non-blocking canary job that warns you early about the next default change.
  • Select Xcode explicitly in every Apple build job. The image default changes between images (15.4, then 16.4, then 26.6), and a silent default change is how "nothing changed but the build broke" happens.
  • Print xcodebuild -version and sw_vers at the start of the job so failure logs show the environment.
  • Put the runner label in one place. A repository or organization variable (runs-on: ${{ vars.MACOS_RUNNER }}) turns the next retirement into a one-line change.
  • Keep matching labels for Intel and arm64. Move -large to -large/-intel and -xlarge to -xlarge, unless you mean to change architecture.

Common mistakes to avoid

  • Fixing runs-on: and missing the matrix. Search for the bare string macos-14, not just runs-on: macos-14.
  • Retrying a brownout failure and calling it flaky. It isn't. It will fail again in the next window and permanently after November 2.
  • Jumping straight to macos-latest in release pipelines. You inherit Xcode 26 and Node 24 at once. That's fine if you tested it, painful if you didn't.
  • Treating macos-14-large as arm64. It's the Intel image; its replacement is macos-15-large or macos-15-intel.
  • Forgetting Azure DevOps. Pipelines with vmImage: 'macOS-14' are on the same retirement schedule.

Troubleshooting

Job fails immediately with no step output on October 5 evening (IST). Check the timestamp against the brownout table. If the job requested a macOS 14 label inside a window, that's the brownout. The fix is the migration, not a rerun.

xcode-select: error: invalid developer directory. The Xcode path you hard-coded isn't installed on the new image. Run ls /Applications | grep Xcode in a debug step and pick a listed version.

Unable to find a destination matching the provided destination specifier. Your simulator name or OS version doesn't exist on the new image. Run xcrun simctl list devices available and update the destination, or use the runtime lookup above.

CocoaPods or Swift Package resolution fails after the switch. A newer Xcode can expose deployment-target warnings that are treated as errors, or a dependency that doesn't compile with the newer Swift toolchain. Update the dependency first. Lowering the warning level only hides the problem until the next Xcode bump.

FAQ

Will my macos-14 jobs fail on October 5 even if I do nothing? Yes, but only for jobs that start between 14:00 and 00:00 UTC on that day and the other seven brownout days. Outside those windows they keep running, with possibly longer queues, until November 2. After that they fail every time.

Is macos-15 or macos-26 the better target? For release pipelines, macos-15 is the smaller change: it still has Xcode 16.x and Node 22, and also includes Xcode 26.0 to 26.3 for when you're ready. Use macos-26 if you already ship with Xcode 26.

I still need Xcode 15. What are my options? No GitHub-hosted image will carry it after November 2. Your options are upgrading the project to Xcode 16+ (the long-term fix) or running that job on a self-hosted macOS runner you maintain yourself.

Does this affect self-hosted macOS runners? No. The retirement covers GitHub-hosted images only. Self-hosted runners keep whatever OS you installed, as long as the runner application itself stays current.

Does Azure DevOps get the same brownouts? GitHub's notice lists both GitHub Actions and Azure DevOps for the November 2 retirement. Treat any macOS-14 vmImage pipeline as on the same deadline.

Key takeaways

WhatDetail
Affected labelsmacos-14, macos-14-large (Intel), macos-14-xlarge
First brownoutOct 5, 2026, 14:00 to 00:00 UTC (eight windows total in October)
Final retirementNovember 2, 2026 (GitHub Actions and Azure DevOps)
Safest arm64 targetmacos-15 (Xcode 16.4 default, 16.0 to 26.3 available)
Biggest hidden breakXcode 15.x and iOS 17 simulators don't exist on any replacement image
Intel buildsmacos-14-large becomes macos-15-large / macos-15-intel
Future-proofingStore the runner label in a variable; select Xcode explicitly

Further Reading