Skip to content
30 changes: 14 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ on:

permissions:
contents: read
actions: write
Comment thread
cursor[bot] marked this conversation as resolved.

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -52,6 +51,9 @@ jobs:
- name: Validate release asset guard
run: node scripts/release_asset_guard.test.js

- name: Validate universal macOS app verifier
run: ./tests/test_verify_universal_macos_app.sh

- name: Validate current GhosttyKit checksum pin
run: ./tests/test_ci_ghosttykit_checksum_present.sh

Expand Down Expand Up @@ -689,6 +691,9 @@ jobs:
release-ghostty-cli-helper:
runs-on: ${{ vars.MACOS_RUNNER_15 || 'warp-macos-15-arm64-6x' }}
timeout-minutes: 20
permissions:
contents: read
actions: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -728,6 +733,9 @@ jobs:
# compiles into the same artifact shape as nightly and stable releases.
runs-on: ${{ vars.MACOS_RUNNER_26 || 'warp-macos-26-arm64-6x' }}
timeout-minutes: 20
permissions:
contents: read
actions: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -817,22 +825,12 @@ jobs:
ghostty-cli-helper/ghostty \
build-universal/Build/Products/Release/cmux.app

- name: Validate Release artifact slices
- name: Validate Release artifact contract
run: |
set -euo pipefail
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty"
test -x "$APP_BINARY"
test -x "$CLI_BINARY"
test -x "$HELPER_BINARY"
file "$APP_BINARY" "$CLI_BINARY" "$HELPER_BINARY"
SDK_VERSION="$(otool -l "$APP_BINARY" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')"
echo "App SDK version: $SDK_VERSION"
lipo "$APP_BINARY" -verify_arch arm64 x86_64
lipo "$CLI_BINARY" -verify_arch arm64 x86_64
lipo "$HELPER_BINARY" -verify_arch arm64 x86_64
[[ "$SDK_VERSION" == 26.* ]]
./scripts/verify-universal-macos-app.sh \
"build-universal/Build/Products/Release/cmux.app" \
--label "CI Release app" \
--require-sdk-prefix "26."
Comment thread
cursor[bot] marked this conversation as resolved.

ui-regressions:
runs-on: ${{ vars.MACOS_RUNNER_15 || 'warp-macos-15-arm64-6x' }}
Expand Down
17 changes: 4 additions & 13 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,19 +284,10 @@ jobs:
- name: Verify nightly binary architectures
if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true'
run: |
set -euo pipefail
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty"
APP_ARCHS="$(lipo -archs "$APP_BINARY")"
CLI_ARCHS="$(lipo -archs "$CLI_BINARY")"
HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")"
echo "App binary architectures: $APP_ARCHS"
echo "CLI binary architectures: $CLI_ARCHS"
echo "Ghostty helper architectures: $HELPER_ARCHS"
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
[[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]]
./scripts/verify-universal-macos-app.sh \
"build-universal/Build/Products/Release/cmux.app" \
--label "Nightly app" \
--require-sdk-prefix "26."

- name: Run CLI version memory guard regression
if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true'
Expand Down
20 changes: 4 additions & 16 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,22 +229,10 @@ jobs:
- name: Verify binary architectures
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
set -euo pipefail
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty"
APP_ARCHS="$(lipo -archs "$APP_BINARY")"
CLI_ARCHS="$(lipo -archs "$CLI_BINARY")"
HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")"
SDK_VERSION="$(otool -l "$APP_BINARY" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')"
echo "App binary architectures: $APP_ARCHS"
echo "CLI binary architectures: $CLI_ARCHS"
echo "Ghostty helper architectures: $HELPER_ARCHS"
echo "App SDK version: $SDK_VERSION"
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
[[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]]
[[ "$SDK_VERSION" == 26.* ]]
./scripts/verify-universal-macos-app.sh \
"build-universal/Build/Products/Release/cmux.app" \
--label "Release app" \
--require-sdk-prefix "26."

- name: Build remote daemon release assets and inject manifest
if: steps.guard_release_assets.outputs.skip_all != 'true'
Expand Down
10 changes: 9 additions & 1 deletion scripts/build-sign-upload.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,22 @@ cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
# --- Build app (Release, unsigned) ---
echo "Building app..."
rm -rf build/
xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5
xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Release -derivedDataPath build \
-destination 'generic/platform=macOS' \
ARCHS="arm64 x86_64" \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5
echo "Build succeeded"

HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty"
if [ ! -x "$HELPER_PATH" ]; then
echo "Ghostty theme picker helper not found at $HELPER_PATH" >&2
exit 1
fi
./scripts/verify-universal-macos-app.sh \
"$APP_PATH" \
--label "Release app" \
--require-sdk-prefix "26."

# --- Inject Sparkle keys ---
echo "Injecting Sparkle keys..."
Expand Down
152 changes: 152 additions & 0 deletions scripts/verify-universal-macos-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'EOF'
Usage: ./scripts/verify-universal-macos-app.sh <app-path> [--label <name>] [--require-sdk-prefix <prefix>]

Verifies that the app executable, bundled cmux CLI, and bundled Ghostty helper
all contain both arm64 and x86_64 Mach-O slices.

When --require-sdk-prefix is provided, also verifies that the app executable's
LC_BUILD_VERSION SDK starts with that prefix, e.g. "26.".
EOF
}

APP_PATH=""
LABEL="macOS app"
SDK_PREFIX=""
while [[ $# -gt 0 ]]; do
case "$1" in
--label)
if [[ $# -lt 2 || -z "${2:-}" ]]; then
echo "Missing value for --label" >&2
exit 1
fi
LABEL="$2"
shift 2
;;
--require-sdk-prefix)
if [[ $# -lt 2 || -z "${2:-}" ]]; then
echo "Missing value for --require-sdk-prefix" >&2
exit 1
fi
SDK_PREFIX="$2"
shift 2
;;
Comment thread
austinywang marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [[ -n "$APP_PATH" ]]; then
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
fi
APP_PATH="$1"
shift
;;
esac
done

if [[ -z "$APP_PATH" ]]; then
usage >&2
exit 1
fi

if [[ -z "$LABEL" ]]; then
echo "Missing value for --label" >&2
exit 1
fi

if [[ ! -d "$APP_PATH" ]]; then
echo "error: app bundle not found at $APP_PATH" >&2
exit 1
fi

LIPO_BIN="${CMUX_LIPO:-lipo}"
if ! command -v "$LIPO_BIN" >/dev/null 2>&1; then
echo "error: lipo is required to verify universal macOS binaries" >&2
exit 1
fi

INFO_PLIST="$APP_PATH/Contents/Info.plist"
EXECUTABLE_NAME=""
if [[ -f "$INFO_PLIST" && -x /usr/libexec/PlistBuddy ]]; then
EXECUTABLE_NAME="$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$INFO_PLIST" 2>/dev/null || true)"
fi
if [[ -z "$EXECUTABLE_NAME" ]]; then
EXECUTABLE_NAME="$(basename "$APP_PATH" .app)"
fi

APP_BINARY="$APP_PATH/Contents/MacOS/$EXECUTABLE_NAME"
CLI_BINARY="$APP_PATH/Contents/Resources/bin/cmux"
HELPER_BINARY="$APP_PATH/Contents/Resources/bin/ghostty"

verify_binary_archs() {
local name="$1"
local path="$2"
local archs

if [[ ! -x "$path" ]]; then
echo "error: $name is missing or not executable at $path" >&2
exit 1
fi

if ! archs="$("$LIPO_BIN" -archs "$path")"; then
echo "error: failed to inspect $name architectures at $path" >&2
exit 1
fi

echo "$LABEL $name architectures: $archs"
for expected_arch in arm64 x86_64; do
case " $archs " in
*" $expected_arch "*)
;;
*)
echo "error: $name at $path is missing $expected_arch slice" >&2
exit 1
;;
esac
done
}

verify_binary_archs "app binary" "$APP_BINARY"
verify_binary_archs "CLI binary" "$CLI_BINARY"
verify_binary_archs "Ghostty helper" "$HELPER_BINARY"

if [[ -n "$SDK_PREFIX" ]]; then
OTOOL_BIN="${CMUX_OTOOL:-otool}"
if ! command -v "$OTOOL_BIN" >/dev/null 2>&1; then
echo "error: otool is required to verify the macOS SDK version" >&2
exit 1
fi

if ! SDK_VERSION="$(
"$OTOOL_BIN" -l "$APP_BINARY" \
| awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }'
)"; then
echo "error: failed to inspect app SDK version at $APP_BINARY" >&2
exit 1
fi
if [[ -z "$SDK_VERSION" ]]; then
echo "error: failed to inspect app SDK version at $APP_BINARY" >&2
exit 1
fi

echo "$LABEL app SDK version: $SDK_VERSION"
case "$SDK_VERSION" in
"$SDK_PREFIX"*)
;;
*)
echo "error: app binary at $APP_BINARY was built with SDK $SDK_VERSION, expected prefix $SDK_PREFIX" >&2
exit 1
;;
esac
fi
19 changes: 12 additions & 7 deletions tests/test_ci_release_sdk_lane.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CI_FILE="$ROOT_DIR/.github/workflows/ci.yml"
RELEASE_FILE="$ROOT_DIR/.github/workflows/release.yml"
BUILD_SIGN_UPLOAD_FILE="$ROOT_DIR/scripts/build-sign-upload.sh"

# 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 builds the macOS 26 SDK app with its own inline helper-build model
# (PR #5077). This lane guards the release/CI artifact-download model added by
# this change; nightly's verifier call is covered by the universal verifier test.

job_section() {
local file="$1" job="$2"
Expand Down Expand Up @@ -69,10 +69,15 @@ for workflow in "$CI_FILE" "$RELEASE_FILE"; do
exit 1
fi

if ! grep -Fq '[[ "$SDK_VERSION" == 26.* ]]' "$workflow"; then
echo "FAIL: $(basename "$workflow") must verify the app binary was built with a macOS 26 SDK" >&2
if ! grep -Fq -- '--require-sdk-prefix "26."' "$workflow"; then
echo "FAIL: $(basename "$workflow") must verify the app binary was built with a macOS 26 SDK through the universal app verifier" >&2
exit 1
fi
done

echo "PASS: release and CI app builds use macOS 26 SDK with a macOS 15-built Ghostty CLI helper"
if ! grep -Fq -- '--require-sdk-prefix "26."' "$BUILD_SIGN_UPLOAD_FILE"; then
echo "FAIL: build-sign-upload.sh must verify the app binary was built with a macOS 26 SDK through the universal app verifier" >&2
exit 1
fi

echo "PASS: release, CI, and manual app builds use macOS 26 SDK with a macOS 15-built Ghostty CLI helper"
30 changes: 24 additions & 6 deletions tests/test_ci_self_hosted_guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ CI_FILE="$ROOT_DIR/.github/workflows/ci.yml"
GHOSTTYKIT_FILE="$ROOT_DIR/.github/workflows/build-ghosttykit.yml"
COMPAT_FILE="$ROOT_DIR/.github/workflows/ci-macos-compat.yml"
E2E_FILE="$ROOT_DIR/.github/workflows/test-e2e.yml"
UNIVERSAL_VERIFIER="$ROOT_DIR/scripts/verify-universal-macos-app.sh"

job_section() {
local file="$1" job="$2"
awk -v job="$job" '
$0 ~ "^ "job":" { in_job=1; next }
in_job && /^ [^[:space:]#][^:]*:[[:space:]]*(#.*)?$/ { exit }
in_job { print }
' "$file"
}

check_macos_runner() {
local file="$1" job="$2"
Expand Down Expand Up @@ -105,18 +115,26 @@ check_xcode_selection() {
}

check_release_build_signal() {
if ! grep -Fq 'lipo "$APP_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then
echo "FAIL: release-build must verify the Release app binary stays universal"
local section
section="$(job_section "$CI_FILE" "release-build")"

if ! grep -Eq '^[[:space:]]*\./scripts/verify-universal-macos-app\.sh([[:space:]\\]|$)' <<< "$section"; then
echo "FAIL: release-build must verify the Release artifact through the universal app verifier"
exit 1
fi

if ! grep -Fq 'verify_binary_archs "app binary" "$APP_BINARY"' "$UNIVERSAL_VERIFIER"; then
echo "FAIL: universal app verifier must check the Release app binary"
exit 1
fi

if ! grep -Fq 'lipo "$CLI_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then
echo "FAIL: release-build must verify the bundled CLI stays universal"
if ! grep -Fq 'verify_binary_archs "CLI binary" "$CLI_BINARY"' "$UNIVERSAL_VERIFIER"; then
echo "FAIL: universal app verifier must check the bundled CLI"
exit 1
fi

if ! grep -Fq 'lipo "$HELPER_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then
echo "FAIL: release-build must verify the bundled Ghostty helper stays universal"
if ! grep -Fq 'verify_binary_archs "Ghostty helper" "$HELPER_BINARY"' "$UNIVERSAL_VERIFIER"; then
echo "FAIL: universal app verifier must check the bundled Ghostty helper"
exit 1
fi

Expand Down
Loading
Loading