diff --git a/.github/actions/run-display-resolution-regression/action.yml b/.github/actions/run-display-resolution-regression/action.yml new file mode 100644 index 000000000000..c3e51c127d81 --- /dev/null +++ b/.github/actions/run-display-resolution-regression/action.yml @@ -0,0 +1,121 @@ +name: Run display resolution regression attempt +description: Run the display resolution churn XCUITest on WarpBuild. +outputs: + test_started: + description: Whether the display regression step started. + value: ${{ steps.run-display-regression.outputs.test_started }} + completed: + description: Whether the display regression reached a deterministic pass/fail outcome. + value: ${{ steps.run-display-regression.outputs.completed }} + passed: + description: Whether the display regression passed. + value: ${{ steps.mark-passed.outputs.passed }} +runs: + using: composite + steps: + - name: Select Xcode + shell: bash + 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: Capture GhosttyKit cache key + id: ghosttykit-cache-key + shell: bash + run: echo "ghostty_sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Cache GhosttyKit.xcframework + id: cache-ghosttykit-display + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('.gitmodules') }}-${{ steps.ghosttykit-cache-key.outputs.ghostty_sha }} + + - name: Download pre-built GhosttyKit.xcframework + if: steps.cache-ghosttykit-display.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + shell: bash + run: ./scripts/install-zig.sh + + - name: Cache Swift packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .ci-source-packages + key: spm-ui-display-resolution-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: spm-ui-display-resolution- + + - name: Resolve Swift packages + shell: bash + 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: Run display resolution churn UI regression + id: run-display-regression + shell: bash + run: | + set -euo pipefail + echo "test_started=true" >> "$GITHUB_OUTPUT" + SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" + HELPER_PATH="/tmp/create-virtual-display" + MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json" + + rm -f "$MANIFEST_PATH" + trap 'rm -f "$MANIFEST_PATH"' EXIT + + clang -framework Foundation -framework CoreGraphics \ + -o "$HELPER_PATH" scripts/create-virtual-display.m + + printf '{"helperBinaryPath":"%s"}\n' "$HELPER_PATH" >"$MANIFEST_PATH" + + set +e + xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" \ + -only-testing:cmuxUITests/DisplayResolutionRegressionUITests \ + test + EXIT_CODE=$? + set -e + echo "completed=true" >> "$GITHUB_OUTPUT" + if [ "$EXIT_CODE" -ne 0 ]; then + exit "$EXIT_CODE" + fi + + - name: Mark display regression passed + if: ${{ success() }} + id: mark-passed + shell: bash + run: echo "passed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/run-lag-regression/action.yml b/.github/actions/run-lag-regression/action.yml new file mode 100644 index 000000000000..0e95b9352055 --- /dev/null +++ b/.github/actions/run-lag-regression/action.yml @@ -0,0 +1,178 @@ +name: Run lag regression attempt +description: Build cmux on WarpBuild and run the workspace churn typing-lag regression. +outputs: + test_started: + description: Whether the lag regression step started. + value: ${{ steps.run-lag-regression.outputs.test_started }} + completed: + description: Whether the lag regression reached a deterministic pass/fail outcome. + value: ${{ steps.run-lag-regression.outputs.completed }} + passed: + description: Whether the lag regression passed. + value: ${{ steps.mark-passed.outputs.passed }} +runs: + using: composite + steps: + - name: Select Xcode + shell: bash + 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: Capture GhosttyKit cache key + id: ghosttykit-cache-key + shell: bash + run: echo "ghostty_sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Cache GhosttyKit.xcframework + id: cache-ghosttykit-lag + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('.gitmodules') }}-${{ steps.ghosttykit-cache-key.outputs.ghostty_sha }} + + - name: Download pre-built GhosttyKit.xcframework + if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + shell: bash + run: ./scripts/install-zig.sh + + - 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 + shell: bash + 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 + shell: bash + 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 + shell: bash + 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 + + - name: Run workspace churn typing-lag regression + id: run-lag-regression + shell: bash + run: | + set -euo pipefail + echo "test_started=true" >> "$GITHUB_OUTPUT" + + 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; } + + set +e + 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 + EXIT_CODE=$? + set -e + echo "completed=true" >> "$GITHUB_OUTPUT" + if [ "$EXIT_CODE" -ne 0 ]; then + exit "$EXIT_CODE" + fi + + - name: Mark lag regression passed + if: ${{ success() }} + id: mark-passed + shell: bash + run: echo "passed=true" >> "$GITHUB_OUTPUT" + + - name: Stop virtual display + if: ${{ always() }} + shell: bash + run: | + if [ -n "${VDISPLAY_PID:-}" ]; then + kill "$VDISPLAY_PID" >/dev/null 2>&1 || true + fi diff --git a/.github/actions/run-macos-compat-check/action.yml b/.github/actions/run-macos-compat-check/action.yml new file mode 100644 index 000000000000..dab97201eee8 --- /dev/null +++ b/.github/actions/run-macos-compat-check/action.yml @@ -0,0 +1,212 @@ +name: Run macOS compatibility check +description: Build cmux on a specific macOS runner, run a targeted compatibility slice, and optionally smoke test with a virtual display. +inputs: + os_label: + description: Runner label used for cache keys and diagnostics. + required: true + skip_zig: + description: Whether Zig installation and local GhosttyKit builds should be skipped. + required: true + smoke: + description: Whether to create a virtual display and run the smoke test. + required: true + compat_test_filters: + description: Newline-delimited xcodebuild -only-testing filters. + required: true +outputs: + completed: + description: Whether the compatibility attempt reached a deterministic pass/fail outcome. + value: ${{ steps.mark-completed.outputs.value }} + passed: + description: Whether the compatibility attempt passed. + value: ${{ steps.mark-passed.outputs.value }} +runs: + using: composite + steps: + - name: Select Xcode + shell: bash + run: | + set -euo pipefail + XCODE_APP="$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort | tail -n 1)" + if [ -z "$XCODE_APP" ]; then + XCODE_APP="/Applications/Xcode.app" + fi + XCODE_DIR="$XCODE_APP/Contents/Developer" + if [ ! -d "$XCODE_DIR" ]; then + echo "No Xcode found under /Applications" >&2 + exit 1 + fi + echo "Selected: $XCODE_APP" + echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV" + export DEVELOPER_DIR="$XCODE_DIR" + XCODE_VERSION_OUTPUT="$(xcodebuild -version)" + XCODE_VER="${XCODE_VERSION_OUTPUT%%$'\n'*}" + echo "XCODE_VER=$XCODE_VER" >> "$GITHUB_ENV" + echo "$XCODE_VERSION_OUTPUT" + xcrun --sdk macosx --show-sdk-path + sw_vers + + - name: Capture GhosttyKit cache key + id: ghosttykit-cache-key + shell: bash + run: echo "ghostty_sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Cache GhosttyKit.xcframework + id: cache-ghosttykit + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('.gitmodules') }}-${{ steps.ghosttykit-cache-key.outputs.ghostty_sha }} + + - name: Download pre-built GhosttyKit.xcframework + if: steps.cache-ghosttykit.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + if: ${{ inputs.skip_zig != 'true' }} + shell: bash + run: ./scripts/install-zig.sh + + - name: Cache DerivedData + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* + key: deriveddata-${{ inputs.os_label }}-${{ env.XCODE_VER }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }} + restore-keys: | + deriveddata-${{ inputs.os_label }}-${{ env.XCODE_VER }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- + deriveddata-${{ inputs.os_label }}-${{ env.XCODE_VER }}- + + - name: Cache Swift packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .ci-source-packages + key: spm-${{ inputs.os_label }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: spm-${{ inputs.os_label }}- + + - name: Resolve Swift packages + shell: bash + 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: Build app + shell: bash + env: + CMUX_SKIP_ZIG_BUILD: ${{ inputs.skip_zig == 'true' && '1' || '0' }} + 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: Run compatibility unit test slice + shell: bash + env: + CMUX_SKIP_ZIG_BUILD: ${{ inputs.skip_zig == 'true' && '1' || '0' }} + run: | + set -euo pipefail + SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" + TEST_FILTERS=() + while IFS= read -r filter; do + if [ -z "$filter" ]; then + continue + fi + TEST_FILTERS+=("$filter") + done <<'EOF' + ${{ inputs.compat_test_filters }} + EOF + + if [ "${#TEST_FILTERS[@]}" -eq 0 ]; then + echo "No compatibility test classes selected" >&2 + exit 1 + fi + + echo "Running ${#TEST_FILTERS[@]} compatibility test classes on ${{ inputs.os_label }}" + + run_compat_tests() { + xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" \ + "${TEST_FILTERS[@]}" \ + test 2>&1 + } + + set +e + run_compat_tests | tee /tmp/compat-test-output.txt + EXIT_CODE=${PIPESTATUS[0]} + OUTPUT=$(cat /tmp/compat-test-output.txt) + set -e + + 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 "Compatibility test slice only hit expected failures, treating as pass" + else + echo "Compatibility test slice failed unexpectedly" + exit 1 + fi + fi + + - name: Create virtual display + if: ${{ inputs.smoke == 'true' }} + shell: bash + run: | + set -euo pipefail + echo "=== Display before ===" + system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)" + echo "" + 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 + echo "=== Display after ===" + system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)" + + - name: Smoke test + if: ${{ inputs.smoke == 'true' }} + shell: bash + run: | + set -euo pipefail + chmod +x scripts/smoke-test-ci.sh + scripts/smoke-test-ci.sh + + - name: Cleanup virtual display + if: ${{ always() && inputs.smoke == 'true' }} + shell: bash + run: | + if [ -n "${VDISPLAY_PID:-}" ]; then + kill "$VDISPLAY_PID" >/dev/null 2>&1 || true + fi + + - name: Mark compatibility attempt completed + if: ${{ always() && !cancelled() }} + id: mark-completed + shell: bash + run: echo "value=true" >> "$GITHUB_OUTPUT" + + - name: Mark compatibility attempt passed + if: ${{ success() }} + id: mark-passed + shell: bash + run: echo "value=true" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/run-unit-test-shard/action.yml b/.github/actions/run-unit-test-shard/action.yml new file mode 100644 index 000000000000..d816a56a0ae4 --- /dev/null +++ b/.github/actions/run-unit-test-shard/action.yml @@ -0,0 +1,175 @@ +name: Run unit test shard +description: Run one shard of the cmux-unit test suite on WarpBuild. +inputs: + shard_index: + description: Zero-based shard index. + required: true + shard_count: + description: Total number of shards. + required: true + skip_zig: + description: Skip zig install and set CMUX_SKIP_ZIG_BUILD. + required: false + default: "0" +outputs: + test_started: + description: Whether the shard reached xcodebuild test execution. + value: ${{ steps.test-start.outputs.value }} + completed: + description: Whether the shard reached a deterministic pass/fail outcome. + value: ${{ steps.run-tests.outputs.completed }} + passed: + description: Whether the shard passed after expected-failure handling. + value: ${{ steps.run-tests.outputs.passed }} +runs: + using: composite + steps: + - name: Select Xcode + shell: bash + 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: Capture GhosttyKit cache key + id: ghosttykit-cache-key + shell: bash + run: echo "ghostty_sha=$(git -C ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Cache GhosttyKit.xcframework + id: cache-ghosttykit + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('.gitmodules') }}-${{ steps.ghosttykit-cache-key.outputs.ghostty_sha }} + + - name: Download pre-built GhosttyKit.xcframework + if: steps.cache-ghosttykit.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + if: inputs.skip_zig != '1' + shell: bash + run: ./scripts/install-zig.sh + + - 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 + shell: bash + 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: Mark test start + id: test-start + shell: bash + run: echo "value=true" >> "$GITHUB_OUTPUT" + + - name: Run unit test shard + id: run-tests + shell: bash + env: + CMUX_SKIP_ZIG_BUILD: ${{ inputs.skip_zig }} + SHARD_INDEX: ${{ inputs.shard_index }} + SHARD_COUNT: ${{ inputs.shard_count }} + run: | + set -euo pipefail + SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" + TEST_FILTERS=() + while IFS= read -r filter; do + TEST_FILTERS+=("$filter") + done < <( + python3 scripts/unit_test_shard_args.py \ + --shard-index "$SHARD_INDEX" \ + --shard-count "$SHARD_COUNT" + ) + + if [ "${#TEST_FILTERS[@]}" -eq 0 ]; then + echo "No test classes assigned to shard $SHARD_INDEX of $SHARD_COUNT" >&2 + exit 1 + fi + + echo "Running shard $((SHARD_INDEX + 1)) of $SHARD_COUNT with ${#TEST_FILTERS[@]} test classes" + + run_unit_tests() { + xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" \ + "${TEST_FILTERS[@]}" \ + test 2>&1 + } + + set +e + run_unit_tests | tee /tmp/test-output.txt + EXIT_CODE=${PIPESTATUS[0]} + OUTPUT=$(cat /tmp/test-output.txt) + set -e + + 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 + + echo "completed=true" >> "$GITHUB_OUTPUT" + + 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 + + echo "passed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-ghosttykit.yml b/.github/workflows/build-ghosttykit.yml index 688fcf5376eb..e33c4dbb74ff 100644 --- a/.github/workflows/build-ghosttykit.yml +++ b/.github/workflows/build-ghosttykit.yml @@ -11,11 +11,16 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - build-ghosttykit: + build-ghosttykit-attempt-1: # 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 + continue-on-error: true runs-on: warp-macos-15-arm64-6x timeout-minutes: 20 + outputs: + build_started: ${{ steps.mark-build-start.outputs.value }} + completed: ${{ steps.mark-build-complete.outputs.value }} + passed: ${{ steps.mark-build-pass.outputs.value }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -62,23 +67,116 @@ jobs: export DEVELOPER_DIR="$XCODE_DIR" xcodebuild -version + - name: Mark GhosttyKit build start + id: mark-build-start + if: steps.check-release.outputs.exists == 'false' + run: echo "value=true" >> "$GITHUB_OUTPUT" + - name: Build GhosttyKit.xcframework if: steps.check-release.outputs.exists == 'false' run: | set -euo pipefail - 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" + ./scripts/install-zig.sh + cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast + + - name: Package xcframework + if: steps.check-release.outputs.exists == 'false' + run: | + set -euo pipefail + rm -rf GhosttyKit.xcframework + cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework + tar czf GhosttyKit.xcframework.tar.gz GhosttyKit.xcframework + + - name: Upload xcframework release + if: steps.check-release.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GHOSTTY_RELEASE_TOKEN }} + run: | + set -euo pipefail + TAG="xcframework-${{ steps.ghostty-sha.outputs.sha }}" + gh release create "$TAG" \ + --repo manaflow-ai/ghostty \ + --title "GhosttyKit xcframework (${{ steps.ghostty-sha.outputs.sha }})" \ + --notes "Pre-built GhosttyKit.xcframework for commit ${{ steps.ghostty-sha.outputs.sha }}" \ + GhosttyKit.xcframework.tar.gz + echo "Published release $TAG" + + - name: Mark GhosttyKit build completed + if: ${{ always() && !cancelled() }} + id: mark-build-complete + run: echo "value=true" >> "$GITHUB_OUTPUT" + + - name: Mark GhosttyKit build success + id: mark-build-pass + run: echo "value=true" >> "$GITHUB_OUTPUT" + + build-ghosttykit-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.build-ghosttykit-attempt-1.outputs.completed != 'true' && needs.build-ghosttykit-attempt-1.outputs.passed != 'true' }} + needs: + - build-ghosttykit-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 20 + outputs: + build_started: ${{ steps.mark-build-start.outputs.value }} + completed: ${{ steps.mark-build-complete.outputs.value }} + passed: ${{ steps.mark-build-pass.outputs.value }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + + - name: Get ghostty SHA + id: ghostty-sha + run: | + SHA=$(git -C ghostty rev-parse HEAD) + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "Ghostty SHA: $SHA" + + - name: Check if xcframework release already exists + id: check-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="xcframework-${{ steps.ghostty-sha.outputs.sha }}" + if gh release view "$TAG" --repo manaflow-ai/ghostty >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Release $TAG already exists, skipping build" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "Release $TAG not found, will build" + fi + + - name: Select Xcode + if: steps.check-release.outputs.exists == 'false' + run: | + set -euo pipefail + if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then + XCODE_DIR="/Applications/Xcode.app/Contents/Developer" 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 + XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -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: Mark GhosttyKit build start + id: mark-build-start + if: steps.check-release.outputs.exists == 'false' + run: echo "value=true" >> "$GITHUB_OUTPUT" + + - name: Build GhosttyKit.xcframework + if: steps.check-release.outputs.exists == 'false' + run: | + set -euo pipefail + ./scripts/install-zig.sh cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast - name: Package xcframework @@ -102,3 +200,33 @@ jobs: --notes "Pre-built GhosttyKit.xcframework for commit ${{ steps.ghostty-sha.outputs.sha }}" \ GhosttyKit.xcframework.tar.gz echo "Published release $TAG" + + - name: Mark GhosttyKit build completed + if: ${{ always() && !cancelled() }} + id: mark-build-complete + run: echo "value=true" >> "$GITHUB_OUTPUT" + + - name: Mark GhosttyKit build success + id: mark-build-pass + run: echo "value=true" >> "$GITHUB_OUTPUT" + + build-ghosttykit: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - build-ghosttykit-attempt-1 + - build-ghosttykit-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify GhosttyKit build passed + run: | + scripts/verify_retry_result.sh \ + "GhosttyKit build" \ + "${{ needs.build-ghosttykit-attempt-1.result }}" \ + "${{ needs.build-ghosttykit-attempt-1.outputs.completed }}" \ + "${{ needs.build-ghosttykit-attempt-1.outputs.passed }}" \ + "${{ needs.build-ghosttykit-attempt-2.result }}" \ + "${{ needs.build-ghosttykit-attempt-2.outputs.completed }}" \ + "${{ needs.build-ghosttykit-attempt-2.outputs.passed }}" diff --git a/.github/workflows/ci-macos-compat.yml b/.github/workflows/ci-macos-compat.yml index ddbd3be0fd46..f51c0b939a98 100644 --- a/.github/workflows/ci-macos-compat.yml +++ b/.github/workflows/ci-macos-compat.yml @@ -7,193 +7,183 @@ on: pull_request: jobs: - compat-tests: - # Only run for the repo itself, not forks (GhosttyKit download needs repo access). + compat-tests-macos-15-attempt-1: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - strategy: - fail-fast: false - matrix: - include: - - os: warp-macos-15-arm64-6x - timeout: 30 - smoke: true - skip_zig: false - - os: warp-macos-26-arm64-6x - timeout: 30 - smoke: false - skip_zig: true # zig 0.15.2 MachO linker can't resolve libSystem on macOS 26 - runs-on: ${{ matrix.os }} - timeout-minutes: ${{ matrix.timeout }} + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 45 + outputs: + completed: ${{ steps.compat.outputs.completed }} + passed: ${{ steps.compat.outputs.passed }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: submodules: recursive - - name: Select Xcode - run: | - set -euo pipefail - # Pick the latest Xcode installed on the runner. GitHub-hosted macos-14 - # defaults to Xcode 15.4, but the project needs Xcode 16+ (Swift tools - # version 6.0 required by sentry-cocoa). - XCODE_APP="$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort | tail -n 1)" - if [ -z "$XCODE_APP" ]; then - XCODE_APP="/Applications/Xcode.app" - fi - XCODE_DIR="$XCODE_APP/Contents/Developer" - if [ ! -d "$XCODE_DIR" ]; then - echo "No Xcode found under /Applications" >&2 - exit 1 - fi - echo "Selected: $XCODE_APP" - echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV" - export DEVELOPER_DIR="$XCODE_DIR" - XCODE_VERSION_OUTPUT="$(xcodebuild -version)" - XCODE_VER="${XCODE_VERSION_OUTPUT%%$'\n'*}" - echo "XCODE_VER=$XCODE_VER" >> "$GITHUB_ENV" - echo "$XCODE_VERSION_OUTPUT" - xcrun --sdk macosx --show-sdk-path - sw_vers - - - name: Cache GhosttyKit.xcframework - id: cache-ghosttykit - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - name: Run macOS 15 compatibility attempt + id: compat + uses: ./.github/actions/run-macos-compat-check 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 - if: ${{ !matrix.skip_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 + os_label: warp-macos-15-arm64-6x + skip_zig: "false" + smoke: "true" + compat_test_filters: | + -only-testing:cmuxTests/AppDelegateLaunchServicesRegistrationTests + -only-testing:cmuxTests/BrowserPaneDropRoutingTests + -only-testing:cmuxTests/CJKIMEShortcutBypassTests + -only-testing:cmuxTests/NotificationMenuSnapshotBuilderTests + -only-testing:cmuxTests/SidebarSelectedWorkspaceColorTests + -only-testing:cmuxTests/TerminalDirectoryOpenTargetAvailabilityTests + -only-testing:cmuxTests/WorkspaceAutoReorderSettingsTests + -only-testing:cmuxTests/WorkspaceTerminalFocusRecoveryTests + + compat-tests-macos-15-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.compat-tests-macos-15-attempt-1.outputs.completed != 'true' && needs.compat-tests-macos-15-attempt-1.outputs.passed != 'true' }} + needs: + - compat-tests-macos-15-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 45 + outputs: + completed: ${{ steps.compat.outputs.completed }} + passed: ${{ steps.compat.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive - - name: Cache DerivedData - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - name: Run macOS 15 compatibility attempt + id: compat + uses: ./.github/actions/run-macos-compat-check + with: + os_label: warp-macos-15-arm64-6x + skip_zig: "false" + smoke: "true" + compat_test_filters: | + -only-testing:cmuxTests/AppDelegateLaunchServicesRegistrationTests + -only-testing:cmuxTests/BrowserPaneDropRoutingTests + -only-testing:cmuxTests/CJKIMEShortcutBypassTests + -only-testing:cmuxTests/NotificationMenuSnapshotBuilderTests + -only-testing:cmuxTests/SidebarSelectedWorkspaceColorTests + -only-testing:cmuxTests/TerminalDirectoryOpenTargetAvailabilityTests + -only-testing:cmuxTests/WorkspaceAutoReorderSettingsTests + -only-testing:cmuxTests/WorkspaceTerminalFocusRecoveryTests + + compat-tests-macos-26-attempt-1: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + runs-on: warp-macos-26-arm64-6x + timeout-minutes: 45 + outputs: + completed: ${{ steps.compat.outputs.completed }} + passed: ${{ steps.compat.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* - key: deriveddata-${{ matrix.os }}-${{ env.XCODE_VER }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }} - restore-keys: | - deriveddata-${{ matrix.os }}-${{ env.XCODE_VER }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- - deriveddata-${{ matrix.os }}-${{ env.XCODE_VER }}- + submodules: recursive - - name: Cache Swift packages - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - name: Run macOS 26 compatibility attempt + id: compat + uses: ./.github/actions/run-macos-compat-check + with: + os_label: warp-macos-26-arm64-6x + skip_zig: "true" + smoke: "false" + compat_test_filters: | + -only-testing:cmuxTests/BrowserPanelAddressBarFocusRequestTests + -only-testing:cmuxTests/CmuxCLIPathInstallerTests + -only-testing:cmuxTests/GhosttyConfigTests + -only-testing:cmuxTests/OmnibarRemoteSuggestionMergeTests + -only-testing:cmuxTests/SidebarWidthPolicyTests + -only-testing:cmuxTests/TerminalKeyboardCopyModeActionTests + -only-testing:cmuxTests/WorkspaceBrowserProfileSelectionTests + -only-testing:cmuxTests/ZshShellIntegrationHandoffTests + + compat-tests-macos-26-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.compat-tests-macos-26-attempt-1.outputs.completed != 'true' && needs.compat-tests-macos-26-attempt-1.outputs.passed != 'true' }} + needs: + - compat-tests-macos-26-attempt-1 + continue-on-error: true + runs-on: warp-macos-26-arm64-6x + timeout-minutes: 45 + outputs: + completed: ${{ steps.compat.outputs.completed }} + passed: ${{ steps.compat.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - path: .ci-source-packages - key: spm-${{ matrix.os }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-${{ matrix.os }}- + submodules: recursive - - name: Resolve Swift packages - run: | - set -euo pipefail - SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - mkdir -p "$SOURCE_PACKAGES_DIR" + - name: Run macOS 26 compatibility attempt + id: compat + uses: ./.github/actions/run-macos-compat-check + with: + os_label: warp-macos-26-arm64-6x + skip_zig: "true" + smoke: "false" + compat_test_filters: | + -only-testing:cmuxTests/BrowserPanelAddressBarFocusRequestTests + -only-testing:cmuxTests/CmuxCLIPathInstallerTests + -only-testing:cmuxTests/GhosttyConfigTests + -only-testing:cmuxTests/OmnibarRemoteSuggestionMergeTests + -only-testing:cmuxTests/SidebarWidthPolicyTests + -only-testing:cmuxTests/TerminalKeyboardCopyModeActionTests + -only-testing:cmuxTests/WorkspaceBrowserProfileSelectionTests + -only-testing:cmuxTests/ZshShellIntegrationHandoffTests - 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 + compat-tests: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - compat-tests-macos-15-attempt-1 + - compat-tests-macos-15-attempt-2 + - compat-tests-macos-26-attempt-1 + - compat-tests-macos-26-attempt-2 + strategy: + fail-fast: false + matrix: + include: + - os_label: warp-macos-15-arm64-6x + check_name: warp-macos-15-arm64-6x, 45, true, false, -only-testing:cmuxTests/AppDelegateLaunchServicesRegistrationTests... + - os_label: warp-macos-26-arm64-6x + check_name: warp-macos-26-arm64-6x, 45, false, true, -only-testing:cmuxTests/BrowserPanelAddressBarFocusRequestTests... + name: compat-tests (${{ matrix.check_name }}) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Run unit tests - env: - CMUX_SKIP_ZIG_BUILD: ${{ matrix.skip_zig && '1' || '0' }} + - name: Verify compatibility attempt result 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 - } - - 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. Retry once after clearing caches. - 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" + case "${{ matrix.os_label }}" in + warp-macos-15-arm64-6x) + ATTEMPT_1_RESULT="${{ needs.compat-tests-macos-15-attempt-1.result }}" + ATTEMPT_1_COMPLETED="${{ needs.compat-tests-macos-15-attempt-1.outputs.completed }}" + ATTEMPT_1_PASSED="${{ needs.compat-tests-macos-15-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.compat-tests-macos-15-attempt-2.result }}" + ATTEMPT_2_COMPLETED="${{ needs.compat-tests-macos-15-attempt-2.outputs.completed }}" + ATTEMPT_2_PASSED="${{ needs.compat-tests-macos-15-attempt-2.outputs.passed }}" + ;; + warp-macos-26-arm64-6x) + ATTEMPT_1_RESULT="${{ needs.compat-tests-macos-26-attempt-1.result }}" + ATTEMPT_1_COMPLETED="${{ needs.compat-tests-macos-26-attempt-1.outputs.completed }}" + ATTEMPT_1_PASSED="${{ needs.compat-tests-macos-26-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.compat-tests-macos-26-attempt-2.result }}" + ATTEMPT_2_COMPLETED="${{ needs.compat-tests-macos-26-attempt-2.outputs.completed }}" + ATTEMPT_2_PASSED="${{ needs.compat-tests-macos-26-attempt-2.outputs.passed }}" + ;; + *) + echo "Unknown compatibility runner ${{ matrix.os_label }}" >&2 exit 1 - fi - fi - - - name: Create virtual display - if: matrix.smoke - run: | - set -euo pipefail - echo "=== Display before ===" - system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)" - echo "" - 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 - echo "=== Display after ===" - system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)" - - - name: Build app for smoke test - if: matrix.smoke - 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 + ;; + esac - - name: Smoke test - if: matrix.smoke - run: | - set -euo pipefail - chmod +x scripts/smoke-test-ci.sh - scripts/smoke-test-ci.sh + scripts/verify_retry_result.sh \ + "Compatibility tests on ${{ matrix.os_label }}" \ + "$ATTEMPT_1_RESULT" "${ATTEMPT_1_COMPLETED:-}" "${ATTEMPT_1_PASSED:-}" \ + "$ATTEMPT_2_RESULT" "${ATTEMPT_2_COMPLETED:-}" "${ATTEMPT_2_PASSED:-}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb65e7e2c156..28beaf3d70a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,6 @@ on: - 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 @@ -26,12 +22,21 @@ jobs: - name: Validate unit-test SwiftPM retry guard run: ./tests/test_ci_unit_test_spm_retry.sh + - name: Validate unit-test shard timeout guard + run: bash ./tests/test_ci_unit_shard_timeout.sh + + - name: Validate retry output gating + run: bash ./tests/test_ci_retry_output_gating.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 macOS compat test slice + run: ./tests/test_ci_macos_compat_test_slice.sh + - name: Validate release asset guard run: node scripts/release_asset_guard.test.js @@ -74,151 +79,84 @@ jobs: - name: Typecheck run: bun tsc --noEmit - tests: + tests-shard-1-attempt-1: # 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 + continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 30 + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.mark-attempt-complete.outputs.value }} + passed: ${{ steps.mark-attempt-pass.outputs.value }} 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 + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard 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 + shard_index: "0" + shard_count: "6" + - name: Run bundled Ghostty theme picker helper regression 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 + 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 - 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 + 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 + - name: Mark unit test shard attempt completed + if: ${{ always() && !cancelled() }} + id: mark-attempt-complete + run: echo "value=true" >> "$GITHUB_OUTPUT" + - name: Mark unit test shard attempt passed + if: ${{ success() }} + id: mark-attempt-pass + run: echo "value=true" >> "$GITHUB_OUTPUT" + + tests-shard-1-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-1-attempt-1.outputs.completed != 'true' && needs.tests-shard-1-attempt-1.outputs.passed != 'true' }} + needs: + - tests-shard-1-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.mark-attempt-complete.outputs.value }} + passed: ${{ steps.mark-attempt-pass.outputs.value }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "0" + shard_count: "6" - 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 @@ -235,468 +173,521 @@ jobs: 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. + - name: Mark unit test shard attempt completed + if: ${{ always() && !cancelled() }} + id: mark-attempt-complete + run: echo "value=true" >> "$GITHUB_OUTPUT" + - name: Mark unit test shard attempt passed + if: ${{ success() }} + id: mark-attempt-pass + run: echo "value=true" >> "$GITHUB_OUTPUT" + + tests-shard-1: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-1-attempt-1 + - tests-shard-1-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify unit test shard passed + run: | + scripts/verify_retry_result.sh \ + "Unit test shard" \ + "${{ needs.tests-shard-1-attempt-1.result }}" \ + "${{ needs.tests-shard-1-attempt-1.outputs.completed }}" \ + "${{ needs.tests-shard-1-attempt-1.outputs.passed }}" \ + "${{ needs.tests-shard-1-attempt-2.result }}" \ + "${{ needs.tests-shard-1-attempt-2.outputs.completed }}" \ + "${{ needs.tests-shard-1-attempt-2.outputs.passed }}" + + tests-shard-2-attempt-1: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 20 + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: submodules: recursive - - - name: Select Xcode + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "1" + shard_count: "6" + + tests-shard-2-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-2-attempt-1.outputs.completed != 'true' && needs.tests-shard-2-attempt-1.outputs.passed != 'true' }} + needs: + - tests-shard-2-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "1" + shard_count: "6" + + tests-shard-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-2-attempt-1 + - tests-shard-2-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify unit test shard passed 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 + scripts/verify_retry_result.sh \ + "Unit test shard" \ + "${{ needs.tests-shard-2-attempt-1.result }}" \ + "${{ needs.tests-shard-2-attempt-1.outputs.completed }}" \ + "${{ needs.tests-shard-2-attempt-1.outputs.passed }}" \ + "${{ needs.tests-shard-2-attempt-2.result }}" \ + "${{ needs.tests-shard-2-attempt-2.outputs.completed }}" \ + "${{ needs.tests-shard-2-attempt-2.outputs.passed }}" + + tests-shard-3-attempt-1: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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' + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "2" + shard_count: "6" + + tests-shard-3-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-3-attempt-1.outputs.completed != 'true' && needs.tests-shard-3-attempt-1.outputs.passed != 'true' }} + needs: + - tests-shard-3-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "2" + shard_count: "6" + + tests-shard-3: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-3-attempt-1 + - tests-shard-3-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify unit test shard passed run: | - ./scripts/download-prebuilt-ghosttykit.sh - - - name: Install zig + scripts/verify_retry_result.sh \ + "Unit test shard" \ + "${{ needs.tests-shard-3-attempt-1.result }}" \ + "${{ needs.tests-shard-3-attempt-1.outputs.completed }}" \ + "${{ needs.tests-shard-3-attempt-1.outputs.passed }}" \ + "${{ needs.tests-shard-3-attempt-2.result }}" \ + "${{ needs.tests-shard-3-attempt-2.outputs.completed }}" \ + "${{ needs.tests-shard-3-attempt-2.outputs.passed }}" + + tests-shard-4-attempt-1: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "3" + shard_count: "6" + + tests-shard-4-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-4-attempt-1.outputs.completed != 'true' && needs.tests-shard-4-attempt-1.outputs.passed != 'true' }} + needs: + - tests-shard-4-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "3" + shard_count: "6" + + tests-shard-4: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-4-attempt-1 + - tests-shard-4-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify unit test shard passed 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 + scripts/verify_retry_result.sh \ + "Unit test shard" \ + "${{ needs.tests-shard-4-attempt-1.result }}" \ + "${{ needs.tests-shard-4-attempt-1.outputs.completed }}" \ + "${{ needs.tests-shard-4-attempt-1.outputs.passed }}" \ + "${{ needs.tests-shard-4-attempt-2.result }}" \ + "${{ needs.tests-shard-4-attempt-2.outputs.completed }}" \ + "${{ needs.tests-shard-4-attempt-2.outputs.passed }}" + + tests-shard-5-attempt-1: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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 + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard 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 + shard_index: "4" + shard_count: "6" + + tests-shard-5-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-5-attempt-1.outputs.completed != 'true' && needs.tests-shard-5-attempt-1.outputs.passed != 'true' }} + needs: + - tests-shard-5-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "4" + shard_count: "6" + + tests-shard-5: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-5-attempt-1 + - tests-shard-5-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify unit test shard passed 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 + scripts/verify_retry_result.sh \ + "Unit test shard" \ + "${{ needs.tests-shard-5-attempt-1.result }}" \ + "${{ needs.tests-shard-5-attempt-1.outputs.completed }}" \ + "${{ needs.tests-shard-5-attempt-1.outputs.passed }}" \ + "${{ needs.tests-shard-5-attempt-2.result }}" \ + "${{ needs.tests-shard-5-attempt-2.outputs.completed }}" \ + "${{ needs.tests-shard-5-attempt-2.outputs.passed }}" + + tests-shard-6-attempt-1: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "5" + shard_count: "6" + + tests-shard-6-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-6-attempt-1.outputs.completed != 'true' && needs.tests-shard-6-attempt-1.outputs.passed != 'true' }} + needs: + - tests-shard-6-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 10 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + completed: ${{ steps.unit-test-shard.outputs.completed }} + passed: ${{ steps.unit-test-shard.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + id: unit-test-shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "5" + shard_count: "6" + + tests-shard-6: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-6-attempt-1 + - tests-shard-6-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify unit test shard passed 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 + scripts/verify_retry_result.sh \ + "Unit test shard" \ + "${{ needs.tests-shard-6-attempt-1.result }}" \ + "${{ needs.tests-shard-6-attempt-1.outputs.completed }}" \ + "${{ needs.tests-shard-6-attempt-1.outputs.passed }}" \ + "${{ needs.tests-shard-6-attempt-2.result }}" \ + "${{ needs.tests-shard-6-attempt-2.outputs.completed }}" \ + "${{ needs.tests-shard-6-attempt-2.outputs.passed }}" - - 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 + tests: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-shard-1 + - tests-shard-2 + - tests-shard-3 + - tests-shard-4 + - tests-shard-5 + - tests-shard-6 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Verify unit test shards passed 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 + SHARD_1_RESULT="${{ needs.tests-shard-1.result }}" + SHARD_2_RESULT="${{ needs.tests-shard-2.result }}" + SHARD_3_RESULT="${{ needs.tests-shard-3.result }}" + SHARD_4_RESULT="${{ needs.tests-shard-4.result }}" + SHARD_5_RESULT="${{ needs.tests-shard-5.result }}" + SHARD_6_RESULT="${{ needs.tests-shard-6.result }}" + + if [ "$SHARD_1_RESULT" = "success" ] \ + && [ "$SHARD_2_RESULT" = "success" ] \ + && [ "$SHARD_3_RESULT" = "success" ] \ + && [ "$SHARD_4_RESULT" = "success" ] \ + && [ "$SHARD_5_RESULT" = "success" ] \ + && [ "$SHARD_6_RESULT" = "success" ]; then + echo "All cmux-unit shards passed." + exit 0 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' - )" + echo "One or more cmux-unit shards failed." >&2 + echo "tests-shard-1=$SHARD_1_RESULT" >&2 + echo "tests-shard-2=$SHARD_2_RESULT" >&2 + echo "tests-shard-3=$SHARD_3_RESULT" >&2 + echo "tests-shard-4=$SHARD_4_RESULT" >&2 + echo "tests-shard-5=$SHARD_5_RESULT" >&2 + echo "tests-shard-6=$SHARD_6_RESULT" >&2 + exit 1 - 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: + tests-build-and-lag-attempt-1: + # Build the full cmux scheme and run the lag regression on WarpBuild. + # XCUITests cannot run on WarpBuild (Virtualization.framework limitation: + # XCUIApplication stuck "Running Background", 62s activation timeout per + # test). Interactive UI tests 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 + continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 20 + outputs: + test_started: ${{ steps.lag-regression.outputs.test_started }} + completed: ${{ steps.lag-regression.outputs.completed }} + passed: ${{ steps.lag-regression.outputs.passed }} 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 + - name: Run lag regression attempt + id: lag-regression + uses: ./.github/actions/run-lag-regression + + tests-build-and-lag-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-build-and-lag-attempt-1.outputs.completed != 'true' && needs.tests-build-and-lag-attempt-1.outputs.passed != 'true' }} + needs: + - tests-build-and-lag-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 20 + outputs: + test_started: ${{ steps.lag-regression.outputs.test_started }} + completed: ${{ steps.lag-regression.outputs.completed }} + passed: ${{ steps.lag-regression.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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")" + submodules: recursive + - name: Run lag regression attempt + id: lag-regression + uses: ./.github/actions/run-lag-regression - - 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" <"$PRELAUNCH_PATH" </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 + tests-build-and-lag: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - tests-build-and-lag-attempt-1 + - tests-build-and-lag-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify lag regression result 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() + scripts/verify_retry_result.sh \ + "Lag regression" \ + "${{ needs.tests-build-and-lag-attempt-1.result }}" \ + "${{ needs.tests-build-and-lag-attempt-1.outputs.completed }}" \ + "${{ needs.tests-build-and-lag-attempt-1.outputs.passed }}" \ + "${{ needs.tests-build-and-lag-attempt-2.result }}" \ + "${{ needs.tests-build-and-lag-attempt-2.outputs.completed }}" \ + "${{ needs.tests-build-and-lag-attempt-2.outputs.passed }}" + + ui-display-resolution-regression-attempt-1: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.display-regression.outputs.test_started }} + completed: ${{ steps.display-regression.outputs.completed }} + passed: ${{ steps.display-regression.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run display regression attempt + id: display-regression + uses: ./.github/actions/run-display-resolution-regression + + ui-display-resolution-regression-attempt-2: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.ui-display-resolution-regression-attempt-1.outputs.completed != 'true' && needs.ui-display-resolution-regression-attempt-1.outputs.passed != 'true' }} + needs: + - ui-display-resolution-regression-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.display-regression.outputs.test_started }} + completed: ${{ steps.display-regression.outputs.completed }} + passed: ${{ steps.display-regression.outputs.passed }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run display regression attempt + id: display-regression + uses: ./.github/actions/run-display-resolution-regression + + ui-display-resolution-regression: + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + needs: + - ui-display-resolution-regression-attempt-1 + - ui-display-resolution-regression-attempt-2 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Verify at least one display regression attempt passed 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 + scripts/verify_retry_result.sh \ + "Display resolution regression" \ + "${{ needs.ui-display-resolution-regression-attempt-1.result }}" \ + "${{ needs.ui-display-resolution-regression-attempt-1.outputs.completed }}" \ + "${{ needs.ui-display-resolution-regression-attempt-1.outputs.passed }}" \ + "${{ needs.ui-display-resolution-regression-attempt-2.result }}" \ + "${{ needs.ui-display-resolution-regression-attempt-2.outputs.completed }}" \ + "${{ needs.ui-display-resolution-regression-attempt-2.outputs.passed }}" diff --git a/scripts/install-zig.sh b/scripts/install-zig.sh new file mode 100755 index 000000000000..e3347b06f7ca --- /dev/null +++ b/scripts/install-zig.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +ZIG_REQUIRED="${1:-0.15.2}" +ZIG_PLATFORM="${2:-aarch64-macos}" + +case "${ZIG_REQUIRED}:${ZIG_PLATFORM}" in + 0.15.2:aarch64-macos) + ZIG_URL="https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz" + ZIG_SHA256="3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b" + ZIG_DIR="/tmp/zig-aarch64-macos-0.15.2" + ;; + *) + echo "Unsupported Zig install target: ${ZIG_REQUIRED}:${ZIG_PLATFORM}" >&2 + exit 1 + ;; +esac + +if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}$"; then + ZIG_ENV_JSON="$(zig env 2>/dev/null || true)" + ZIG_LIB_DIR="$(printf '%s\n' "$ZIG_ENV_JSON" | sed -n 's/.*"lib_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" + if [ -n "$ZIG_LIB_DIR" ] && [ -d "$ZIG_LIB_DIR" ]; then + echo "zig ${ZIG_REQUIRED} already installed" + exit 0 + fi + echo "zig ${ZIG_REQUIRED} binary is present but lib_dir is missing, reinstalling" +fi + +echo "Installing zig ${ZIG_REQUIRED} from tarball" +ARCHIVE="/tmp/zig.tar.xz" +curl -fSL "$ZIG_URL" -o "$ARCHIVE" + +ACTUAL_SHA256="$(shasum -a 256 "$ARCHIVE" | awk '{print $1}')" +if [ "$ACTUAL_SHA256" != "$ZIG_SHA256" ]; then + echo "zig ${ZIG_REQUIRED} checksum mismatch" >&2 + echo "Expected: $ZIG_SHA256" >&2 + echo "Actual: $ACTUAL_SHA256" >&2 + exit 1 +fi + +rm -rf "$ZIG_DIR" +tar xf "$ARCHIVE" -C /tmp + +sudo mkdir -p /usr/local/bin /usr/local/lib +sudo rm -f /usr/local/bin/zig +sudo rm -rf /usr/local/lib/zig +sudo cp -f "$ZIG_DIR/zig" /usr/local/bin/zig +sudo cp -R "$ZIG_DIR/lib" /usr/local/lib/zig + +export PATH="/usr/local/bin:$PATH" +zig version diff --git a/scripts/unit_test_shard_args.py b/scripts/unit_test_shard_args.py new file mode 100644 index 000000000000..b0bbed93475b --- /dev/null +++ b/scripts/unit_test_shard_args.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +import argparse +import pathlib +import re +import sys + + +CLASS_PATTERN = re.compile( + r"^(?:final\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*XCTestCase\b", + re.MULTILINE, +) + + +def discover_test_classes(test_root: pathlib.Path) -> list[str]: + classes: set[str] = set() + for path in sorted(test_root.glob("*.swift")): + contents = path.read_text(encoding="utf-8") + classes.update(CLASS_PATTERN.findall(contents)) + return sorted(classes) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Emit xcodebuild -only-testing arguments for one cmux-unit shard." + ) + parser.add_argument("--shard-index", type=int, required=True) + parser.add_argument("--shard-count", type=int, required=True) + parser.add_argument("--target", default="cmuxTests") + args = parser.parse_args() + + if args.shard_count <= 0: + parser.error("--shard-count must be positive") + if args.shard_index < 0 or args.shard_index >= args.shard_count: + parser.error("--shard-index must be within [0, shard-count)") + + repo_root = pathlib.Path(__file__).resolve().parents[1] + test_root = repo_root / "cmuxTests" + test_classes = discover_test_classes(test_root) + if not test_classes: + print("No XCTestCase subclasses found under cmuxTests", file=sys.stderr) + return 1 + + shard_classes = [ + class_name + for idx, class_name in enumerate(test_classes) + if idx % args.shard_count == args.shard_index + ] + if not shard_classes: + print( + f"Shard {args.shard_index} of {args.shard_count} has no assigned test classes", + file=sys.stderr, + ) + return 1 + + for class_name in shard_classes: + print(f"-only-testing:{args.target}/{class_name}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_retry_result.sh b/scripts/verify_retry_result.sh new file mode 100755 index 000000000000..4926a3c6e215 --- /dev/null +++ b/scripts/verify_retry_result.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 7 ]; then + echo "usage: $0