Add terminal right-click paste mode with long-press context menu + delay setting #6606
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | |
| 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 Zig packages | |
| uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 | |
| with: | |
| path: ~/.cache/zig | |
| key: zig-packages-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }} | |
| restore-keys: zig-packages- | |
| - 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. | |
| 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 Zig packages | |
| uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 | |
| with: | |
| path: ~/.cache/zig | |
| key: zig-packages-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }} | |
| restore-keys: zig-packages- | |
| - 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: | |
| 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 Zig packages | |
| uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 | |
| with: | |
| path: ~/.cache/zig | |
| key: zig-packages-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }} | |
| restore-keys: zig-packages- | |
| - 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 |