chore(build): enforce Xcode 27.0 for Apple builds

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-12 11:30:39 -07:00
parent c09730e874
commit d0a1d61533
14 changed files with 157 additions and 14 deletions

View File

@@ -121,11 +121,16 @@ jobs:
mobile-swift:
name: Mobile Swift
runs-on: macos-latest
runs-on: xcode-27
timeout-minutes: 30
if: inputs.lane == 'mobile-swift' && inputs.mobile
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Install Flutter dependencies
run: cd mobile && flutter pub get

View File

@@ -23,18 +23,24 @@ env:
jobs:
desktop-build-macos:
name: Desktop Build (macOS)
runs-on: macos-latest
runs-on: xcode-27
timeout-minutes: 45
if: github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Verify macOS icon representations
run: scripts/macos-icon.sh --check
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
key: xcode-${{ hashFiles('.xcode-version') }}
workspaces: desktop/src-tauri
save-if: ${{ github.event_name != 'pull_request' }}
- name: Install desktop dependencies
@@ -64,7 +70,7 @@ jobs:
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ github.workspace }}/.cache/mesh-llama
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }}
- name: Build mesh llama native libraries
if: steps.llama_cache.outputs.cache-hit != 'true'
env:
@@ -88,7 +94,7 @@ jobs:
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ github.workspace }}/.cache/mesh-llama
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }}
- name: Build Tauri app
run: cd desktop && pnpm tauri build
env:

View File

@@ -71,6 +71,8 @@ jobs:
- '.github/workflows/_ci-desktop.yml'
- 'scripts/ci-apt-retry.sh'
- 'Justfile'
- '.xcode-version'
- 'scripts/check-xcode.sh'
- 'scripts/generate-macos-icon.py'
- 'scripts/macos-icon.sh'
- 'bin/uv'
@@ -86,6 +88,8 @@ jobs:
- 'pnpm-lock.yaml'
mobile:
- 'mobile/**'
- '.xcode-version'
- 'scripts/check-xcode.sh'
- 'scripts/mobile-release.sh'
- 'scripts/mobile-worktree-overrides.sh'
- 'scripts/mobile-worktree-clean.sh'
@@ -108,7 +112,9 @@ jobs:
scripts/test-postgres-test-discovery.sh
scripts/test-postgres-test-wrapper.sh
- name: Release workflow source contract
run: scripts/test-release-ref-contract.sh
run: |
scripts/test-release-ref-contract.sh
scripts/test-xcode-version.sh
- name: Relay image eligibility contract
run: scripts/test-relay-image-eligibility-workflow.sh
- name: Desktop release candidate contract

View File

@@ -12,7 +12,7 @@ jobs:
macos:
name: Prove macOS ${{ matrix.target }} cache visibility
if: github.repository == 'block/buzz'
runs-on: macos-latest
runs-on: xcode-27
timeout-minutes: 15
strategy:
fail-fast: false
@@ -28,6 +28,11 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Patch proof dependency graph
run: |

View File

@@ -13,7 +13,7 @@ jobs:
build:
name: Build macOS Intel canary
if: github.repository == 'block/buzz'
runs-on: macos-latest
runs-on: xcode-27
timeout-minutes: 60
env:
TARGET: x86_64-apple-darwin
@@ -31,6 +31,11 @@ jobs:
with:
persist-credentials: false
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Add Rust target

View File

@@ -52,7 +52,7 @@ jobs:
release:
name: Release
if: github.repository == 'block/buzz'
runs-on: macos-latest
runs-on: xcode-27
needs: setup
timeout-minutes: 60
permissions:
@@ -70,6 +70,11 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
@@ -108,7 +113,7 @@ jobs:
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ github.workspace }}/.cache/mesh-llama
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }}
- name: Build mesh llama native libraries
if: steps.llama_cache.outputs.cache-hit != 'true'
env:
@@ -132,7 +137,7 @@ jobs:
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ github.workspace }}/.cache/mesh-llama
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }}
- name: Build unsigned Tauri app
run: cd desktop && pnpm tauri build --verbose --no-sign --features mesh-llm --config src-tauri/tauri.release.conf.json
@@ -265,7 +270,7 @@ jobs:
release-macos-x64:
name: Release macOS (Intel)
if: github.repository == 'block/buzz'
runs-on: macos-latest
runs-on: xcode-27
needs: setup
timeout-minutes: 60
permissions:
@@ -284,6 +289,11 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh desktop-v "$VERSION"

View File

@@ -13,7 +13,7 @@ jobs:
build:
name: Build signed macOS canary
if: github.repository == 'block/buzz'
runs-on: macos-latest
runs-on: xcode-27
timeout-minutes: 60
permissions:
contents: read
@@ -32,6 +32,11 @@ jobs:
with:
persist-credentials: false
- name: Select pinned Xcode
run: |
export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer"
scripts/check-xcode.sh
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Get pnpm store directory
@@ -133,7 +138,7 @@ jobs:
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ github.workspace }}/.cache/mesh-llama
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }}
- name: Build mesh llama native libraries
if: steps.llama_cache.outputs.cache-hit != 'true'
@@ -158,7 +163,7 @@ jobs:
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ github.workspace }}/.cache/mesh-llama
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }}
- name: Build unsigned Tauri app
run: cd desktop && pnpm tauri build --verbose --no-sign --features mesh-llm --config src-tauri/tauri.canary.conf.json

1
.xcode-version Normal file
View File

@@ -0,0 +1 @@
27.0.0

View File

@@ -102,6 +102,29 @@ Hermit pins Rust, `just`, Node, pnpm, and other tools to the versions in
upfront. If you don't use Hermit, ensure your toolchain meets the minimum
versions in the table above.
#### macOS: Xcode
Apple builds use the exact version in [`.xcode-version`](.xcode-version),
currently Xcode 27.0.0. Install that version and set `DEVELOPER_DIR` to its
`Xcode.app/Contents/Developer` directory for your shell. For example:
```bash
export DEVELOPER_DIR=/Applications/Xcode_27.app/Contents/Developer
scripts/check-xcode.sh
```
Local Tauri build/dev commands and native `just` recipes reject a different
active version. Direct Cargo, Swift, Flutter, or Xcode invocations should run
`scripts/check-xcode.sh` first in the same environment. Nothing installs Xcode
or changes the system-wide `xcode-select` setting automatically. Xcode 27
requires macOS 26.6 or later.
GitHub Apple jobs use the `xcode-27` runner image and select the file's exact
version before compiling. That runner image is currently a GitHub public
preview, even though the selected Xcode 27.0 compiler is stable. When changing
the pin, verify runner availability and coordinate the internal release
pipeline's scheduling pin in `squareup/buzz-releases`.
#### Linux: Tauri system libraries
Hermit pins language toolchains, not system libraries. On Linux, the desktop

View File

@@ -220,25 +220,30 @@ _ensure-migrations: _ensure-services
# the default graph covers the `#[cfg(not(feature = "mesh-llm"))]` arms and
# the feature-enabled graph covers the mesh code.
desktop-tauri-clippy: _ensure-sidecar-stubs
./scripts/check-xcode.sh
cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets -- -D warnings
cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets --features mesh-llm -- -D warnings
# Check the desktop Tauri Rust crate compiles
desktop-tauri-check: _ensure-sidecar-stubs
./scripts/check-xcode.sh
cargo check --manifest-path {{desktop_tauri_manifest}}
# Run desktop Tauri Rust unit tests
desktop-tauri-test: _ensure-sidecar-stubs
./scripts/check-xcode.sh
cd desktop/src-tauri && cargo test --workspace
# Run the desktop Tauri lib tests with the mesh-llm feature graph
desktop-tauri-test-mesh: _ensure-sidecar-stubs
./scripts/check-xcode.sh
cargo test --manifest-path {{desktop_tauri_manifest}} --features mesh-llm --lib
# Run the native terminal latency gate explicitly on a known-idle host.
# This is intentionally excluded from shared CI: scheduler contention makes a
# wall-clock assertion flaky, and the release profile is the shipped shape.
desktop-terminal-performance-test:
./scripts/check-xcode.sh
cargo test --manifest-path desktop/src-tauri/crates/buzz-terminal/Cargo.toml --release --test latency g3_renderer_acquire_stays_within_frame_budget -- --ignored --exact --nocapture
# Verify compiled-flag behavior in each compile state.
@@ -255,6 +260,7 @@ desktop-terminal-performance-test:
desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
./scripts/check-xcode.sh
cd desktop/src-tauri
unset BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY BUZZ_BUILD_DEMO_SLUG
unset BUZZ_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY BUZZ_TEST_EXPECTED_DEMO_SLUG
@@ -314,6 +320,7 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
desktop-release-build target="aarch64-apple-darwin":
#!/usr/bin/env bash
set -euo pipefail
./scripts/check-xcode.sh
TARGET={{target}}
mkdir -p desktop/src-tauri/binaries
touch "desktop/src-tauri/binaries/buzz-acp-$TARGET"
@@ -331,6 +338,7 @@ desktop-release-build target="aarch64-apple-darwin":
desktop-demo-build demo_name target="aarch64-apple-darwin":
#!/usr/bin/env bash
set -euo pipefail
./scripts/check-xcode.sh
TARGET={{target}}
[[ "$(uname -s)" == "Darwin" && "$TARGET" == *-apple-darwin ]] || { echo "Demo DMGs require a macOS Apple target" >&2; exit 2; }
CONFIG_PATH="$(mktemp "${TMPDIR:-/tmp}/buzz-demo-config.XXXXXX")"
@@ -1015,6 +1023,7 @@ mobile-build-android:
mobile-dev:
#!/usr/bin/env bash
set -euo pipefail
./scripts/check-xcode.sh
if ! pgrep -x Simulator &>/dev/null; then
open -a Simulator
sleep 3

View File

@@ -31,6 +31,17 @@ function runTauri(args, options = {}) {
}
export function runTauriCommand(args) {
if (process.platform === "darwin" && ["build", "dev"].includes(args[0])) {
const check = spawnSync(
"bash",
[path.join(desktopRoot, "../scripts/check-xcode.sh")],
{
stdio: "inherit",
},
);
if (check.error) throw check.error;
if (check.status !== 0) return check.status ?? 1;
}
if (args[0] !== "build") return runTauri(args);
// Tauri runs beforeBuildCommand and then consumes frontendDist. Give the

23
scripts/check-xcode.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Validate the active Apple toolchain without changing xcode-select globally.
set -euo pipefail
[[ "$(uname -s)" == Darwin ]] || exit 0
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
expected="$(cat "$root/.xcode-version")"
[[ "$expected" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "Invalid .xcode-version: expected a major.minor.patch version" >&2
exit 1
}
if ! version="$(xcodebuild -version)"; then
echo "Buzz requires Xcode $expected. Set DEVELOPER_DIR to its Xcode.app/Contents/Developer." >&2
exit 1
fi
actual="$(printf '%s\n' "$version" | sed -n 's/^Xcode //p')"
[[ "$actual" == *.* ]] || actual="$actual.0"
[[ "$actual" == *.*.* ]] || actual="$actual.0"
if [[ "$actual" != "$expected" ]]; then
echo "Buzz requires Xcode $expected (.xcode-version); active toolchain reports: $version" >&2
echo "Set DEVELOPER_DIR to the matching Xcode.app/Contents/Developer and retry." >&2
exit 1
fi
printf '%s\n' "$version"

View File

@@ -24,6 +24,7 @@ cat > "$fixture/scripts/mobile-worktree-overrides.sh" <<'STUB'
exit 0
STUB
chmod +x "$fixture/bin/flutter" "$fixture/bin/pgrep" "$fixture/scripts/mobile-worktree-overrides.sh"
cp "$fixture/scripts/mobile-worktree-overrides.sh" "$fixture/scripts/check-xcode.sh"
export PATH="$fixture/bin:$PATH"
export CALL_LOG="$fixture/arguments"
unset BUZZ_PUSH_GATEWAY_URL

33
scripts/test-xcode-version.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Exercise version enforcement without installing or switching a real Xcode.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd)"
fixture="$(mktemp -d)"
trap 'rm -rf "$fixture"' EXIT
mkdir -p "$fixture/scripts" "$fixture/bin"
cp "$root/scripts/check-xcode.sh" "$fixture/scripts/"
cp "$root/.xcode-version" "$fixture/"
cat > "$fixture/bin/uname" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' "${TEST_OS:-Darwin}"
STUB
cat > "$fixture/bin/xcodebuild" <<'STUB'
#!/usr/bin/env bash
[[ "${TEST_XCODE:-}" != broken ]] || exit 1
printf 'Xcode %s\nBuild version fixture\n' "$TEST_XCODE"
STUB
chmod +x "$fixture/bin/"*
export PATH="$fixture/bin:$PATH"
check() { "$fixture/scripts/check-xcode.sh" >/dev/null 2>&1; }
export TEST_XCODE=27.0
check
export TEST_XCODE=27
check
for TEST_XCODE in 26.6 27.1 '27.0 beta' broken ''; do
export TEST_XCODE
if check; then echo "Accepted wrong Xcode: $TEST_XCODE" >&2; exit 1; fi
done
printf 'latest\n' > "$fixture/.xcode-version"
if check; then echo 'Accepted invalid pin' >&2; exit 1; fi
TEST_OS=Linux check
printf 'Xcode version enforcement passed\n'