Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ jobs:
- name: Validate nightly tag push auth
run: ./tests/test_ci_nightly_tag_push_auth.sh

- name: Validate nightly Xcode selection
run: ./tests/test_ci_nightly_xcode_selection.sh

- name: Validate universal nightly workflow
run: bash ./tests/test_nightly_universal_build.sh

- name: Validate release asset guard
run: node scripts/release_asset_guard.test.js

Expand Down
66 changes: 7 additions & 59 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,12 @@ jobs:
build-sign-notarize-nightly:
needs: decide
if: needs.decide.outputs.should_build == 'true'
# Run on the macOS 15 host because zig 0.15.2 can cross-link the universal
# (arm64 + x86_64) Ghostty CLI helper only against a pre-26 SDK. The Swift
# app is built with the macOS 26 Xcode on this same runner so it adopts
# Liquid Glass on Tahoe; the helper is built separately with the older Xcode
# and injected before signing (see Select Xcode + the helper build/inject
# steps). Building the whole app with Xcode 16 (#5022) shipped a macOS 15 SDK
# binary that Tahoe forced into the legacy non-Liquid-Glass appearance.
# Run on the macOS 15 host that carries the signing/notarization secrets.
# The Swift app still selects an Xcode with the macOS 26 SDK so it adopts
# Liquid Glass on Tahoe. The Ghostty CLI helper is built separately and
# injected before signing, preferring a pre-26 SDK when the runner image has
# one but falling back to the selected app Xcode when the image only ships
# Xcode 26. The helper build remains required and lipo-verified below.
runs-on: ${{ vars.MACOS_RUNNER_15 || 'warp-macos-15-arm64-6x' }}
timeout-minutes: 30
steps:
Expand Down Expand Up @@ -141,57 +140,7 @@ jobs:
if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true'
run: |
set -euo pipefail
# The app must link against the macOS 26 SDK so it adopts Liquid Glass
# on Tahoe (the team is pinned to Xcode 26 via .xcode-version). The zig
# Ghostty CLI helper must link against a pre-26 SDK because zig 0.15.2
# cannot cross-link x86_64 against the macOS 26 SDK. Both Xcodes ship on
# the macOS 15 runner image, so pick the newest of each by SDK version.
# Compare SDK versions numerically (not lexicographically) so e.g.
# 26.10 sorts above 26.3.
sdk_rank() {
# "26.2" -> 26002 ; "26" -> 26000
local v="$1" maj min
maj="${v%%.*}"
min="${v#*.}"
[ "$min" = "$v" ] && min=0
min="${min%%.*}"
printf '%d' "$(( maj * 1000 + min ))"
}
APP_DEVELOPER_DIR=""; APP_SDK_VER=""; APP_RANK=-1
HELPER_DEVELOPER_DIR=""; HELPER_SDK_VER=""; HELPER_RANK=-1
while IFS= read -r app; do
[ -n "$app" ] || continue
dev="$app/Contents/Developer"
[ -d "$dev" ] || continue
sdk_ver="$(DEVELOPER_DIR="$dev" xcrun --sdk macosx --show-sdk-version 2>/dev/null || true)"
[ -n "$sdk_ver" ] || continue
rank="$(sdk_rank "$sdk_ver")"
echo "Found $app -> macOS SDK $sdk_ver (rank $rank)"
if [ "${sdk_ver%%.*}" -ge 26 ]; then
if [ "$rank" -gt "$APP_RANK" ]; then
APP_DEVELOPER_DIR="$dev"; APP_SDK_VER="$sdk_ver"; APP_RANK="$rank"
fi
else
if [ "$rank" -gt "$HELPER_RANK" ]; then
HELPER_DEVELOPER_DIR="$dev"; HELPER_SDK_VER="$sdk_ver"; HELPER_RANK="$rank"
fi
fi
done < <(find /Applications -maxdepth 1 -name 'Xcode*.app' -print 2>/dev/null)

if [ -z "$APP_DEVELOPER_DIR" ]; then
echo "No Xcode with the macOS 26+ SDK found; the app would ship without Liquid Glass on Tahoe." >&2
exit 1
fi
if [ -z "$HELPER_DEVELOPER_DIR" ]; then
echo "No pre-26 Xcode found to cross-link the universal Ghostty CLI helper (zig 0.15.2 cannot link x86_64 against the macOS 26 SDK)." >&2
exit 1
fi

echo "App build Xcode (DEVELOPER_DIR): $APP_DEVELOPER_DIR (macOS SDK $APP_SDK_VER)"
echo "Helper build Xcode (HELPER_DEVELOPER_DIR): $HELPER_DEVELOPER_DIR (macOS SDK $HELPER_SDK_VER)"
DEVELOPER_DIR="$APP_DEVELOPER_DIR" xcodebuild -version
echo "DEVELOPER_DIR=$APP_DEVELOPER_DIR" >> "$GITHUB_ENV"
echo "HELPER_DEVELOPER_DIR=$HELPER_DEVELOPER_DIR" >> "$GITHUB_ENV"
./scripts/select-nightly-xcodes.sh

- name: Install build deps
if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true'
Expand All @@ -203,7 +152,6 @@ jobs:
- name: Build universal Ghostty CLI helper
if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true'
env:
# zig 0.15.2 can cross-link the x86_64 slice only against a pre-26 SDK.
DEVELOPER_DIR: ${{ env.HELPER_DEVELOPER_DIR }}
run: |
set -euo pipefail
Expand Down
90 changes: 90 additions & 0 deletions scripts/select-nightly-xcodes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Select Xcodes for the nightly macOS build.
set -euo pipefail

APPLICATIONS_DIR="${CMUX_XCODE_APPLICATIONS_DIR:-/Applications}"
PRINT_VERSION="${CMUX_SELECT_XCODE_PRINT_VERSION:-1}"

sdk_major() {
local v="$1" maj
maj="${v%%.*}"
case "$maj" in
''|*[!0-9]*) return 1 ;;
esac
printf '%d' "$maj"
}

sdk_rank() {
local v="$1" maj min
maj="${v%%.*}"
min="${v#*.}"
[ "$min" = "$v" ] && min=0
min="${min%%.*}"
case "$maj" in
''|*[!0-9]*) return 1 ;;
esac
case "$min" in
''|*[!0-9]*) min=0 ;;
esac
printf '%d' "$(( maj * 1000 + min ))"
}

APP_DEVELOPER_DIR=""
APP_SDK_VER=""
APP_RANK=-1
HELPER_DEVELOPER_DIR=""
HELPER_SDK_VER=""
HELPER_RANK=-1

while IFS= read -r app; do
[ -n "$app" ] || continue
dev="$app/Contents/Developer"
[ -d "$dev" ] || continue
sdk_ver="$(DEVELOPER_DIR="$dev" xcrun --sdk macosx --show-sdk-version 2>/dev/null || true)"
[ -n "$sdk_ver" ] || continue
if ! major="$(sdk_major "$sdk_ver")"; then
echo "Ignoring $app with unparsable macOS SDK version: $sdk_ver" >&2
continue
fi
if ! rank="$(sdk_rank "$sdk_ver")"; then
echo "Ignoring $app with unparsable macOS SDK version: $sdk_ver" >&2
continue
fi
echo "Found $app -> macOS SDK $sdk_ver (rank $rank)"
if [ "$major" -ge 26 ]; then
if [ "$rank" -gt "$APP_RANK" ]; then
APP_DEVELOPER_DIR="$dev"
APP_SDK_VER="$sdk_ver"
APP_RANK="$rank"
fi
else
if [ "$rank" -gt "$HELPER_RANK" ]; then
HELPER_DEVELOPER_DIR="$dev"
HELPER_SDK_VER="$sdk_ver"
HELPER_RANK="$rank"
fi
fi
done < <(find "$APPLICATIONS_DIR" -maxdepth 1 -name 'Xcode*.app' -print 2>/dev/null | sort)

if [ -z "$APP_DEVELOPER_DIR" ]; then
echo "No Xcode with the macOS 26+ SDK found; the app would ship without Liquid Glass on Tahoe." >&2
exit 1
fi

if [ -z "$HELPER_DEVELOPER_DIR" ]; then
HELPER_DEVELOPER_DIR="$APP_DEVELOPER_DIR"
HELPER_SDK_VER="$APP_SDK_VER"
echo "No pre-26 Xcode found for the Ghostty CLI helper; falling back to the app Xcode. The universal helper build and lipo verification remain required." >&2
Comment on lines +75 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the helper off the macOS 26 SDK

On single-Xcode runners this fallback sets HELPER_DEVELOPER_DIR to the same Xcode 26 SDK selected for the app, but the very next nightly step runs the real build-ghostty-cli-helper.sh --universal with that value as DEVELOPER_DIR. The repo's release lane still builds this helper on the macOS 15/pre-26 lane because Zig 0.15.2 cannot link it on macOS 26, so the intended single-Xcode nightly will get past selection only to fail during the required helper build rather than producing a nightly.

Useful? React with 👍 / 👎.

fi
Comment on lines +74 to +78

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback may not unblock single-Xcode runners if zig still can't link x86_64 against macOS 26 SDK

The nightly.yml app-build step still carries a comment saying the in-Xcode zig build "would fail to cross-link x86_64 against the macOS 26 SDK" — and that step explicitly sets CMUX_SKIP_ZIG_BUILD=1 to skip it. The "Build universal Ghostty CLI helper" step also invokes a zig-driven universal build under DEVELOPER_DIR: ${{ env.HELPER_DEVELOPER_DIR }}. When the fallback here sets HELPER_DEVELOPER_DIR to the macOS 26 SDK Xcode, the standalone build-ghostty-cli-helper.sh --universal will run with that same macOS 26 SDK. If build-ghostty-cli-helper.sh shares the same zig x86_64 cross-link limitation, the CI will still fail on single-Xcode runners — just at the helper-build step instead of "Select Xcode". A brief inline comment confirming that the standalone script is exempt from the zig macOS 26 SDK limitation would make the fallback logic self-documenting.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


echo "App build Xcode (DEVELOPER_DIR): $APP_DEVELOPER_DIR (macOS SDK $APP_SDK_VER)"
echo "Helper build Xcode (HELPER_DEVELOPER_DIR): $HELPER_DEVELOPER_DIR (macOS SDK $HELPER_SDK_VER)"

if [ -n "${GITHUB_ENV:-}" ]; then
echo "DEVELOPER_DIR=$APP_DEVELOPER_DIR" >> "$GITHUB_ENV"
echo "HELPER_DEVELOPER_DIR=$HELPER_DEVELOPER_DIR" >> "$GITHUB_ENV"
fi

if [ "$PRINT_VERSION" != "0" ]; then
DEVELOPER_DIR="$APP_DEVELOPER_DIR" xcodebuild -version
fi
89 changes: 89 additions & 0 deletions tests/test_ci_nightly_xcode_selection.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Regression test for nightly Xcode selection on one-Xcode runner images.
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$ROOT_DIR/scripts/select-nightly-xcodes.sh"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

BIN_DIR="$TMP_DIR/bin"
APPS_DIR="$TMP_DIR/Applications"
mkdir -p "$BIN_DIR" "$APPS_DIR"

cat > "$BIN_DIR/xcrun" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

if [ "${1:-}" = "--sdk" ] && [ "${2:-}" = "macosx" ] && [ "${3:-}" = "--show-sdk-version" ]; then
cat "${DEVELOPER_DIR:?}/sdk-version"
exit 0
fi

echo "unexpected xcrun invocation: $*" >&2
exit 2
EOF
chmod +x "$BIN_DIR/xcrun"

make_xcode() {
local name="$1" sdk="$2" dev
dev="$APPS_DIR/$name/Contents/Developer"
mkdir -p "$dev"
printf '%s\n' "$sdk" > "$dev/sdk-version"
}

run_selector() {
local out="$1" env_file="$2"
PATH="$BIN_DIR:$PATH" \
CMUX_XCODE_APPLICATIONS_DIR="$APPS_DIR" \
CMUX_SELECT_XCODE_PRINT_VERSION=0 \
GITHUB_ENV="$env_file" \
"$SCRIPT" > "$out" 2>&1
}

assert_env_line() {
local env_file="$1" expected="$2"
if ! grep -Fxq "$expected" "$env_file"; then
echo "FAIL: missing env line: $expected" >&2
echo "--- env file ---" >&2
cat "$env_file" >&2
exit 1
fi
}

make_xcode "Xcode.app" "26.2"
ONLY_OUT="$TMP_DIR/only.out"
ONLY_ENV="$TMP_DIR/only.env"
run_selector "$ONLY_OUT" "$ONLY_ENV"
assert_env_line "$ONLY_ENV" "DEVELOPER_DIR=$APPS_DIR/Xcode.app/Contents/Developer"
assert_env_line "$ONLY_ENV" "HELPER_DEVELOPER_DIR=$APPS_DIR/Xcode.app/Contents/Developer"
if ! grep -Fq "falling back to the app Xcode" "$ONLY_OUT"; then
echo "FAIL: one-Xcode selection must explain the helper fallback" >&2
cat "$ONLY_OUT" >&2
exit 1
fi

make_xcode "Xcode_16.app" "15.5"
DUAL_OUT="$TMP_DIR/dual.out"
DUAL_ENV="$TMP_DIR/dual.env"
run_selector "$DUAL_OUT" "$DUAL_ENV"
assert_env_line "$DUAL_ENV" "DEVELOPER_DIR=$APPS_DIR/Xcode.app/Contents/Developer"
assert_env_line "$DUAL_ENV" "HELPER_DEVELOPER_DIR=$APPS_DIR/Xcode_16.app/Contents/Developer"
Comment on lines +54 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No test for highest-rank selection among multiple macOS 26+ Xcodes

The dual-Xcode case tests one macOS 26 Xcode and one pre-26 Xcode. There is no test with two macOS 26+ Xcodes (e.g. Xcode.app at 26.2 and Xcode_26_3.app at 26.3) to verify that the highest-ranked app Xcode is actually chosen. Without this, a regression in the sdk_rank comparison path for the app side would go undetected.


rm -rf "$APPS_DIR"
mkdir -p "$APPS_DIR"
make_xcode "Xcode_16.app" "15.5"
MISSING_OUT="$TMP_DIR/missing.out"
MISSING_ENV="$TMP_DIR/missing.env"
if run_selector "$MISSING_OUT" "$MISSING_ENV"; then
echo "FAIL: selection must fail when no macOS 26+ SDK app Xcode exists" >&2
cat "$MISSING_OUT" >&2
exit 1
fi
if ! grep -Fq "No Xcode with the macOS 26+ SDK found" "$MISSING_OUT"; then
echo "FAIL: missing app Xcode failure did not explain the macOS 26 requirement" >&2
cat "$MISSING_OUT" >&2
exit 1
fi

echo "PASS: nightly Xcode selection supports one-Xcode macOS 26 runners"
8 changes: 4 additions & 4 deletions tests/test_ci_release_sdk_lane.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CI_FILE="$ROOT_DIR/.github/workflows/ci.yml"
RELEASE_FILE="$ROOT_DIR/.github/workflows/release.yml"

# nightly.yml is intentionally not covered here. It builds the macOS 26 SDK app
# with its own inline helper-build model (PR #5077) and self-guards via its
# "Select Xcode" loud-fail and "Verify nightly binary architectures" steps.
# This lane guards the release/CI artifact-download model added by this change.
# nightly.yml is intentionally not covered here. It has its own helper-build
# model and guards via test_ci_nightly_xcode_selection.sh plus
# test_nightly_universal_build.sh. This lane guards the release/CI
# artifact-download model.

job_section() {
local file="$1" job="$2"
Expand Down
Loading
Loading