Skip to content

ci: move macOS runners to macos-15 and pin the deployment target to 14.0 - #2849

Merged
DeusData merged 1 commit into
mainfrom
ci/macos-15-runners
Oct 31, 2026
Merged

DeusData merged 1 commit into
mainfrom
ci/macos-15-runners

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Why

GitHub retires the macos-14 runner image on 2026-11-02. The scheduled
brownouts before that date already cancel our macos-14 jobs, and after it
every job pinned to macos-14 stops getting a runner: the macOS arm64 PR
legs, the darwin-arm64 release build and the release smoke. This PR moves
all of them to macos-15 (arm64).

That move alone would quietly change what we ship. No deployment target is
set anywhere, so clang stamps the binary with the SDK's default minimum
(LC_BUILD_VERSION minos). Built on macos-15, the darwin-arm64 release
would refuse to start on macOS 14. So this PR also pins the minimum macOS to
14.0 for every Darwin build.

What changes

1. Runner labels (13 references in 9 workflows)

File:line (main -> branch) Job / leg Trigger Gates?
_test.yml:63 -> :66 test-unix lane unix-macos14 every PR that selects the lane (T1 and up), dry run, release yes: ci-ok needs test; release build needs test
_test.yml:95 -> :98 test-tsan lane tsan-mac PRs that select it, dry run, release yes, same chain
pr.yml:178 pr-smoke lane smoke-mac PRs that select it (platform/packaging) yes: ci-ok needs pr-smoke
pr.yml:217, pr.yml:257 if: matrix.os == 'macos-15' (ccache install, build + smoke) same as above moved in lockstep with :178, so the macOS steps still run
_build.yml:44 -> :46 darwin-arm64 release binary dry run, release yes: smoke/soak need build, release-draft needs both
_smoke.yml:36 release smoke, darwin-arm64 dry run, release yes: release-draft needs smoke
_soak.yml:47, _soak.yml:338 soak-quick and soak-asan, darwin-arm64 dry run, release, nightly-soak release: yes (release-draft needs soak); nightly: no
_memwaste.yml:118 event-macos (mode full only) dry run, release no: report-only, not in release-draft's needs
bug-repro.yml:43 repro board, macos group workflow_dispatch only no
cache-warm.yml:30 nightly ccache warmer schedule + dispatch no (build only)
smoke.yml:42 all-platform smoke dispatch + qa/smoke-** push no (non-gating by design)

Duplicates removed. Three places already ran a macos-15 leg next to
the macos-14 one. A plain rename would have produced two identical legs,
so:

  • _test.yml BROAD_UNIX: the broad macos-15 entry is dropped (the core
    leg runs there now).
  • _smoke.yml BROAD_UNIX: now []. Its only entry was macos-15
    darwin-arm64, which is now the core leg.
  • smoke.yml: the macos-14 entry is dropped; the existing macos-15
    entry stays.

Effect, checked by executing the real "Compute matrices" scripts from main
and from this branch:

  • PR shape (LANES = every lane, pr shard profile): the same legs and the
    same counts. Only macos-14 became macos-15.
  • Release/dry-run shape (BROAD=true, LANES=all): one fewer macOS arm64
    test leg and one fewer smoke leg, because macOS 14 + 15 became macOS 15
    only. No duplicate legs.

Lane names are unchanged. unix-macos14, tsan-mac and smoke-mac are
lane keys: scripts/ci/select-lanes.sh, scripts/test-impact/replay-selector.sh,
tests/fixtures/select-lanes/cases.tsv and the lane contracts key on them.
ci-ok (scripts/ci/require-all-green.sh) reads the lane back from the
parenthesised matrix values of the job name. It takes the one value that is a
known lane, so the runner label inside the job name never matters. Renaming
unix-macos14 would be a separate change, so this PR keeps it and adds a
comment in _test.yml. The two synthetic job names in
tests/test_lane_gate_contract.sh now say macos-15, which proves the gate
maps the new names.

Not touched: test-lsan-macos runs on macos-latest (GitHub's alias). Four
comments still mention macos-14
(scripts/memwaste.sh, scripts/run-test-wave.py, src/foundation/subprocess.c,
tests/test_watcher.c), but each one records a past observation on that
runner, so they stay as written.

2. Minimum macOS pinned to 14.0 (Makefile.cbm)

-mmacosx-version-min=14.0 is folded into CC/CXX with override,
right next to the existing ARCHFLAGS fold. This means it:

  • reaches every compile and every link, including the vendored objects that
    use their own *_CFLAGS (mimalloc, sqlite3, tre, zstd, lz4, the grammars);
  • applies on every build path: scripts/build.sh (release), scripts/test.sh,
    direct make (cache-warm), and the recursive diag/LSan/coverage/fuzz
    lanes, which re-enter the Makefile with CC=;
  • is part of each command line, so compiler caches key on it. That is why it
    is a flag and not an exported MACOSX_DEPLOYMENT_TARGET;
  • is applied only to Apple targets. Detection uses the compiler's
    __APPLE__ predefine, not the host, so a non-Apple cross compiler on a Mac
    gets nothing. Linux and Windows builds are unchanged.

Why 14.0. Releases built on the macos-14 runner carried that image's
SDK default as their minimum, a 14.x value by the mechanism shown in the
proof below. So 14.0 drops no user who can run today's arm64 release. Going lower (13, 12, ...) would be a new
support-policy decision, and after 2026-11-02 no hosted runner older than
macOS 15 is left to back it up. The Intel binary (macos-15-intel) gets the
same pin. Until now it carried that image's SDK default (15.x), so this
probably extends Intel support to macOS 14. Confirm on the next dry run (see
below).

API safety.

  • No __builtin_available, @available or API_AVAILABLE exists in src/,
    internal/ or vendored/.
  • The two vendored version checks (mimalloc MAC_OS_X_VERSION_MIN_REQUIRED >= 10_15,
    sqlite > 1050) resolve the same way at 14.0 as at the host default.
  • The pinned binary has zero weak imports, and its undefined-symbol set
    is identical to main's. Clang weak-links any SDK API newer than the
    deployment target, so nothing referenced is newer than macOS 14.0.
  • The -Werror build has zero warnings, so no
    -Wunguarded-availability-new hits and no linker "built for newer macOS"
    notes.

Proof (local, macOS 26.6, Apple clang 21, SDK 26.5)

Build LC_BUILD_VERSION
origin/main production binary (scripts/build.sh CC=cc CXX=c++) minos 26.0, sdk 26.5
this branch, same command minos 14.0, sdk 26.5
this branch, build/c/test-runner via direct make (no ARCHFLAGS) minos 14.0
  • Driver probe with the installed MacOSX15.4.sdk: unpinned gives
    arm64-apple-macosx15.4.0, pinned gives arm64-apple-macosx14.0.0. That is
    the macos-15 runner case in miniature.
  • An x86_64 probe link with the pin gives minos 14.0.
  • All 171 compile/link commands of the production build carry the flag, and
    none lacks it.
  • Dylib dependencies are unchanged: libSystem, libc++, libz.
  • Suites: platform passed 32/32. daemon_ipc passed 60, with 2 skipped;
    both skips are Linux-only by design.
  • Contract tests that read the workflows all pass: test_lane_gate_contract,
    test_lane_wiring_contract, test_select_lanes,
    test_release_gate_chain_contract, test_smoke_fixture_contract,
    test_soak_daemon_recovery_contract, test_venue_parity_contract,
    test_coverage_map, test_security_gate_fail_closed,
    test_windows_bundle_contract.
  • All 9 changed workflows parse.

Cost and flake surface

  • Cost: unchanged per leg. macos-15 arm64 is the same standard runner
    class as macos-14 (3 vCPU M1, 7 GB). PR CI has the same leg count. The
    dry run and release have one fewer macOS arm64 test leg and one fewer smoke
    leg.
  • One-time cold cache: the ccache keys contain matrix.os. The first
    macos-15 runs of unix-macos14, tsan-mac and smoke-mac may build
    cold, which the _test.yml comment puts at about 8-12 minutes. That lasts
    until cache-warm writes macos-15 entries on main (it moves in this PR
    too).
  • New toolchain: the new image ships a newer Xcode/Apple clang. Under
    -Werror a new warning would fail the gating legs. A local Apple clang 21
    build has zero warnings, so this is unlikely, but unix-macos14 is the
    first leg to read.
  • Sanitizers: tsan-mac is the leg most tied to the OS/Xcode pair. If it
    goes red, attribute it first (runtime vs. code) before touching anything.

What to watch on the first run

  1. test / test-unix (macos-15, ..., unix-macos14, ...) and tsan-mac:
    compile warnings, sanitizer runtime start-up, and the timing-sensitive
    watcher/subprocess tests (same hardware class, so no change is expected).
  2. pr-smoke (macos-15, smoke-mac): codesign --sign - --force, then
    smoke-local.sh. The binary-security phase must still show
    codesign --verify rejecting the tampered copy (the deterministic check
    from fix(smoke): de-flake 10c by corrupting the signature instead of removing it #692).
  3. The next release dry run: the darwin-arm64 build on macos-15, the
    release smoke, and the soak legs. Then run
    vtool -show-build / otool -l | grep -A4 LC_BUILD_VERSION on both
    darwin release candidates. Both must show minos 14.0.

Known limits / follow-ups (not in this PR)

  • After 2026-11-02 no hosted runner runs macOS 14. The pin guarantees that
    the loader accepts the binary on macOS 14.0+, and the weak-import check
    shows no newer API is referenced. But macOS-14 kernel behaviour is no
    longer exercised in CI (for example, the linkat / AT_SYMLINK_NOFOLLOW_ANY
    difference documented in src/daemon/ipc.c). A local macOS 14 VM leg
    would close that gap.
  • A one-line release-smoke assertion that the darwin artifacts report
    minos 14.0 would guard the pin against regression. No runtime check on
    a macOS 15 runner can catch a raised minimum.
  • The unix-macos14 lane rename, and an optional second arm64 macOS version
    in the broad matrices (e.g. a newer image), are separate decisions.

Test plan

  • Production binary main vs branch: minos 26.0 vs minos 14.0
  • Test runner via direct make: minos 14.0; platform + daemon_ipc green
  • Zero warnings, zero weak imports, identical undefined-symbol set
  • Matrix scripts executed (main vs branch): PR legs identical, no duplicates
  • 10 workflow-reading contract tests green
  • PR CI: unix-macos14, tsan-mac, smoke-mac green on macos-15
  • Next dry run: both darwin release candidates report minos 14.0

GitHub retires the macos-14 runner image on 2026-11-02, and its brownouts
already cancel our macos-14 jobs. Move every macos-14 runner reference to
macos-15 (arm64): the release darwin-arm64 build, the unix-macos14 and
tsan-mac test legs, the smoke-mac PR leg (its two matrix.os guards move in
lockstep), the release smoke, both soak legs, the memwaste event lane, the
bug-repro board, the cache warmer and the all-platform smoke. Lane names
stay unchanged: select-lanes.sh, ci-ok and the lane contracts key on them,
never on the runner label.

Two broad legs already ran on macos-15 (_test.yml BROAD_UNIX, _smoke.yml
BROAD_UNIX) and smoke.yml listed both images, so the moved legs would have
duplicated them. Those duplicates are dropped; PR CI keeps exactly the
same legs.

Without an explicit minimum, clang stamps the binary with the SDK's
default (LC_BUILD_VERSION minos), so a release built on macos-15 would
refuse to start on macOS 14. Makefile.cbm now folds
-mmacosx-version-min=14.0 into CC/CXX next to ARCHFLAGS for Apple targets
(detected from the compiler's target, not the host), so it reaches every
compile and link, vendored objects included, on every build path.

Proof on macOS 26.6 / SDK 26.5: the production binary built from
origin/main reports minos 26.0; with this change it reports minos 14.0,
with zero warnings under -Werror, no weak imports and an identical
undefined-symbol set. The test runner (direct make, no env.sh) is minos
14.0 too; platform and daemon_ipc suites pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant