cmux/.github/workflows/ci.yml
austinpower1258 b5fb304af0 Handle headless CI runners in browser find focus UI test
On WarpBuild runners without a GUI session, XCUIApplication.launch()
blocks ~60s then fails with "Failed to activate application (current
state: Running Background)". Wrap launch() in XCTExpectFailure so the
test can continue — keyboard and element APIs work via accessibility
even when the app is in .runningBackground.

Increase test execution time allowance from 120s to 180s to account
for the 60s activation timeout on headless runners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 04:46:18 -07:00

702 lines
28 KiB
YAML

name: CI
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
workflow-guard-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Validate WarpBuild runner guards
run: ./tests/test_ci_self_hosted_guard.sh
- name: Validate create-dmg version pinning
run: ./tests/test_ci_create_dmg_pinned.sh
- name: Validate unit-test SwiftPM retry guard
run: ./tests/test_ci_unit_test_spm_retry.sh
- name: Validate cmux scheme test configuration
run: ./tests/test_ci_scheme_testaction_debug.sh
- name: Validate GhosttyKit checksum verification
run: ./tests/test_ci_ghosttykit_checksum_verification.sh
- name: Validate release asset guard
run: node scripts/release_asset_guard.test.js
- name: Validate current GhosttyKit checksum pin
run: ./tests/test_ci_ghosttykit_checksum_present.sh
remote-daemon-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: daemon/remote/go.mod
- name: Run remote daemon tests
working-directory: daemon/remote
run: go test ./...
- name: Validate remote daemon release assets
run: ./tests/test_remote_daemon_release_assets.sh
web-typecheck:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Typecheck
run: bun tsc --noEmit
tests:
# Never run WarpBuild jobs for fork pull requests (avoid billing on external PRs).
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: warp-macos-15-arm64-6x
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
- name: Cache GhosttyKit.xcframework
id: cache-ghosttykit
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty') }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit.outputs.cache-hit != 'true'
run: |
./scripts/download-prebuilt-ghosttykit.sh
- name: Install zig
run: |
ZIG_REQUIRED="0.15.2"
if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then
echo "zig ${ZIG_REQUIRED} already installed"
else
echo "Installing zig ${ZIG_REQUIRED} from tarball"
curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz
tar xf /tmp/zig.tar.xz -C /tmp
sudo mkdir -p /usr/local/bin /usr/local/lib
sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig
sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig
export PATH="/usr/local/bin:$PATH"
zig version
fi
- name: Cache DerivedData
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
key: deriveddata-tests-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }}
restore-keys: |
deriveddata-tests-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-
deriveddata-tests-
- name: Cache Swift packages
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: .ci-source-packages
key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-
- name: Resolve Swift packages
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
mkdir -p "$SOURCE_PACKAGES_DIR"
for attempt in 1 2 3; do
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-resolvePackageDependencies; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to resolve Swift packages after 3 attempts" >&2
exit 1
fi
echo "Package resolution failed on attempt $attempt, retrying..."
sleep $((attempt * 5))
done
- name: Run unit tests
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
run_unit_tests() {
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
-skip-testing:cmuxTests/AppDelegateShortcutRoutingTests/testCmdWClosesWindowWhenClosingLastSurfaceInLastWorkspace \
test 2>&1
}
# Stream output via tee so CI logs are visible in real time, while still
# capturing for post-run analysis of expected vs unexpected failures.
set +e
run_unit_tests | tee /tmp/test-output.txt
EXIT_CODE=${PIPESTATUS[0]}
OUTPUT=$(cat /tmp/test-output.txt)
set -e
# SwiftPM binary artifact resolution can occasionally fail on ephemeral
# runners with "Could not resolve package dependencies". Retry once after
# clearing SwiftPM/DerivedData caches to recover from transient corruption.
if [ "$EXIT_CODE" -ne 0 ] && echo "$OUTPUT" | grep -q "Could not resolve package dependencies"; then
echo "SwiftPM package resolution failed, clearing caches and retrying once"
rm -rf ~/Library/Caches/org.swift.swiftpm
mkdir -p ~/Library/Caches/org.swift.swiftpm
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
set +e
run_unit_tests | tee /tmp/test-output.txt
EXIT_CODE=${PIPESTATUS[0]}
OUTPUT=$(cat /tmp/test-output.txt)
set -e
fi
if [ "$EXIT_CODE" -ne 0 ]; then
SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1)
if echo "$SUMMARY" | grep -q "(0 unexpected)"; then
echo "All failures are expected, treating as pass"
else
echo "Unexpected test failures detected"
exit 1
fi
fi
- name: Run bundled Ghostty theme picker helper regression
run: |
set -euo pipefail
CMUX_SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" \
./tests/test_bundled_ghostty_theme_picker_helper.sh
- name: Run CLI version memory guard regression
run: |
set -euo pipefail
CLI_BIN="$(
find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -exec stat -f '%m %N' {} \; \
| sort -nr \
| head -1 \
| cut -d' ' -f2-
)"
if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then
echo "cmux CLI binary not found in DerivedData" >&2
exit 1
fi
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py
tests-build-and-lag:
# Build the full cmux scheme and run the lag regression on WarpBuild.
# Keep lag validation separate from UI regressions so functional UI failures
# and performance regressions stay isolated. Broader interactive UI suites
# still run via test-e2e.yml on GitHub-hosted runners.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: warp-macos-15-arm64-6x
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
- name: Cache GhosttyKit.xcframework
id: cache-ghosttykit-lag
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty') }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true'
run: |
./scripts/download-prebuilt-ghosttykit.sh
- name: Install zig
run: |
ZIG_REQUIRED="0.15.2"
if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then
echo "zig ${ZIG_REQUIRED} already installed"
else
echo "Installing zig ${ZIG_REQUIRED} from tarball"
curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz
tar xf /tmp/zig.tar.xz -C /tmp
sudo mkdir -p /usr/local/bin /usr/local/lib
sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig
sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig
export PATH="/usr/local/bin:$PATH"
zig version
fi
- name: Cache DerivedData
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
key: deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }}
restore-keys: |
deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-
deriveddata-build-
- name: Cache Swift packages
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: .ci-source-packages
key: spm-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-build-
- name: Resolve Swift packages
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
mkdir -p "$SOURCE_PACKAGES_DIR"
for attempt in 1 2 3; do
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-resolvePackageDependencies; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to resolve Swift packages after 3 attempts" >&2
exit 1
fi
echo "Package resolution failed on attempt $attempt, retrying..."
sleep $((attempt * 5))
done
- name: Build app
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" build
- name: Create virtual display
run: |
set -euo pipefail
clang -framework Foundation -framework CoreGraphics \
-o /tmp/create-virtual-display scripts/create-virtual-display.m
/tmp/create-virtual-display &
VDISPLAY_PID=$!
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
sleep 3
kill -0 "$VDISPLAY_PID"
- name: Run workspace churn typing-lag regression
run: |
set -euo pipefail
APP="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux DEV.app" -print -quit)"
if [ -z "${APP:-}" ] || [ ! -d "$APP" ]; then
echo "cmux DEV.app not found in DerivedData" >&2
exit 1
fi
TAG="ci-lag"
SOCK="/tmp/cmux-debug-${TAG}.sock"
BUNDLE_ID="$(
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist" 2>/dev/null \
|| echo 'com.cmuxterm.app.debug'
)"
pkill -x "cmux DEV" || true
rm -f "$SOCK" "/tmp/cmux-${TAG}.sock" || true
defaults write "$BUNDLE_ID" socketControlMode -string full >/dev/null 2>&1 || true
CMUX_TAG="$TAG" CMUX_SOCKET_PATH="$SOCK" CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/tmp/cmux-ci-lag.log 2>&1 &
APP_PID=$!
trap 'kill "$APP_PID" >/dev/null 2>&1 || true' EXIT
for _ in {1..240}; do
[ -S "$SOCK" ] && break
sleep 0.25
done
[ -S "$SOCK" ] || { echo "Socket not ready at $SOCK" >&2; exit 1; }
CMUX_SOCKET_PATH="$SOCK" \
CMUX_LAG_MAX_P95_RATIO=1.70 \
CMUX_LAG_MAX_AVG_RATIO=1.70 \
CMUX_LAG_MIN_BASELINE_P95_MS_FOR_RATIO=6.0 \
CMUX_LAG_MIN_BASELINE_AVG_MS_FOR_RATIO=4.0 \
CMUX_LAG_MAX_P95_DELTA_MS=20.0 \
CMUX_LAG_MAX_AVG_DELTA_MS=12.0 \
CMUX_LAG_MAX_CHURN_P95_MS=35 \
CMUX_LAG_KEY_EVENTS=180 \
python3 tests/test_workspace_churn_up_arrow_lag.py
- name: Cleanup virtual display
if: always()
run: |
set -euo pipefail
if [ -n "${VDISPLAY_PID:-}" ]; then
kill "$VDISPLAY_PID" >/dev/null 2>&1 || true
fi
rm -f /tmp/create-virtual-display
ui-regressions:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: warp-macos-15-arm64-6x
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
xcrun --sdk macosx --show-sdk-path
- name: Download pre-built GhosttyKit.xcframework
run: ./scripts/download-prebuilt-ghosttykit.sh
- name: Install zig
run: |
ZIG_REQUIRED="0.15.2"
if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then
echo "zig ${ZIG_REQUIRED} already installed"
else
echo "Installing zig ${ZIG_REQUIRED} from tarball"
curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz
tar xf /tmp/zig.tar.xz -C /tmp
sudo mkdir -p /usr/local/bin /usr/local/lib
sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig
sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig
export PATH="/usr/local/bin:$PATH"
zig version
fi
- name: Cache Swift packages
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: .ci-source-packages
key: spm-ui-regressions-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-ui-regressions-
- name: Resolve Swift packages
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
mkdir -p "$SOURCE_PACKAGES_DIR"
for attempt in 1 2 3; do
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-resolvePackageDependencies; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to resolve Swift packages after 3 attempts" >&2
exit 1
fi
echo "Package resolution failed on attempt $attempt, retrying..."
sleep $((attempt * 5))
done
- name: Build for testing (display resolution)
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
build-for-testing
- name: Create persistent virtual display
run: |
set -euo pipefail
HELPER_PATH="/tmp/create-virtual-display"
clang -framework Foundation -framework CoreGraphics \
-o "$HELPER_PATH" scripts/create-virtual-display.m
VDISPLAY_READY="/tmp/cmux-vdisplay-persistent.ready"
VDISPLAY_ID_PATH="/tmp/cmux-vdisplay-persistent.id"
rm -f "$VDISPLAY_READY" "$VDISPLAY_ID_PATH"
"$HELPER_PATH" \
--modes "1920x1080" \
--ready-path "$VDISPLAY_READY" \
--display-id-path "$VDISPLAY_ID_PATH" \
> /tmp/cmux-vdisplay-persistent.log 2>&1 &
echo "VDISPLAY_PERSISTENT_PID=$!" >> "$GITHUB_ENV"
echo "Waiting for persistent virtual display..."
for i in $(seq 1 24); do
if [ -f "$VDISPLAY_READY" ]; then break; fi
sleep 0.5
done
if [ ! -f "$VDISPLAY_READY" ]; then
echo "ERROR: Persistent virtual display not ready after 12s" >&2
cat /tmp/cmux-vdisplay-persistent.log 2>/dev/null || true
exit 1
fi
echo "Persistent virtual display ready: ID=$(cat "$VDISPLAY_ID_PATH")"
- name: Run display resolution churn UI regression
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
HELPER_PATH="/tmp/create-virtual-display"
TOKEN="$(uuidgen)"
DIAG_PATH="/tmp/cmux-ui-test-display-churn-${TOKEN}.json"
DISPLAY_READY="/tmp/cmux-ui-test-display-${TOKEN}.ready"
DISPLAY_ID_PATH="/tmp/cmux-ui-test-display-${TOKEN}.id"
DISPLAY_START="/tmp/cmux-ui-test-display-${TOKEN}.start"
DISPLAY_DONE="/tmp/cmux-ui-test-display-${TOKEN}.done"
HELPER_LOG="/tmp/cmux-ui-test-display-${TOKEN}-helper.log"
cleanup() {
pkill -x "cmux DEV" 2>/dev/null || true
rm -f "$DIAG_PATH" "$DISPLAY_READY" "$DISPLAY_ID_PATH" "$DISPLAY_START" "$DISPLAY_DONE" "$HELPER_LOG"
rm -f /tmp/cmux-ui-test-prelaunch.json /tmp/cmux-ui-test-display-harness.json
}
trap cleanup EXIT
# Build display helper
clang -framework Foundation -framework CoreGraphics \
-o "$HELPER_PATH" scripts/create-virtual-display.m
# Find the app binary
APP_BINARY=$(find ~/Library/Developer/Xcode/DerivedData -path "*/Build/Products/Debug/cmux DEV.app/Contents/MacOS/cmux DEV" -print -quit 2>/dev/null || true)
if [ -z "$APP_BINARY" ]; then
echo "ERROR: App binary not found in DerivedData" >&2
exit 1
fi
echo "App binary: $APP_BINARY"
for attempt in 1 2; do
cleanup 2>/dev/null || true
# Launch display helper from shell (non-sandboxed).
# Use --start-delay-ms instead of --start-path because the XCTest
# runner is sandboxed and can't write to /tmp/ for the start signal.
# 10s delay gives the test time to capture baseline render stats.
"$HELPER_PATH" \
--modes "1920x1080,1728x1117,1600x900,1440x810" \
--ready-path "$DISPLAY_READY" \
--display-id-path "$DISPLAY_ID_PATH" \
--done-path "$DISPLAY_DONE" \
--iterations 40 \
--interval-ms 40 \
--start-delay-ms 10000 \
> "$HELPER_LOG" 2>&1 &
HELPER_PID=$!
# Wait for display ready
echo "Waiting for virtual display..."
for i in $(seq 1 24); do
if [ -f "$DISPLAY_READY" ]; then break; fi
sleep 0.5
done
if [ ! -f "$DISPLAY_READY" ]; then
echo "ERROR: Virtual display not ready after 12s" >&2
cat "$HELPER_LOG" 2>/dev/null || true
continue
fi
DISPLAY_ID=$(cat "$DISPLAY_ID_PATH")
echo "Virtual display ready: ID=$DISPLAY_ID"
# Launch app from shell (non-sandboxed, outside XCTest sandbox)
CMUX_UI_TEST_MODE=1 \
CMUX_UI_TEST_DIAGNOSTICS_PATH="$DIAG_PATH" \
CMUX_UI_TEST_DISPLAY_RENDER_STATS=1 \
CMUX_UI_TEST_TARGET_DISPLAY_ID="$DISPLAY_ID" \
CMUX_TAG="ui-tests-display-resolution" \
"$APP_BINARY" > /tmp/cmux-ui-test-app.log 2>&1 &
APP_PID=$!
echo "App launched: PID=$APP_PID"
# Wait for app diagnostics
echo "Waiting for app diagnostics..."
APP_READY=false
for i in $(seq 1 30); do
if [ -f "$DIAG_PATH" ]; then
if python3 -c "import json; d=json.load(open('$DIAG_PATH')); assert d.get('pid')" 2>/dev/null; then
APP_READY=true
break
fi
fi
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "ERROR: App crashed during startup"
cat /tmp/cmux-ui-test-app.log 2>/dev/null | tail -30 || true
break
fi
sleep 0.5
done
if [ "$APP_READY" != "true" ]; then
echo "Attempt $attempt: App not ready after 15s"
pkill -x "cmux DEV" 2>/dev/null || true
kill "$HELPER_PID" 2>/dev/null || true
if [ "$attempt" -eq 2 ]; then
echo "Display resolution UI regression failed after 2 attempts" >&2
echo "--- App log ---"
cat /tmp/cmux-ui-test-app.log 2>/dev/null | tail -50 || true
echo "--- Helper log ---"
cat "$HELPER_LOG" 2>/dev/null | tail -20 || true
echo "--- Diagnostics ---"
cat "$DIAG_PATH" 2>/dev/null || echo "(not found)"
exit 1
fi
sleep 3
continue
fi
echo "App started. Diagnostics:"
cat "$DIAG_PATH"
# Wait for render stats (terminal surface initialization)
echo "Waiting for render stats..."
RENDER_READY=false
for i in $(seq 1 40); do
if python3 -c "import json; d=json.load(open('$DIAG_PATH')); assert d.get('renderStatsAvailable') == '1'" 2>/dev/null; then
RENDER_READY=true
echo "Render stats available after $((i / 2))s"
break
fi
sleep 0.5
done
if [ "$RENDER_READY" != "true" ]; then
echo "WARNING: Render stats not available after 20s. Diagnostics:"
cat "$DIAG_PATH" 2>/dev/null || true
echo "--- App log ---"
cat /tmp/cmux-ui-test-app.log 2>/dev/null | tail -30 || true
fi
# Write manifests so test can find the pre-launched state
MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json"
cat >"$MANIFEST_PATH" <<MANIFEST_EOF
{"readyPath":"$DISPLAY_READY","displayIDPath":"$DISPLAY_ID_PATH","donePath":"$DISPLAY_DONE","logPath":"$HELPER_LOG"}
MANIFEST_EOF
PRELAUNCH_PATH="/tmp/cmux-ui-test-prelaunch.json"
cat >"$PRELAUNCH_PATH" <<PRELAUNCH_EOF
{"diagnosticsPath":"$DIAG_PATH"}
PRELAUNCH_EOF
# Run test — app is already launched from shell
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
-only-testing:cmuxUITests/DisplayResolutionRegressionUITests \
test-without-building; then
exit 0
fi
pkill -x "cmux DEV" 2>/dev/null || true
kill "$HELPER_PID" 2>/dev/null || true
if [ "$attempt" -eq 2 ]; then
echo "Display resolution UI regression failed after 2 attempts" >&2
exit 1
fi
echo "Attempt $attempt failed, retrying..."
sleep 3
done
- name: Run browser find focus UI regression
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
-maximum-test-execution-time-allowance 180 \
-only-testing:cmuxUITests/BrowserPaneNavigationKeybindUITests/testCmdFFocusesBrowserFindFieldAfterCmdDCmdLNavigation \
test-without-building
- name: Cleanup persistent virtual display
if: always()
run: |
if [ -n "${VDISPLAY_PERSISTENT_PID:-}" ]; then
kill "$VDISPLAY_PERSISTENT_PID" >/dev/null 2>&1 || true
fi
rm -f /tmp/cmux-vdisplay-persistent.ready /tmp/cmux-vdisplay-persistent.id /tmp/cmux-vdisplay-persistent.log