From 9d85e63526abe79c07a5f0e69a1b584bd4147079 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 06:19:48 -0700 Subject: [PATCH 01/23] Retry Warp display regression once in CI --- .github/workflows/ci.yml | 144 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78dea4576c16..c348ae76efd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -386,10 +386,122 @@ jobs: CMUX_LAG_KEY_EVENTS=180 \ python3 tests/test_workspace_churn_up_arrow_lag.py - ui-display-resolution-regression: + 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: + passed: ${{ steps.mark-passed.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 + 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 + 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 + run: | + set -euo pipefail + 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 + + cat >"$MANIFEST_PATH" <> "$GITHUB_OUTPUT" + + 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.passed != 'true' }} + needs: + - ui-display-resolution-regression-attempt-1 + continue-on-error: true runs-on: warp-macos-15-arm64-6x timeout-minutes: 25 + outputs: + passed: ${{ steps.mark-passed.outputs.passed }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -484,3 +596,33 @@ jobs: -destination "platform=macOS" \ -only-testing:cmuxUITests/DisplayResolutionRegressionUITests \ test + + - name: Mark display regression passed + if: ${{ success() }} + id: mark-passed + run: echo "passed=true" >> "$GITHUB_OUTPUT" + + 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: warp-macos-15-arm64-6x + timeout-minutes: 5 + steps: + - name: Verify at least one display regression attempt passed + run: | + set -euo pipefail + + ATTEMPT_1_PASSED="${{ needs.ui-display-resolution-regression-attempt-1.outputs.passed }}" + ATTEMPT_2_PASSED="${{ needs.ui-display-resolution-regression-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then + echo "Display resolution regression passed." + exit 0 + fi + + echo "Display resolution regression did not pass in either WarpBuild attempt." >&2 + echo "Attempt 1 result: ${{ needs.ui-display-resolution-regression-attempt-1.result }}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2 result: ${{ needs.ui-display-resolution-regression-attempt-2.result }}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 From f00070666dbabd789f29ea48cedb30e542da21aa Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 06:25:46 -0700 Subject: [PATCH 02/23] Retry Warp startup cancellations only --- .github/workflows/ci.yml | 232 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c348ae76efd6..6d7c4f46b96c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,14 +229,186 @@ jobs: CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py - tests-build-and-lag: + 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: 20 + outputs: + test_started: ${{ steps.mark-test-started.outputs.test_started }} + passed: ${{ steps.mark-passed.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 + + - name: Cache GhosttyKit.xcframework + id: cache-ghosttykit-lag + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty') }} + + - name: Download pre-built GhosttyKit.xcframework + if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true' + run: | + ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + run: | + ZIG_REQUIRED="0.15.2" + if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then + echo "zig ${ZIG_REQUIRED} already installed" + else + echo "Installing zig ${ZIG_REQUIRED} from tarball" + curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz + tar xf /tmp/zig.tar.xz -C /tmp + sudo mkdir -p /usr/local/bin /usr/local/lib + sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig + sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig + export PATH="/usr/local/bin:$PATH" + zig version + fi + + - name: Cache DerivedData + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* + key: deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }} + restore-keys: | + deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- + deriveddata-build- + + - name: Cache Swift packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .ci-source-packages + key: spm-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: spm-build- + + - name: Resolve Swift packages + run: | + set -euo pipefail + SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" + mkdir -p "$SOURCE_PACKAGES_DIR" + + for attempt in 1 2 3; do + if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -resolvePackageDependencies; then + exit 0 + fi + if [ "$attempt" -eq 3 ]; then + echo "Failed to resolve Swift packages after 3 attempts" >&2 + exit 1 + fi + echo "Package resolution failed on attempt $attempt, retrying..." + sleep $((attempt * 5)) + done + + - name: Build app + run: | + set -euo pipefail + SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" + xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" build + + - name: Create virtual display + run: | + set -euo pipefail + clang -framework Foundation -framework CoreGraphics \ + -o /tmp/create-virtual-display scripts/create-virtual-display.m + /tmp/create-virtual-display & + VDISPLAY_PID=$! + echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" + sleep 3 + + - name: Mark lag regression test started + id: mark-test-started + run: echo "test_started=true" >> "$GITHUB_OUTPUT" + + - 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: Mark lag regression passed + if: ${{ success() }} + id: mark-passed + run: echo "passed=true" >> "$GITHUB_OUTPUT" + + 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.result == 'cancelled' && needs.tests-build-and-lag-attempt-1.outputs.test_started != '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.mark-test-started.outputs.test_started }} + passed: ${{ steps.mark-passed.outputs.passed }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -344,6 +516,10 @@ jobs: echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" sleep 3 + - name: Mark lag regression test started + id: mark-test-started + run: echo "test_started=true" >> "$GITHUB_OUTPUT" + - name: Run workspace churn typing-lag regression run: | set -euo pipefail @@ -386,12 +562,47 @@ jobs: CMUX_LAG_KEY_EVENTS=180 \ python3 tests/test_workspace_churn_up_arrow_lag.py + - name: Mark lag regression passed + if: ${{ success() }} + id: mark-passed + run: echo "passed=true" >> "$GITHUB_OUTPUT" + + 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: warp-macos-15-arm64-6x + timeout-minutes: 5 + steps: + - name: Verify lag regression result + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-build-and-lag-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-build-and-lag-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-build-and-lag-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-build-and-lag-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-build-and-lag-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-build-and-lag-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then + echo "Lag regression passed." + exit 0 + fi + + echo "Lag regression did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + 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.mark-test-started.outputs.test_started }} passed: ${{ steps.mark-passed.outputs.passed }} steps: - name: Checkout @@ -464,6 +675,10 @@ jobs: sleep $((attempt * 5)) done + - name: Run display resolution churn UI regression + id: mark-test-started + run: echo "test_started=true" >> "$GITHUB_OUTPUT" + - name: Run display resolution churn UI regression run: | set -euo pipefail @@ -494,13 +709,14 @@ jobs: run: echo "passed=true" >> "$GITHUB_OUTPUT" 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.passed != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.ui-display-resolution-regression-attempt-1.result == 'cancelled' && needs.ui-display-resolution-regression-attempt-1.outputs.test_started != '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.mark-test-started.outputs.test_started }} passed: ${{ steps.mark-passed.outputs.passed }} steps: - name: Checkout @@ -573,6 +789,10 @@ jobs: sleep $((attempt * 5)) done + - name: Mark display regression test started + id: mark-test-started + run: echo "test_started=true" >> "$GITHUB_OUTPUT" + - name: Run display resolution churn UI regression run: | set -euo pipefail @@ -614,7 +834,11 @@ jobs: run: | set -euo pipefail + ATTEMPT_1_RESULT="${{ needs.ui-display-resolution-regression-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.ui-display-resolution-regression-attempt-1.outputs.test_started }}" ATTEMPT_1_PASSED="${{ needs.ui-display-resolution-regression-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.ui-display-resolution-regression-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.ui-display-resolution-regression-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.ui-display-resolution-regression-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -623,6 +847,6 @@ jobs: fi echo "Display resolution regression did not pass in either WarpBuild attempt." >&2 - echo "Attempt 1 result: ${{ needs.ui-display-resolution-regression-attempt-1.result }}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2 result: ${{ needs.ui-display-resolution-regression-attempt-2.result }}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 From 6d9c9260ac38e59e3a4d7cefc974c0b5915d64c4 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 06:29:51 -0700 Subject: [PATCH 03/23] Run CI wrapper jobs on hosted runners --- .github/workflows/ci.yml | 4 +-- tests/test_ci_self_hosted_guard.sh | 42 ++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d7c4f46b96c..4b3c7e5521cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -572,7 +572,7 @@ jobs: needs: - tests-build-and-lag-attempt-1 - tests-build-and-lag-attempt-2 - runs-on: warp-macos-15-arm64-6x + runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Verify lag regression result @@ -827,7 +827,7 @@ jobs: needs: - ui-display-resolution-regression-attempt-1 - ui-display-resolution-regression-attempt-2 - runs-on: warp-macos-15-arm64-6x + runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Verify at least one display regression attempt passed diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index 5e22c00f6793..f8d8fac2ab32 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -27,30 +27,56 @@ if ! awk ' exit 1 fi -# tests-build-and-lag: must use WarpBuild runner with fork guard (paid runner) +# tests-build-and-lag-attempt-1: must use WarpBuild runner with fork guard (paid runner) if ! awk ' - /^ tests-build-and-lag:/ { in_tests=1; next } + /^ tests-build-and-lag-attempt-1:/ { in_tests=1; next } in_tests && /^ [^[:space:]]/ { in_tests=0 } in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } END { exit !(saw_warp && saw_guard) } ' "$WORKFLOW_FILE"; then - echo "FAIL: tests-build-and-lag block must keep both warp-macos-15-arm64-6x runner and fork guard" + echo "FAIL: tests-build-and-lag-attempt-1 block must keep both warp-macos-15-arm64-6x runner and fork guard" exit 1 fi -# ui-display-resolution-regression: must use WarpBuild runner with fork guard (paid runner) +# tests-build-and-lag wrapper: should stay on hosted Linux because it only aggregates attempt outputs. if ! awk ' - /^ ui-display-resolution-regression:/ { in_tests=1; next } + /^ tests-build-and-lag:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: ubuntu-latest/ { saw_hosted=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_hosted && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-build-and-lag wrapper must keep both ubuntu-latest runner and fork guard" + exit 1 +fi + +# ui-display-resolution-regression-attempt-1: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ ui-display-resolution-regression-attempt-1:/ { in_tests=1; next } in_tests && /^ [^[:space:]]/ { in_tests=0 } in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } END { exit !(saw_warp && saw_guard) } ' "$WORKFLOW_FILE"; then - echo "FAIL: ui-display-resolution-regression block must keep both warp-macos-15-arm64-6x runner and fork guard" + echo "FAIL: ui-display-resolution-regression-attempt-1 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# ui-display-resolution-regression wrapper: should stay on hosted Linux because it only aggregates attempt outputs. +if ! awk ' + /^ ui-display-resolution-regression:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: ubuntu-latest/ { saw_hosted=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_hosted && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: ui-display-resolution-regression wrapper must keep both ubuntu-latest runner and fork guard" exit 1 fi echo "PASS: tests WarpBuild runner fork guard is present" -echo "PASS: tests-build-and-lag WarpBuild runner fork guard is present" -echo "PASS: ui-display-resolution-regression WarpBuild runner fork guard is present" +echo "PASS: tests-build-and-lag attempt WarpBuild runner fork guard is present" +echo "PASS: tests-build-and-lag wrapper hosted runner guard is present" +echo "PASS: ui-display-resolution-regression attempt WarpBuild runner fork guard is present" +echo "PASS: ui-display-resolution-regression wrapper hosted runner guard is present" From 594791d3d84583e1b214ea010ba057672b32d50f Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 06:48:30 -0700 Subject: [PATCH 04/23] Extract CI retry steps into composite actions --- .../action.yml | 116 ++++ .github/actions/run-lag-regression/action.yml | 171 ++++++ .github/workflows/ci.yml | 528 +----------------- 3 files changed, 307 insertions(+), 508 deletions(-) create mode 100644 .github/actions/run-display-resolution-regression/action.yml create mode 100644 .github/actions/run-lag-regression/action.yml 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..e82c8581696d --- /dev/null +++ b/.github/actions/run-display-resolution-regression/action.yml @@ -0,0 +1,116 @@ +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.mark-test-started.outputs.test_started }} + 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: Download pre-built GhosttyKit.xcframework + shell: bash + run: ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + shell: bash + run: | + ZIG_REQUIRED="0.15.2" + if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then + echo "zig ${ZIG_REQUIRED} already installed" + else + echo "Installing zig ${ZIG_REQUIRED} from tarball" + curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz + tar xf /tmp/zig.tar.xz -C /tmp + sudo mkdir -p /usr/local/bin /usr/local/lib + sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig + sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig + export PATH="/usr/local/bin:$PATH" + zig version + fi + + - name: Cache Swift packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .ci-source-packages + key: spm-ui-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: Mark display regression test started + id: mark-test-started + shell: bash + run: echo "test_started=true" >> "$GITHUB_OUTPUT" + + - name: Run display resolution churn UI regression + shell: bash + run: | + set -euo pipefail + 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 + + cat >"$MANIFEST_PATH" <> "$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..fc14742e7401 --- /dev/null +++ b/.github/actions/run-lag-regression/action.yml @@ -0,0 +1,171 @@ +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.mark-test-started.outputs.test_started }} + 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: 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' + shell: bash + run: ./scripts/download-prebuilt-ghosttykit.sh + + - name: Install zig + shell: bash + run: | + ZIG_REQUIRED="0.15.2" + if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then + echo "zig ${ZIG_REQUIRED} already installed" + else + echo "Installing zig ${ZIG_REQUIRED} from tarball" + curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz + tar xf /tmp/zig.tar.xz -C /tmp + sudo mkdir -p /usr/local/bin /usr/local/lib + sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig + sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig + export PATH="/usr/local/bin:$PATH" + zig version + fi + + - name: Cache DerivedData + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* + key: deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }} + restore-keys: | + deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- + deriveddata-build- + + - name: Cache Swift packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: .ci-source-packages + key: spm-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: spm-build- + + - name: Resolve Swift packages + 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: Mark lag regression test started + id: mark-test-started + shell: bash + run: echo "test_started=true" >> "$GITHUB_OUTPUT" + + - name: Run workspace churn typing-lag regression + shell: bash + 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: Mark lag regression passed + if: ${{ success() }} + id: mark-passed + shell: bash + run: echo "passed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b3c7e5521cc..4c5349a501fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,165 +239,16 @@ jobs: runs-on: warp-macos-15-arm64-6x timeout-minutes: 20 outputs: - test_started: ${{ steps.mark-test-started.outputs.test_started }} - passed: ${{ steps.mark-passed.outputs.passed }} + test_started: ${{ steps.lag-regression.outputs.test_started }} + 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 - - - name: Cache GhosttyKit.xcframework - id: cache-ghosttykit-lag - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: GhosttyKit.xcframework - key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty') }} - - - name: Download pre-built GhosttyKit.xcframework - if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true' - run: | - ./scripts/download-prebuilt-ghosttykit.sh - - - name: Install zig - run: | - ZIG_REQUIRED="0.15.2" - if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then - echo "zig ${ZIG_REQUIRED} already installed" - else - echo "Installing zig ${ZIG_REQUIRED} from tarball" - curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz - tar xf /tmp/zig.tar.xz -C /tmp - sudo mkdir -p /usr/local/bin /usr/local/lib - sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig - sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig - export PATH="/usr/local/bin:$PATH" - zig version - fi - - - name: Cache DerivedData - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* - key: deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }} - restore-keys: | - deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- - deriveddata-build- - - - name: Cache Swift packages - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: .ci-source-packages - key: spm-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-build- - - - name: Resolve Swift packages - run: | - set -euo pipefail - SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - mkdir -p "$SOURCE_PACKAGES_DIR" - - for attempt in 1 2 3; do - if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ - -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ - -resolvePackageDependencies; then - exit 0 - fi - if [ "$attempt" -eq 3 ]; then - echo "Failed to resolve Swift packages after 3 attempts" >&2 - exit 1 - fi - echo "Package resolution failed on attempt $attempt, retrying..." - sleep $((attempt * 5)) - done - - - name: Build app - run: | - set -euo pipefail - SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ - -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ - -disableAutomaticPackageResolution \ - -destination "platform=macOS" build - - - name: Create virtual display - run: | - set -euo pipefail - clang -framework Foundation -framework CoreGraphics \ - -o /tmp/create-virtual-display scripts/create-virtual-display.m - /tmp/create-virtual-display & - VDISPLAY_PID=$! - echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" - sleep 3 - - - name: Mark lag regression test started - id: mark-test-started - run: echo "test_started=true" >> "$GITHUB_OUTPUT" - - - 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: Mark lag regression passed - if: ${{ success() }} - id: mark-passed - run: echo "passed=true" >> "$GITHUB_OUTPUT" + - 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.result == 'cancelled' && needs.tests-build-and-lag-attempt-1.outputs.test_started != 'true' }} @@ -407,165 +258,16 @@ jobs: runs-on: warp-macos-15-arm64-6x timeout-minutes: 20 outputs: - test_started: ${{ steps.mark-test-started.outputs.test_started }} - passed: ${{ steps.mark-passed.outputs.passed }} + test_started: ${{ steps.lag-regression.outputs.test_started }} + 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 - - - name: Cache GhosttyKit.xcframework - id: cache-ghosttykit-lag - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: GhosttyKit.xcframework - key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty') }} - - - name: Download pre-built GhosttyKit.xcframework - if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true' - run: | - ./scripts/download-prebuilt-ghosttykit.sh - - - name: Install zig - run: | - ZIG_REQUIRED="0.15.2" - if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then - echo "zig ${ZIG_REQUIRED} already installed" - else - echo "Installing zig ${ZIG_REQUIRED} from tarball" - curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz - tar xf /tmp/zig.tar.xz -C /tmp - sudo mkdir -p /usr/local/bin /usr/local/lib - sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig - sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig - export PATH="/usr/local/bin:$PATH" - zig version - fi - - - name: Cache DerivedData - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* - key: deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.pbxproj') }} - restore-keys: | - deriveddata-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- - deriveddata-build- - - - name: Cache Swift packages - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: .ci-source-packages - key: spm-build-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-build- - - - name: Resolve Swift packages - run: | - set -euo pipefail - SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - mkdir -p "$SOURCE_PACKAGES_DIR" - - for attempt in 1 2 3; do - if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ - -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ - -resolvePackageDependencies; then - exit 0 - fi - if [ "$attempt" -eq 3 ]; then - echo "Failed to resolve Swift packages after 3 attempts" >&2 - exit 1 - fi - echo "Package resolution failed on attempt $attempt, retrying..." - sleep $((attempt * 5)) - done - - - name: Build app - run: | - set -euo pipefail - SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ - -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ - -disableAutomaticPackageResolution \ - -destination "platform=macOS" build - - - name: Create virtual display - run: | - set -euo pipefail - clang -framework Foundation -framework CoreGraphics \ - -o /tmp/create-virtual-display scripts/create-virtual-display.m - /tmp/create-virtual-display & - VDISPLAY_PID=$! - echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" - sleep 3 - - - name: Mark lag regression test started - id: mark-test-started - run: echo "test_started=true" >> "$GITHUB_OUTPUT" - - - 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: Mark lag regression passed - if: ${{ success() }} - id: mark-passed - run: echo "passed=true" >> "$GITHUB_OUTPUT" + - name: Run lag regression attempt + id: lag-regression + uses: ./.github/actions/run-lag-regression tests-build-and-lag: if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} @@ -602,111 +304,16 @@ jobs: runs-on: warp-macos-15-arm64-6x timeout-minutes: 25 outputs: - test_started: ${{ steps.mark-test-started.outputs.test_started }} - passed: ${{ steps.mark-passed.outputs.passed }} + test_started: ${{ steps.display-regression.outputs.test_started }} + passed: ${{ steps.display-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 - 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 - 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: mark-test-started - run: echo "test_started=true" >> "$GITHUB_OUTPUT" - - - name: Run display resolution churn UI regression - run: | - set -euo pipefail - 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 - - cat >"$MANIFEST_PATH" <> "$GITHUB_OUTPUT" + - 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.result == 'cancelled' && needs.ui-display-resolution-regression-attempt-1.outputs.test_started != 'true' }} @@ -716,111 +323,16 @@ jobs: runs-on: warp-macos-15-arm64-6x timeout-minutes: 25 outputs: - test_started: ${{ steps.mark-test-started.outputs.test_started }} - passed: ${{ steps.mark-passed.outputs.passed }} + test_started: ${{ steps.display-regression.outputs.test_started }} + passed: ${{ steps.display-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 - 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 - 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: Mark display regression test started - id: mark-test-started - run: echo "test_started=true" >> "$GITHUB_OUTPUT" - - - name: Run display resolution churn UI regression - run: | - set -euo pipefail - 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 - - cat >"$MANIFEST_PATH" <> "$GITHUB_OUTPUT" + - 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) }} From 62dabe1e314be47bc8d91d014d3bce527ec05e7a Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 06:49:56 -0700 Subject: [PATCH 05/23] Tighten CI retry start signals --- .../action.yml | 9 ++---- .github/actions/run-lag-regression/action.yml | 9 ++---- tests/test_ci_self_hosted_guard.sh | 30 +++++++++++++++++-- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/.github/actions/run-display-resolution-regression/action.yml b/.github/actions/run-display-resolution-regression/action.yml index e82c8581696d..f450154bceac 100644 --- a/.github/actions/run-display-resolution-regression/action.yml +++ b/.github/actions/run-display-resolution-regression/action.yml @@ -3,7 +3,7 @@ description: Run the display resolution churn XCUITest on WarpBuild. outputs: test_started: description: Whether the display regression step started. - value: ${{ steps.mark-test-started.outputs.test_started }} + value: ${{ steps.run-display-regression.outputs.test_started }} passed: description: Whether the display regression passed. value: ${{ steps.mark-passed.outputs.passed }} @@ -79,15 +79,12 @@ runs: sleep $((attempt * 5)) done - - name: Mark display regression test started - id: mark-test-started - shell: bash - run: echo "test_started=true" >> "$GITHUB_OUTPUT" - - 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" diff --git a/.github/actions/run-lag-regression/action.yml b/.github/actions/run-lag-regression/action.yml index fc14742e7401..979f667a8ea5 100644 --- a/.github/actions/run-lag-regression/action.yml +++ b/.github/actions/run-lag-regression/action.yml @@ -3,7 +3,7 @@ description: Build cmux on WarpBuild and run the workspace churn typing-lag regr outputs: test_started: description: Whether the lag regression step started. - value: ${{ steps.mark-test-started.outputs.test_started }} + value: ${{ steps.run-lag-regression.outputs.test_started }} passed: description: Whether the lag regression passed. value: ${{ steps.mark-passed.outputs.passed }} @@ -116,15 +116,12 @@ runs: echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" sleep 3 - - name: Mark lag regression test started - id: mark-test-started - shell: bash - run: echo "test_started=true" >> "$GITHUB_OUTPUT" - - 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 diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index f8d8fac2ab32..893184d5c042 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -39,6 +39,18 @@ if ! awk ' exit 1 fi +# tests-build-and-lag-attempt-2: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ tests-build-and-lag-attempt-2:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-build-and-lag-attempt-2 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + # tests-build-and-lag wrapper: should stay on hosted Linux because it only aggregates attempt outputs. if ! awk ' /^ tests-build-and-lag:/ { in_tests=1; next } @@ -63,6 +75,18 @@ if ! awk ' exit 1 fi +# ui-display-resolution-regression-attempt-2: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ ui-display-resolution-regression-attempt-2:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: ui-display-resolution-regression-attempt-2 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + # ui-display-resolution-regression wrapper: should stay on hosted Linux because it only aggregates attempt outputs. if ! awk ' /^ ui-display-resolution-regression:/ { in_tests=1; next } @@ -76,7 +100,9 @@ if ! awk ' fi echo "PASS: tests WarpBuild runner fork guard is present" -echo "PASS: tests-build-and-lag attempt WarpBuild runner fork guard is present" +echo "PASS: tests-build-and-lag attempt-1 WarpBuild runner fork guard is present" +echo "PASS: tests-build-and-lag attempt-2 WarpBuild runner fork guard is present" echo "PASS: tests-build-and-lag wrapper hosted runner guard is present" -echo "PASS: ui-display-resolution-regression attempt WarpBuild runner fork guard is present" +echo "PASS: ui-display-resolution-regression attempt-1 WarpBuild runner fork guard is present" +echo "PASS: ui-display-resolution-regression attempt-2 WarpBuild runner fork guard is present" echo "PASS: ui-display-resolution-regression wrapper hosted runner guard is present" From b3598dc75562cb0d8fea55a184cc8a37a62f8845 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 07:00:57 -0700 Subject: [PATCH 06/23] Polish CI composite actions --- .../run-display-resolution-regression/action.yml | 12 +++++++++--- .github/actions/run-lag-regression/action.yml | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/actions/run-display-resolution-regression/action.yml b/.github/actions/run-display-resolution-regression/action.yml index f450154bceac..e29e7198c89e 100644 --- a/.github/actions/run-display-resolution-regression/action.yml +++ b/.github/actions/run-display-resolution-regression/action.yml @@ -30,7 +30,15 @@ runs: xcodebuild -version xcrun --sdk macosx --show-sdk-path + - name: Cache GhosttyKit.xcframework + id: cache-ghosttykit-display + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + with: + path: GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty') }} + - name: Download pre-built GhosttyKit.xcframework + if: steps.cache-ghosttykit-display.outputs.cache-hit != 'true' shell: bash run: ./scripts/download-prebuilt-ghosttykit.sh @@ -95,9 +103,7 @@ runs: clang -framework Foundation -framework CoreGraphics \ -o "$HELPER_PATH" scripts/create-virtual-display.m - cat >"$MANIFEST_PATH" <"$MANIFEST_PATH" xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ diff --git a/.github/actions/run-lag-regression/action.yml b/.github/actions/run-lag-regression/action.yml index 979f667a8ea5..f6e2bd1e8223 100644 --- a/.github/actions/run-lag-regression/action.yml +++ b/.github/actions/run-lag-regression/action.yml @@ -166,3 +166,11 @@ runs: 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 From 07f313bac3359ffab54d1b64d4d3f2faf9b750a1 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 07:33:50 -0700 Subject: [PATCH 07/23] Raise CI tests timeout budget --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c5349a501fe..77012360e175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: # Never run WarpBuild jobs for fork pull requests (avoid billing on external PRs). if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: warp-macos-15-arm64-6x - timeout-minutes: 20 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 From 0ca466dd0c81a61ed4a99b47de73d00eae82e287 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 07:37:59 -0700 Subject: [PATCH 08/23] Harden Zig install and GhosttyKit cache keys --- .../action.yml | 22 +++------ .github/actions/run-lag-regression/action.yml | 22 +++------ .github/workflows/ci.yml | 21 +++------ scripts/install-zig.sh | 46 +++++++++++++++++++ 4 files changed, 66 insertions(+), 45 deletions(-) create mode 100755 scripts/install-zig.sh diff --git a/.github/actions/run-display-resolution-regression/action.yml b/.github/actions/run-display-resolution-regression/action.yml index e29e7198c89e..f0317a2e1159 100644 --- a/.github/actions/run-display-resolution-regression/action.yml +++ b/.github/actions/run-display-resolution-regression/action.yml @@ -30,12 +30,17 @@ runs: 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', 'ghostty') }} + 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' @@ -44,20 +49,7 @@ runs: - name: Install zig shell: bash - 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 + run: ./scripts/install-zig.sh - name: Cache Swift packages uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 diff --git a/.github/actions/run-lag-regression/action.yml b/.github/actions/run-lag-regression/action.yml index f6e2bd1e8223..84d209104d62 100644 --- a/.github/actions/run-lag-regression/action.yml +++ b/.github/actions/run-lag-regression/action.yml @@ -29,12 +29,17 @@ runs: 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', 'ghostty') }} + 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' @@ -43,20 +48,7 @@ runs: - name: Install zig shell: bash - 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 + run: ./scripts/install-zig.sh - name: Cache DerivedData uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77012360e175..738cdee7dad5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,12 +99,16 @@ jobs: export DEVELOPER_DIR="$XCODE_DIR" xcodebuild -version + - name: Capture GhosttyKit cache key + id: ghosttykit-cache-key + 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', 'ghostty') }} + 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' @@ -112,20 +116,7 @@ jobs: ./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 + run: ./scripts/install-zig.sh - name: Cache DerivedData uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 diff --git a/scripts/install-zig.sh b/scripts/install-zig.sh new file mode 100755 index 000000000000..e5571a5cfe44 --- /dev/null +++ b/scripts/install-zig.sh @@ -0,0 +1,46 @@ +#!/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 + echo "zig ${ZIG_REQUIRED} already installed" + exit 0 +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 From fc7b5f4450c096f41931e0716b971ee7fb89ab10 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 08:18:40 -0700 Subject: [PATCH 09/23] Raise CI unit test budgets and harden Zig reuse --- .github/workflows/ci-macos-compat.yml | 25 ++++++++----------------- .github/workflows/ci.yml | 2 +- scripts/install-zig.sh | 9 +++++++-- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci-macos-compat.yml b/.github/workflows/ci-macos-compat.yml index c3e9b358bbde..2e734dc3724a 100644 --- a/.github/workflows/ci-macos-compat.yml +++ b/.github/workflows/ci-macos-compat.yml @@ -15,11 +15,11 @@ jobs: matrix: include: - os: warp-macos-15-arm64-6x - timeout: 30 + timeout: 45 smoke: true skip_zig: false - os: warp-macos-26-arm64-6x - timeout: 30 + timeout: 45 smoke: false skip_zig: true # zig 0.15.2 MachO linker can't resolve libSystem on macOS 26 runs-on: ${{ matrix.os }} @@ -55,12 +55,16 @@ jobs: xcrun --sdk macosx --show-sdk-path sw_vers + - name: Capture GhosttyKit cache key + id: ghosttykit-cache-key + 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', 'ghostty') }} + 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' @@ -69,20 +73,7 @@ jobs: - 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 + run: ./scripts/install-zig.sh - name: Cache DerivedData uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 738cdee7dad5..4cd33ef48e52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: # Never run WarpBuild jobs for fork pull requests (avoid billing on external PRs). if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: warp-macos-15-arm64-6x - timeout-minutes: 30 + timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/scripts/install-zig.sh b/scripts/install-zig.sh index e5571a5cfe44..e3347b06f7ca 100755 --- a/scripts/install-zig.sh +++ b/scripts/install-zig.sh @@ -17,8 +17,13 @@ case "${ZIG_REQUIRED}:${ZIG_PLATFORM}" in esac if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}$"; then - echo "zig ${ZIG_REQUIRED} already installed" - exit 0 + 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" From b82c95e322bd79a782fa1e632292a0151c69eda7 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 09:20:21 -0700 Subject: [PATCH 10/23] Shard Warp unit tests and slim compat CI --- .../actions/run-unit-test-shard/action.yml | 152 +++++++++++ .github/workflows/ci-macos-compat.yml | 52 +--- .github/workflows/ci.yml | 242 +++++++++--------- scripts/unit_test_shard_args.py | 63 +++++ tests/test_ci_self_hosted_guard.sh | 86 ++++++- tests/test_ci_unit_test_spm_retry.sh | 10 +- 6 files changed, 430 insertions(+), 175 deletions(-) create mode 100644 .github/actions/run-unit-test-shard/action.yml create mode 100644 scripts/unit_test_shard_args.py 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..3afa769270e5 --- /dev/null +++ b/.github/actions/run-unit-test-shard/action.yml @@ -0,0 +1,152 @@ +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" +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: Run unit test shard + 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" + mapfile -t TEST_FILTERS < <( + 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 + + 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 diff --git a/.github/workflows/ci-macos-compat.yml b/.github/workflows/ci-macos-compat.yml index 2e734dc3724a..79d0f6569b31 100644 --- a/.github/workflows/ci-macos-compat.yml +++ b/.github/workflows/ci-macos-compat.yml @@ -111,48 +111,16 @@ jobs: sleep $((attempt * 5)) done - - name: Run unit tests + - name: Build app env: CMUX_SKIP_ZIG_BUILD: ${{ matrix.skip_zig && '1' || '0' }} 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" 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" - exit 1 - fi - fi + xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" build - name: Create virtual display if: matrix.smoke @@ -170,16 +138,6 @@ jobs: 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 - - name: Smoke test if: matrix.smoke run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cd33ef48e52..e6ed74b26ffe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,139 +70,26 @@ jobs: - name: Typecheck run: bun tsc --noEmit - tests: + tests-shard-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 runs-on: warp-macos-15-arm64-6x - timeout-minutes: 45 + 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 - - - name: Capture GhosttyKit cache key - id: ghosttykit-cache-key - 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' - run: | - ./scripts/download-prebuilt-ghosttykit.sh - - - name: Install zig - 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 + - name: Run unit test shard + uses: ./.github/actions/run-unit-test-shard 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" test 2>&1 - } - - # xcodebuild exits 65 even for expected failures (XCTExpectFailure). - # Capture output and fail only if there are unexpected failures. - set +e - OUTPUT=$(run_unit_tests) - EXIT_CODE=$? - 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 - OUTPUT=$(run_unit_tests) - EXIT_CODE=$? - set -e - fi - - echo "$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 - + 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 @@ -220,6 +107,123 @@ jobs: CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py + tests-shard-2: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "1" + shard_count: "6" + + tests-shard-3: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "2" + shard_count: "6" + + tests-shard-4: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "3" + shard_count: "6" + + tests-shard-5: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "4" + shard_count: "6" + + tests-shard-6: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + - name: Run unit test shard + uses: ./.github/actions/run-unit-test-shard + with: + shard_index: "5" + shard_count: "6" + + 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 + + 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 + + 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 + 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: 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/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index 893184d5c042..a527a0b57e93 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -15,15 +15,87 @@ if ! grep -Fq "$EXPECTED_IF" "$WORKFLOW_FILE"; then exit 1 fi -# tests: must use WarpBuild runner with fork guard (paid runner) +# tests-shard-1: must use WarpBuild runner with fork guard (paid runner) if ! awk ' - /^ tests:/ { in_tests=1; next } + /^ tests-shard-1:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-shard-1 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# tests-shard-2: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ tests-shard-2:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-shard-2 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# tests-shard-3: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ tests-shard-3:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-shard-3 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# tests-shard-4: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ tests-shard-4:/ { in_tests=1; next } in_tests && /^ [^[:space:]]/ { in_tests=0 } in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } END { exit !(saw_warp && saw_guard) } ' "$WORKFLOW_FILE"; then - echo "FAIL: tests block must keep both warp-macos-15-arm64-6x runner and fork guard" + echo "FAIL: tests-shard-4 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# tests-shard-5: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ tests-shard-5:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-shard-5 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# tests-shard-6: must use WarpBuild runner with fork guard (paid runner) +if ! awk ' + /^ tests-shard-6:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_warp && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests-shard-6 block must keep both warp-macos-15-arm64-6x runner and fork guard" + exit 1 +fi + +# tests wrapper: should stay on hosted Linux because it only aggregates shard outputs. +if ! awk ' + /^ tests:/ { in_tests=1; next } + in_tests && /^ [^[:space:]]/ { in_tests=0 } + in_tests && /runs-on: ubuntu-latest/ { saw_hosted=1 } + in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } + END { exit !(saw_hosted && saw_guard) } +' "$WORKFLOW_FILE"; then + echo "FAIL: tests wrapper must keep both ubuntu-latest runner and fork guard" exit 1 fi @@ -99,7 +171,13 @@ if ! awk ' exit 1 fi -echo "PASS: tests WarpBuild runner fork guard is present" +echo "PASS: tests-shard-1 WarpBuild runner fork guard is present" +echo "PASS: tests-shard-2 WarpBuild runner fork guard is present" +echo "PASS: tests-shard-3 WarpBuild runner fork guard is present" +echo "PASS: tests-shard-4 WarpBuild runner fork guard is present" +echo "PASS: tests-shard-5 WarpBuild runner fork guard is present" +echo "PASS: tests-shard-6 WarpBuild runner fork guard is present" +echo "PASS: tests wrapper hosted runner guard is present" echo "PASS: tests-build-and-lag attempt-1 WarpBuild runner fork guard is present" echo "PASS: tests-build-and-lag attempt-2 WarpBuild runner fork guard is present" echo "PASS: tests-build-and-lag wrapper hosted runner guard is present" diff --git a/tests/test_ci_unit_test_spm_retry.sh b/tests/test_ci_unit_test_spm_retry.sh index 8888a645b7a5..a642d3b52b3a 100755 --- a/tests/test_ci_unit_test_spm_retry.sh +++ b/tests/test_ci_unit_test_spm_retry.sh @@ -3,20 +3,20 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci.yml" +ACTION_FILE="$ROOT_DIR/.github/actions/run-unit-test-shard/action.yml" REQUIRED_PATTERNS=( "run_unit_tests()" "Could not resolve package dependencies" "rm -rf ~/Library/Caches/org.swift.swiftpm" - "OUTPUT=\$(run_unit_tests)" + "run_unit_tests | tee /tmp/test-output.txt" ) for pattern in "${REQUIRED_PATTERNS[@]}"; do - if ! grep -Fq "$pattern" "$WORKFLOW_FILE"; then - echo "FAIL: Missing pattern in ci.yml: $pattern" + if ! grep -Fq "$pattern" "$ACTION_FILE"; then + echo "FAIL: Missing pattern in run-unit-test-shard action: $pattern" exit 1 fi done -echo "PASS: CI unit-test SwiftPM retry guard is present" +echo "PASS: CI unit-test shard SwiftPM retry guard is present" From 9901891dfa9e75dd61153c16ab4c56003f00da1c Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 09:33:23 -0700 Subject: [PATCH 11/23] Fix shard bash portability and restore compat test coverage --- .../actions/run-unit-test-shard/action.yml | 5 +- .github/workflows/ci-macos-compat.yml | 51 +++++++++++++++++++ .github/workflows/ci.yml | 3 ++ ...est_ci_ghosttykit_checksum_verification.sh | 13 +++-- tests/test_ci_macos_compat_test_slice.sh | 19 +++++++ 5 files changed, 85 insertions(+), 6 deletions(-) create mode 100755 tests/test_ci_macos_compat_test_slice.sh diff --git a/.github/actions/run-unit-test-shard/action.yml b/.github/actions/run-unit-test-shard/action.yml index 3afa769270e5..53584045877b 100644 --- a/.github/actions/run-unit-test-shard/action.yml +++ b/.github/actions/run-unit-test-shard/action.yml @@ -101,7 +101,10 @@ runs: run: | set -euo pipefail SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - mapfile -t TEST_FILTERS < <( + 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" diff --git a/.github/workflows/ci-macos-compat.yml b/.github/workflows/ci-macos-compat.yml index 79d0f6569b31..c352976bdc1f 100644 --- a/.github/workflows/ci-macos-compat.yml +++ b/.github/workflows/ci-macos-compat.yml @@ -18,10 +18,12 @@ jobs: timeout: 45 smoke: true skip_zig: false + compat_test_shard_index: 0 - os: warp-macos-26-arm64-6x timeout: 45 smoke: false skip_zig: true # zig 0.15.2 MachO linker can't resolve libSystem on macOS 26 + compat_test_shard_index: 1 runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout }} steps: @@ -122,6 +124,55 @@ jobs: -disableAutomaticPackageResolution \ -destination "platform=macOS" build + - name: Run compatibility unit test slice + env: + CMUX_SKIP_ZIG_BUILD: ${{ matrix.skip_zig && '1' || '0' }} + COMPAT_TEST_SHARD_INDEX: ${{ matrix.compat_test_shard_index }} + COMPAT_TEST_SHARD_COUNT: "24" + 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 "$COMPAT_TEST_SHARD_INDEX" \ + --shard-count "$COMPAT_TEST_SHARD_COUNT" + ) + + if [ "${#TEST_FILTERS[@]}" -eq 0 ]; then + echo "No compatibility test classes selected" >&2 + exit 1 + fi + + echo "Running ${#TEST_FILTERS[@]} compatibility test classes on ${{ matrix.os }}" + + 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: matrix.smoke run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6ed74b26ffe..e46bb581e9c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: - 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 diff --git a/tests/test_ci_ghosttykit_checksum_verification.sh b/tests/test_ci_ghosttykit_checksum_verification.sh index 1eba6ecfc05c..3cbbf8b7329c 100755 --- a/tests/test_ci_ghosttykit_checksum_verification.sh +++ b/tests/test_ci_ghosttykit_checksum_verification.sh @@ -7,8 +7,11 @@ SCRIPT="$ROOT_DIR/scripts/download-prebuilt-ghosttykit.sh" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT -WORKFLOWS=( - "$ROOT_DIR/.github/workflows/ci.yml" +HELPER_CALL_SITES=( + "$ROOT_DIR/.github/actions/run-unit-test-shard/action.yml" + "$ROOT_DIR/.github/actions/run-display-resolution-regression/action.yml" + "$ROOT_DIR/.github/actions/run-lag-regression/action.yml" + "$ROOT_DIR/.github/workflows/ci-macos-compat.yml" "$ROOT_DIR/.github/workflows/nightly.yml" "$ROOT_DIR/.github/workflows/release.yml" ) @@ -31,9 +34,9 @@ printf 'fixture\n' > "$FIXTURE_DIR/GhosttyKit.xcframework/marker.txt" ACTUAL_SHA256="$(shasum -a 256 "$TMP_DIR/GhosttyKit.xcframework.tar.gz" | awk '{print $1}')" printf '%s %s\n' "$FIXTURE_SHA" "$ACTUAL_SHA256" > "$CHECKSUMS_FILE" -for workflow in "${WORKFLOWS[@]}"; do - if ! grep -Fq './scripts/download-prebuilt-ghosttykit.sh' "$workflow"; then - echo "FAIL: $workflow must call download-prebuilt-ghosttykit.sh" +for helper_call_site in "${HELPER_CALL_SITES[@]}"; do + if ! grep -Fq './scripts/download-prebuilt-ghosttykit.sh' "$helper_call_site"; then + echo "FAIL: $helper_call_site must call download-prebuilt-ghosttykit.sh" exit 1 fi done diff --git a/tests/test_ci_macos_compat_test_slice.sh b/tests/test_ci_macos_compat_test_slice.sh new file mode 100755 index 000000000000..8c44a9ce4073 --- /dev/null +++ b/tests/test_ci_macos_compat_test_slice.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci-macos-compat.yml" + +if ! awk ' + /^ - name: Run compatibility unit test slice$/ { in_step=1; next } + in_step && /^ - name:/ { in_step=0 } + in_step && /scripts\/unit_test_shard_args\.py/ { saw_shard_args=1 } + in_step && /-scheme cmux-unit/ { saw_unit_scheme=1 } + in_step && /test 2>&1/ { saw_test=1 } + END { exit !(saw_shard_args && saw_unit_scheme && saw_test) } +' "$WORKFLOW_FILE"; then + echo "FAIL: ci-macos-compat.yml must keep a real compatibility unit test slice" + exit 1 +fi + +echo "PASS: macOS compat workflow keeps a compatibility unit test slice" From db6538a345853e2cdf2e5f5d9a75bd57896796d9 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 09:52:21 -0700 Subject: [PATCH 12/23] Stabilize macOS compat test selection --- .github/workflows/ci-macos-compat.yml | 33 ++++++++++++++------ tests/test_ci_macos_compat_test_slice.sh | 38 ++++++++++++++++-------- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci-macos-compat.yml b/.github/workflows/ci-macos-compat.yml index c352976bdc1f..e842d217c8dc 100644 --- a/.github/workflows/ci-macos-compat.yml +++ b/.github/workflows/ci-macos-compat.yml @@ -18,12 +18,28 @@ jobs: timeout: 45 smoke: true skip_zig: false - compat_test_shard_index: 0 + 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 - os: warp-macos-26-arm64-6x timeout: 45 smoke: false skip_zig: true # zig 0.15.2 MachO linker can't resolve libSystem on macOS 26 - compat_test_shard_index: 1 + 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 runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout }} steps: @@ -127,19 +143,18 @@ jobs: - name: Run compatibility unit test slice env: CMUX_SKIP_ZIG_BUILD: ${{ matrix.skip_zig && '1' || '0' }} - COMPAT_TEST_SHARD_INDEX: ${{ matrix.compat_test_shard_index }} - COMPAT_TEST_SHARD_COUNT: "24" 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 < <( - python3 scripts/unit_test_shard_args.py \ - --shard-index "$COMPAT_TEST_SHARD_INDEX" \ - --shard-count "$COMPAT_TEST_SHARD_COUNT" - ) + done <<'EOF' + ${{ matrix.compat_test_filters }} + EOF if [ "${#TEST_FILTERS[@]}" -eq 0 ]; then echo "No compatibility test classes selected" >&2 diff --git a/tests/test_ci_macos_compat_test_slice.sh b/tests/test_ci_macos_compat_test_slice.sh index 8c44a9ce4073..e09886edcb2e 100755 --- a/tests/test_ci_macos_compat_test_slice.sh +++ b/tests/test_ci_macos_compat_test_slice.sh @@ -4,16 +4,30 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci-macos-compat.yml" -if ! awk ' - /^ - name: Run compatibility unit test slice$/ { in_step=1; next } - in_step && /^ - name:/ { in_step=0 } - in_step && /scripts\/unit_test_shard_args\.py/ { saw_shard_args=1 } - in_step && /-scheme cmux-unit/ { saw_unit_scheme=1 } - in_step && /test 2>&1/ { saw_test=1 } - END { exit !(saw_shard_args && saw_unit_scheme && saw_test) } -' "$WORKFLOW_FILE"; then - echo "FAIL: ci-macos-compat.yml must keep a real compatibility unit test slice" - exit 1 -fi +require_pattern() { + local pattern="$1" + local message="$2" + if ! grep -q -- "$pattern" "$WORKFLOW_FILE"; then + echo "FAIL: $message" + exit 1 + fi +} -echo "PASS: macOS compat workflow keeps a compatibility unit test slice" +reject_pattern() { + local pattern="$1" + local message="$2" + if grep -q -- "$pattern" "$WORKFLOW_FILE"; then + echo "FAIL: $message" + exit 1 + fi +} + +require_pattern "compat_test_filters:" "ci-macos-compat.yml must define explicit compatibility test filters" +require_pattern "cmuxTests/AppDelegateLaunchServicesRegistrationTests" "macOS 15 compat filters are missing" +require_pattern "cmuxTests/ZshShellIntegrationHandoffTests" "macOS 26 compat filters are missing" +require_pattern "while IFS= read -r filter; do" "compat step must still load test filters into an array" +require_pattern "-scheme cmux-unit" "compat step must run the cmux-unit scheme" +require_pattern "test 2>&1" "compat step must execute unit tests" +reject_pattern "scripts/unit_test_shard_args.py" "compat workflow must not rely on shard-based test selection" + +echo "PASS: macOS compat workflow keeps an explicit compatibility unit test slice" From e606e9616e58d3ae5406b239d9b013db55640f50 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:02:02 -0700 Subject: [PATCH 13/23] Retry CI jobs only before real work starts --- .../actions/run-unit-test-shard/action.yml | 15 + .github/workflows/build-ghosttykit.yml | 138 ++++++- .github/workflows/ci.yml | 373 +++++++++++++++++- tests/test_ci_self_hosted_guard.sh | 286 +++++--------- 4 files changed, 625 insertions(+), 187 deletions(-) diff --git a/.github/actions/run-unit-test-shard/action.yml b/.github/actions/run-unit-test-shard/action.yml index 53584045877b..56261c44d0d6 100644 --- a/.github/actions/run-unit-test-shard/action.yml +++ b/.github/actions/run-unit-test-shard/action.yml @@ -11,6 +11,13 @@ inputs: 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 }} + passed: + description: Whether the shard passed after expected-failure handling. + value: ${{ steps.run-tests.outputs.passed }} runs: using: composite steps: @@ -92,7 +99,13 @@ runs: 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 }} @@ -153,3 +166,5 @@ runs: exit 1 fi fi + + echo "passed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-ghosttykit.yml b/.github/workflows/build-ghosttykit.yml index 80850f500f4d..5a73dd7369fd 100644 --- a/.github/workflows/build-ghosttykit.yml +++ b/.github/workflows/build-ghosttykit.yml @@ -7,11 +7,14 @@ on: 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 }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -58,6 +61,11 @@ 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: | @@ -98,3 +106,131 @@ jobs: --notes "Pre-built GhosttyKit.xcframework for commit ${{ steps.ghostty-sha.outputs.sha }}" \ GhosttyKit.xcframework.tar.gz echo "Published release $TAG" + + 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.result != 'success' && needs.build-ghosttykit-attempt-1.outputs.build_started != '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 }} + 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 + 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 + 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 + 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" + + 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: Verify GhosttyKit build passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.build-ghosttykit-attempt-1.result }}" + ATTEMPT_1_BUILD_STARTED="${{ needs.build-ghosttykit-attempt-1.outputs.build_started }}" + ATTEMPT_2_RESULT="${{ needs.build-ghosttykit-attempt-2.result }}" + ATTEMPT_2_BUILD_STARTED="${{ needs.build-ghosttykit-attempt-2.outputs.build_started }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "GhosttyKit build passed." + exit 0 + fi + + echo "GhosttyKit build did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, build_started=${ATTEMPT_1_BUILD_STARTED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, build_started=${ATTEMPT_2_BUILD_STARTED:-}" >&2 + exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e46bb581e9c6..10db4137a7ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,17 +73,22 @@ jobs: - name: Typecheck run: bun tsc --noEmit - tests-shard-1: + 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: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: "0" @@ -110,81 +115,433 @@ jobs: CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py - tests-shard-2: + 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.result != 'success' && needs.tests-shard-1-attempt-1.outputs.test_started != 'true' }} + needs: + - tests-shard-1-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: "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 + + 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-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: Verify unit test shard passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-shard-1-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-1-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-shard-1-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-shard-1-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-1-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-shard-1-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "Unit test shard passed." + exit 0 + fi + + echo "Unit test shard did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + + 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: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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-3: + 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.result != 'success' && needs.tests-shard-2-attempt-1.outputs.test_started != 'true' }} + needs: + - tests-shard-2-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: Verify unit test shard passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-shard-2-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-2-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-shard-2-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-shard-2-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-2-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-shard-2-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "Unit test shard passed." + exit 0 + fi + + echo "Unit test shard did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + + 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: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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-4: + 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.result != 'success' && needs.tests-shard-3-attempt-1.outputs.test_started != 'true' }} + needs: + - tests-shard-3-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: Verify unit test shard passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-shard-3-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-3-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-shard-3-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-shard-3-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-3-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-shard-3-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "Unit test shard passed." + exit 0 + fi + + echo "Unit test shard did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + + 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: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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-5: + 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.result != 'success' && needs.tests-shard-4-attempt-1.outputs.test_started != 'true' }} + needs: + - tests-shard-4-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: Verify unit test shard passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-shard-4-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-4-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-shard-4-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-shard-4-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-4-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-shard-4-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "Unit test shard passed." + exit 0 + fi + + echo "Unit test shard did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + + 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: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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-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.result != 'success' && needs.tests-shard-5-attempt-1.outputs.test_started != 'true' }} + needs: + - tests-shard-5-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: Verify unit test shard passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-shard-5-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-5-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-shard-5-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-shard-5-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-5-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-shard-5-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "Unit test shard passed." + exit 0 + fi + + echo "Unit test shard did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + + 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: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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.result != 'success' && needs.tests-shard-6-attempt-1.outputs.test_started != 'true' }} + needs: + - tests-shard-6-attempt-1 + continue-on-error: true + runs-on: warp-macos-15-arm64-6x + timeout-minutes: 25 + outputs: + test_started: ${{ steps.unit-test-shard.outputs.test_started }} + 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: Verify unit test shard passed + run: | + set -euo pipefail + + ATTEMPT_1_RESULT="${{ needs.tests-shard-6-attempt-1.result }}" + ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-6-attempt-1.outputs.test_started }}" + ATTEMPT_1_PASSED="${{ needs.tests-shard-6-attempt-1.outputs.passed }}" + ATTEMPT_2_RESULT="${{ needs.tests-shard-6-attempt-2.result }}" + ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-6-attempt-2.outputs.test_started }}" + ATTEMPT_2_PASSED="${{ needs.tests-shard-6-attempt-2.outputs.passed }}" + + if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + echo "Unit test shard passed." + exit 0 + fi + + echo "Unit test shard did not pass." >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + exit 1 + tests: if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} needs: @@ -249,7 +606,7 @@ jobs: 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.result == 'cancelled' && needs.tests-build-and-lag-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-build-and-lag-attempt-1.result != 'success' && needs.tests-build-and-lag-attempt-1.outputs.test_started != 'true' }} needs: - tests-build-and-lag-attempt-1 continue-on-error: true @@ -314,7 +671,7 @@ jobs: 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.result == 'cancelled' && needs.ui-display-resolution-regression-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.ui-display-resolution-regression-attempt-1.result != 'success' && needs.ui-display-resolution-regression-attempt-1.outputs.test_started != 'true' }} needs: - ui-display-resolution-regression-attempt-1 continue-on-error: true diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index a527a0b57e93..2bfdc08335ba 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -4,183 +4,113 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci.yml" - +CI_WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci.yml" +BUILD_WORKFLOW_FILE="$ROOT_DIR/.github/workflows/build-ghosttykit.yml" EXPECTED_IF="if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository" -if ! grep -Fq "$EXPECTED_IF" "$WORKFLOW_FILE"; then - echo "FAIL: Missing fork pull_request guard in $WORKFLOW_FILE" - echo "Expected line:" - echo " $EXPECTED_IF" - exit 1 -fi - -# tests-shard-1: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-shard-1:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-shard-1 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-shard-2: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-shard-2:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-shard-2 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-shard-3: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-shard-3:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-shard-3 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-shard-4: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-shard-4:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-shard-4 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-shard-5: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-shard-5:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-shard-5 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-shard-6: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-shard-6:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-shard-6 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests wrapper: should stay on hosted Linux because it only aggregates shard outputs. -if ! awk ' - /^ tests:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: ubuntu-latest/ { saw_hosted=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_hosted && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests wrapper must keep both ubuntu-latest runner and fork guard" - exit 1 -fi - -# tests-build-and-lag-attempt-1: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-build-and-lag-attempt-1:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-build-and-lag-attempt-1 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-build-and-lag-attempt-2: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ tests-build-and-lag-attempt-2:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-build-and-lag-attempt-2 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# tests-build-and-lag wrapper: should stay on hosted Linux because it only aggregates attempt outputs. -if ! awk ' - /^ tests-build-and-lag:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: ubuntu-latest/ { saw_hosted=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_hosted && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: tests-build-and-lag wrapper must keep both ubuntu-latest runner and fork guard" - exit 1 -fi - -# ui-display-resolution-regression-attempt-1: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ ui-display-resolution-regression-attempt-1:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: ui-display-resolution-regression-attempt-1 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# ui-display-resolution-regression-attempt-2: must use WarpBuild runner with fork guard (paid runner) -if ! awk ' - /^ ui-display-resolution-regression-attempt-2:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: warp-macos-15-arm64-6x/ { saw_warp=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_warp && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: ui-display-resolution-regression-attempt-2 block must keep both warp-macos-15-arm64-6x runner and fork guard" - exit 1 -fi - -# ui-display-resolution-regression wrapper: should stay on hosted Linux because it only aggregates attempt outputs. -if ! awk ' - /^ ui-display-resolution-regression:/ { in_tests=1; next } - in_tests && /^ [^[:space:]]/ { in_tests=0 } - in_tests && /runs-on: ubuntu-latest/ { saw_hosted=1 } - in_tests && /github.event.pull_request.head.repo.full_name == github.repository/ { saw_guard=1 } - END { exit !(saw_hosted && saw_guard) } -' "$WORKFLOW_FILE"; then - echo "FAIL: ui-display-resolution-regression wrapper must keep both ubuntu-latest runner and fork guard" - exit 1 -fi - -echo "PASS: tests-shard-1 WarpBuild runner fork guard is present" -echo "PASS: tests-shard-2 WarpBuild runner fork guard is present" -echo "PASS: tests-shard-3 WarpBuild runner fork guard is present" -echo "PASS: tests-shard-4 WarpBuild runner fork guard is present" -echo "PASS: tests-shard-5 WarpBuild runner fork guard is present" -echo "PASS: tests-shard-6 WarpBuild runner fork guard is present" -echo "PASS: tests wrapper hosted runner guard is present" -echo "PASS: tests-build-and-lag attempt-1 WarpBuild runner fork guard is present" -echo "PASS: tests-build-and-lag attempt-2 WarpBuild runner fork guard is present" -echo "PASS: tests-build-and-lag wrapper hosted runner guard is present" -echo "PASS: ui-display-resolution-regression attempt-1 WarpBuild runner fork guard is present" -echo "PASS: ui-display-resolution-regression attempt-2 WarpBuild runner fork guard is present" -echo "PASS: ui-display-resolution-regression wrapper hosted runner guard is present" +assert_workflow_guard() { + local workflow_file="$1" + if ! grep -Fq "$EXPECTED_IF" "$workflow_file"; then + echo "FAIL: Missing fork pull_request guard in $workflow_file" + echo "Expected line:" + echo " $EXPECTED_IF" + exit 1 + fi +} + +assert_job_runner_guard() { + local workflow_file="$1" + local job_name="$2" + local runner_label="$3" + local failure_message="$4" + + if ! awk -v job_header=" ${job_name}:" -v runner_line="runs-on: ${runner_label}" -v guard_text="github.event.pull_request.head.repo.full_name == github.repository" ' + $0 == job_header { in_job=1; next } + in_job && /^ [^[:space:]]/ { in_job=0 } + in_job && index($0, runner_line) { saw_runner=1 } + in_job && index($0, guard_text) { saw_guard=1 } + END { exit !(saw_runner && saw_guard) } + ' "$workflow_file"; then + echo "FAIL: $failure_message" + exit 1 + fi +} + +assert_workflow_guard "$CI_WORKFLOW_FILE" +assert_workflow_guard "$BUILD_WORKFLOW_FILE" + +warp_ci_jobs=( + tests-shard-1-attempt-1 + tests-shard-1-attempt-2 + tests-shard-2-attempt-1 + tests-shard-2-attempt-2 + tests-shard-3-attempt-1 + tests-shard-3-attempt-2 + tests-shard-4-attempt-1 + tests-shard-4-attempt-2 + tests-shard-5-attempt-1 + tests-shard-5-attempt-2 + tests-shard-6-attempt-1 + tests-shard-6-attempt-2 + tests-build-and-lag-attempt-1 + tests-build-and-lag-attempt-2 + ui-display-resolution-regression-attempt-1 + ui-display-resolution-regression-attempt-2 +) + +hosted_ci_jobs=( + tests-shard-1 + tests-shard-2 + tests-shard-3 + tests-shard-4 + tests-shard-5 + tests-shard-6 + tests + tests-build-and-lag + ui-display-resolution-regression +) + +warp_build_jobs=( + build-ghosttykit-attempt-1 + build-ghosttykit-attempt-2 +) + +hosted_build_jobs=( + build-ghosttykit +) + +for job_name in "${warp_ci_jobs[@]}"; do + assert_job_runner_guard \ + "$CI_WORKFLOW_FILE" \ + "$job_name" \ + "warp-macos-15-arm64-6x" \ + "$job_name block must keep both warp-macos-15-arm64-6x runner and fork guard" + echo "PASS: $job_name WarpBuild runner fork guard is present" +done + +for job_name in "${hosted_ci_jobs[@]}"; do + assert_job_runner_guard \ + "$CI_WORKFLOW_FILE" \ + "$job_name" \ + "ubuntu-latest" \ + "$job_name block must keep both ubuntu-latest runner and fork guard" + echo "PASS: $job_name hosted runner guard is present" +done + +for job_name in "${warp_build_jobs[@]}"; do + assert_job_runner_guard \ + "$BUILD_WORKFLOW_FILE" \ + "$job_name" \ + "warp-macos-15-arm64-6x" \ + "$job_name block must keep both warp-macos-15-arm64-6x runner and fork guard" + echo "PASS: $job_name WarpBuild runner fork guard is present" +done + +for job_name in "${hosted_build_jobs[@]}"; do + assert_job_runner_guard \ + "$BUILD_WORKFLOW_FILE" \ + "$job_name" \ + "ubuntu-latest" \ + "$job_name block must keep both ubuntu-latest runner and fork guard" + echo "PASS: $job_name hosted runner guard is present" +done From 13f27f5128ceb111d8883382b9eacad49a578f12 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:03:33 -0700 Subject: [PATCH 14/23] Tighten compat workflow guard coverage --- tests/test_ci_macos_compat_test_slice.sh | 32 +++++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_ci_macos_compat_test_slice.sh b/tests/test_ci_macos_compat_test_slice.sh index e09886edcb2e..c8d73fe0e691 100755 --- a/tests/test_ci_macos_compat_test_slice.sh +++ b/tests/test_ci_macos_compat_test_slice.sh @@ -3,6 +3,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci-macos-compat.yml" +STEP_BLOCK="$( + sed -n '/^ - name: Run compatibility unit test slice$/,/^ - name:/p' "$WORKFLOW_FILE" | sed '$d' +)" require_pattern() { local pattern="$1" @@ -25,9 +28,30 @@ reject_pattern() { require_pattern "compat_test_filters:" "ci-macos-compat.yml must define explicit compatibility test filters" require_pattern "cmuxTests/AppDelegateLaunchServicesRegistrationTests" "macOS 15 compat filters are missing" require_pattern "cmuxTests/ZshShellIntegrationHandoffTests" "macOS 26 compat filters are missing" -require_pattern "while IFS= read -r filter; do" "compat step must still load test filters into an array" -require_pattern "-scheme cmux-unit" "compat step must run the cmux-unit scheme" -require_pattern "test 2>&1" "compat step must execute unit tests" -reject_pattern "scripts/unit_test_shard_args.py" "compat workflow must not rely on shard-based test selection" + +if [ -z "$STEP_BLOCK" ]; then + echo "FAIL: compat workflow is missing the compatibility test-slice step" + exit 1 +fi + +if ! grep -q -- "while IFS= read -r filter; do" <<<"$STEP_BLOCK"; then + echo "FAIL: compat step must still load test filters into an array" + exit 1 +fi + +if ! grep -q -- "-scheme cmux-unit" <<<"$STEP_BLOCK"; then + echo "FAIL: compat step must run the cmux-unit scheme" + exit 1 +fi + +if ! grep -q -- "test 2>&1" <<<"$STEP_BLOCK"; then + echo "FAIL: compat step must execute unit tests" + exit 1 +fi + +if grep -q -- "scripts/unit_test_shard_args.py" <<<"$STEP_BLOCK"; then + echo "FAIL: compat workflow must not rely on shard-based test selection inside the compat step" + exit 1 +fi echo "PASS: macOS compat workflow keeps an explicit compatibility unit test slice" From 6cac9a2b34f5abddfd06bd521212fb5107c8ca23 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:15:00 -0700 Subject: [PATCH 15/23] test(ci): guard unit shard hang timeout --- .github/workflows/ci.yml | 3 +++ tests/test_ci_unit_shard_timeout.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/test_ci_unit_shard_timeout.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10db4137a7ac..ee4441fb7e37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ 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: ./tests/test_ci_unit_shard_timeout.sh + - name: Validate cmux scheme test configuration run: ./tests/test_ci_scheme_testaction_debug.sh diff --git a/tests/test_ci_unit_shard_timeout.sh b/tests/test_ci_unit_shard_timeout.sh new file mode 100644 index 000000000000..d2935446a3af --- /dev/null +++ b/tests/test_ci_unit_shard_timeout.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Regression test for bounded WarpBuild unit-shard hangs in CI. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CI_WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci.yml" + +assert_job_timeout() { + local job_name="$1" + local timeout_minutes="$2" + + if ! awk -v job_header=" ${job_name}:" -v timeout_line="timeout-minutes: ${timeout_minutes}" ' + $0 == job_header { in_job=1; next } + in_job && /^ [^[:space:]]/ { in_job=0 } + in_job && index($0, timeout_line) { found=1 } + END { exit !found } + ' "$CI_WORKFLOW_FILE"; then + echo "FAIL: ${job_name} must keep timeout-minutes: ${timeout_minutes}" + exit 1 + fi +} + +for shard in 1 2 3 4 5 6; do + for attempt in 1 2; do + assert_job_timeout "tests-shard-${shard}-attempt-${attempt}" "10" + echo "PASS: tests-shard-${shard}-attempt-${attempt} timeout is bounded to 10 minutes" + done +done From e13a86d3fabed6d86a2bb47603d2b6f7eb40ce24 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:15:28 -0700 Subject: [PATCH 16/23] fix(ci): cap unit shard hang duration --- .github/workflows/ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee4441fb7e37..651f0ed4f584 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: 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: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -124,7 +124,7 @@ jobs: - tests-shard-1-attempt-1 continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -194,7 +194,7 @@ jobs: 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: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -216,7 +216,7 @@ jobs: - tests-shard-2-attempt-1 continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -265,7 +265,7 @@ jobs: 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: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -287,7 +287,7 @@ jobs: - tests-shard-3-attempt-1 continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -336,7 +336,7 @@ jobs: 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: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -358,7 +358,7 @@ jobs: - tests-shard-4-attempt-1 continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -407,7 +407,7 @@ jobs: 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: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -429,7 +429,7 @@ jobs: - tests-shard-5-attempt-1 continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -478,7 +478,7 @@ jobs: 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: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} @@ -500,7 +500,7 @@ jobs: - tests-shard-6-attempt-1 continue-on-error: true runs-on: warp-macos-15-arm64-6x - timeout-minutes: 25 + timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} passed: ${{ steps.unit-test-shard.outputs.passed }} From 948356a4e8743b6e5fae9b914074c34e60d85c7e Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:21:50 -0700 Subject: [PATCH 17/23] test(ci): guard retry output gating --- .github/workflows/ci.yml | 5 +- tests/test_ci_retry_output_gating.sh | 121 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/test_ci_retry_output_gating.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 651f0ed4f584..cdfc626953d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,10 @@ jobs: run: ./tests/test_ci_unit_test_spm_retry.sh - name: Validate unit-test shard timeout guard - run: ./tests/test_ci_unit_shard_timeout.sh + 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 diff --git a/tests/test_ci_retry_output_gating.sh b/tests/test_ci_retry_output_gating.sh new file mode 100644 index 000000000000..c22d843e11d9 --- /dev/null +++ b/tests/test_ci_retry_output_gating.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Regression test for retry and wrapper logic on continue-on-error CI jobs. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CI_WORKFLOW_FILE="$ROOT_DIR/.github/workflows/ci.yml" +BUILD_WORKFLOW_FILE="$ROOT_DIR/.github/workflows/build-ghosttykit.yml" + +extract_job_block() { + local workflow_file="$1" + local job_name="$2" + + awk -v job_header=" ${job_name}:" ' + $0 == job_header { in_job=1; print; next } + in_job && /^ [^[:space:]]/ { exit } + in_job { print } + ' "$workflow_file" +} + +assert_job_if_uses_outputs_only() { + local workflow_file="$1" + local job_name="$2" + local required_output="$3" + local block + local if_line + + block="$(extract_job_block "$workflow_file" "$job_name")" + if [ -z "$block" ]; then + echo "FAIL: missing job block for $job_name" + exit 1 + fi + + if_line="$(printf '%s\n' "$block" | grep -m1 '^[[:space:]]*if:')" + if [ -z "$if_line" ]; then + echo "FAIL: missing if: line for $job_name" + exit 1 + fi + + if [[ "$if_line" != *"$required_output"* ]]; then + echo "FAIL: $job_name retry guard must key off $required_output" + exit 1 + fi + + if [[ "$if_line" == *".result"* ]]; then + echo "FAIL: $job_name retry guard must not depend on needs..result" + exit 1 + fi +} + +assert_job_block_contains() { + local workflow_file="$1" + local job_name="$2" + local pattern="$3" + local message="$4" + local block + + block="$(extract_job_block "$workflow_file" "$job_name")" + if [ -z "$block" ]; then + echo "FAIL: missing job block for $job_name" + exit 1 + fi + + if ! printf '%s\n' "$block" | grep -Fq -- "$pattern"; then + echo "FAIL: $message" + exit 1 + fi +} + +for shard in 1 2 3 4 5 6; do + assert_job_if_uses_outputs_only \ + "$CI_WORKFLOW_FILE" \ + "tests-shard-${shard}-attempt-2" \ + "outputs.test_started != 'true'" + assert_job_block_contains \ + "$CI_WORKFLOW_FILE" \ + "tests-shard-${shard}" \ + 'if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then' \ + "tests-shard-${shard} wrapper must key success off passed outputs" +done + +assert_job_if_uses_outputs_only \ + "$CI_WORKFLOW_FILE" \ + "tests-build-and-lag-attempt-2" \ + "outputs.test_started != 'true'" +assert_job_block_contains \ + "$CI_WORKFLOW_FILE" \ + "tests-build-and-lag" \ + 'if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then' \ + "tests-build-and-lag wrapper must key success off passed outputs" + +assert_job_if_uses_outputs_only \ + "$CI_WORKFLOW_FILE" \ + "ui-display-resolution-regression-attempt-2" \ + "outputs.test_started != 'true'" +assert_job_block_contains \ + "$CI_WORKFLOW_FILE" \ + "ui-display-resolution-regression" \ + 'if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then' \ + "ui-display-resolution-regression wrapper must key success off passed outputs" + +assert_job_if_uses_outputs_only \ + "$BUILD_WORKFLOW_FILE" \ + "build-ghosttykit-attempt-2" \ + "outputs.build_started != 'true'" +assert_job_block_contains \ + "$BUILD_WORKFLOW_FILE" \ + "build-ghosttykit-attempt-1" \ + 'passed: ${{ steps.mark-build-pass.outputs.value }}' \ + "build-ghosttykit-attempt-1 must expose a passed output" +assert_job_block_contains \ + "$BUILD_WORKFLOW_FILE" \ + "build-ghosttykit-attempt-2" \ + 'passed: ${{ steps.mark-build-pass.outputs.value }}' \ + "build-ghosttykit-attempt-2 must expose a passed output" +assert_job_block_contains \ + "$BUILD_WORKFLOW_FILE" \ + "build-ghosttykit" \ + 'if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then' \ + "build-ghosttykit wrapper must key success off passed outputs" + +echo "PASS: retry and wrapper guards use explicit outputs, not needs..result" From 1e5405a037b52ddee3819a3b85e7cfe3e157cbe9 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:23:02 -0700 Subject: [PATCH 18/23] fix(ci): trust explicit retry outputs --- .github/workflows/build-ghosttykit.yml | 20 ++++++++++++++---- .github/workflows/ci.yml | 28 +++++++++++++------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-ghosttykit.yml b/.github/workflows/build-ghosttykit.yml index 5a73dd7369fd..621475274999 100644 --- a/.github/workflows/build-ghosttykit.yml +++ b/.github/workflows/build-ghosttykit.yml @@ -15,6 +15,7 @@ jobs: timeout-minutes: 20 outputs: build_started: ${{ steps.mark-build-start.outputs.value }} + passed: ${{ steps.mark-build-pass.outputs.value }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -107,8 +108,12 @@ jobs: GhosttyKit.xcframework.tar.gz echo "Published release $TAG" + - 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.result != 'success' && needs.build-ghosttykit-attempt-1.outputs.build_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.build-ghosttykit-attempt-1.outputs.build_started != 'true' }} needs: - build-ghosttykit-attempt-1 continue-on-error: true @@ -116,6 +121,7 @@ jobs: timeout-minutes: 20 outputs: build_started: ${{ steps.mark-build-start.outputs.value }} + passed: ${{ steps.mark-build-pass.outputs.value }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -208,6 +214,10 @@ jobs: GhosttyKit.xcframework.tar.gz echo "Published release $TAG" + - 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: @@ -222,15 +232,17 @@ jobs: ATTEMPT_1_RESULT="${{ needs.build-ghosttykit-attempt-1.result }}" ATTEMPT_1_BUILD_STARTED="${{ needs.build-ghosttykit-attempt-1.outputs.build_started }}" + ATTEMPT_1_PASSED="${{ needs.build-ghosttykit-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.build-ghosttykit-attempt-2.result }}" ATTEMPT_2_BUILD_STARTED="${{ needs.build-ghosttykit-attempt-2.outputs.build_started }}" + ATTEMPT_2_PASSED="${{ needs.build-ghosttykit-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "GhosttyKit build passed." exit 0 fi echo "GhosttyKit build did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, build_started=${ATTEMPT_1_BUILD_STARTED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, build_started=${ATTEMPT_2_BUILD_STARTED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, build_started=${ATTEMPT_1_BUILD_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, build_started=${ATTEMPT_2_BUILD_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdfc626953d5..d2950accd39c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py 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.result != 'success' && needs.tests-shard-1-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-1-attempt-1.outputs.test_started != 'true' }} needs: - tests-shard-1-attempt-1 continue-on-error: true @@ -183,7 +183,7 @@ jobs: ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-1-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-1-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "Unit test shard passed." exit 0 fi @@ -214,7 +214,7 @@ jobs: 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.result != 'success' && needs.tests-shard-2-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-2-attempt-1.outputs.test_started != 'true' }} needs: - tests-shard-2-attempt-1 continue-on-error: true @@ -254,7 +254,7 @@ jobs: ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-2-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-2-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "Unit test shard passed." exit 0 fi @@ -285,7 +285,7 @@ jobs: 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.result != 'success' && needs.tests-shard-3-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-3-attempt-1.outputs.test_started != 'true' }} needs: - tests-shard-3-attempt-1 continue-on-error: true @@ -325,7 +325,7 @@ jobs: ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-3-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-3-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "Unit test shard passed." exit 0 fi @@ -356,7 +356,7 @@ jobs: 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.result != 'success' && needs.tests-shard-4-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-4-attempt-1.outputs.test_started != 'true' }} needs: - tests-shard-4-attempt-1 continue-on-error: true @@ -396,7 +396,7 @@ jobs: ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-4-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-4-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "Unit test shard passed." exit 0 fi @@ -427,7 +427,7 @@ jobs: 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.result != 'success' && needs.tests-shard-5-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-5-attempt-1.outputs.test_started != 'true' }} needs: - tests-shard-5-attempt-1 continue-on-error: true @@ -467,7 +467,7 @@ jobs: ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-5-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-5-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "Unit test shard passed." exit 0 fi @@ -498,7 +498,7 @@ jobs: 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.result != 'success' && needs.tests-shard-6-attempt-1.outputs.test_started != 'true' }} + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.tests-shard-6-attempt-1.outputs.test_started != 'true' }} needs: - tests-shard-6-attempt-1 continue-on-error: true @@ -538,7 +538,7 @@ jobs: ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-6-attempt-2.outputs.test_started }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-6-attempt-2.outputs.passed }}" - if [ "$ATTEMPT_1_RESULT" = "success" ] || [ "$ATTEMPT_2_RESULT" = "success" ]; then + if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then echo "Unit test shard passed." exit 0 fi @@ -612,7 +612,7 @@ jobs: 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.result != 'success' && needs.tests-build-and-lag-attempt-1.outputs.test_started != 'true' }} + 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.test_started != 'true' }} needs: - tests-build-and-lag-attempt-1 continue-on-error: true @@ -677,7 +677,7 @@ jobs: 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.result != 'success' && needs.ui-display-resolution-regression-attempt-1.outputs.test_started != 'true' }} + 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.test_started != 'true' }} needs: - ui-display-resolution-regression-attempt-1 continue-on-error: true From b153299041311a42c52c3dd61534f02f9fcf6a02 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 10:24:49 -0700 Subject: [PATCH 19/23] fix(test): tighten unit shard timeout guard --- tests/test_ci_unit_shard_timeout.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_ci_unit_shard_timeout.sh b/tests/test_ci_unit_shard_timeout.sh index d2935446a3af..b4c4549617ed 100644 --- a/tests/test_ci_unit_shard_timeout.sh +++ b/tests/test_ci_unit_shard_timeout.sh @@ -12,7 +12,13 @@ assert_job_timeout() { if ! awk -v job_header=" ${job_name}:" -v timeout_line="timeout-minutes: ${timeout_minutes}" ' $0 == job_header { in_job=1; next } in_job && /^ [^[:space:]]/ { in_job=0 } - in_job && index($0, timeout_line) { found=1 } + in_job { + line=$0 + sub(/^[[:space:]]+/, "", line) + if (line == timeout_line) { + found=1 + } + } END { exit !found } ' "$CI_WORKFLOW_FILE"; then echo "FAIL: ${job_name} must keep timeout-minutes: ${timeout_minutes}" From 120aff4ad1182d4c7ffe8d0f88605972ed780eaf Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 11:45:21 -0700 Subject: [PATCH 20/23] Retry cancelled CI attempts deterministically --- .../action.yml | 10 ++ .github/actions/run-lag-regression/action.yml | 10 ++ .../actions/run-unit-test-shard/action.yml | 5 + .github/workflows/build-ghosttykit.yml | 48 +++---- .github/workflows/ci.yml | 100 ++++++++++---- scripts/verify_retry_result.sh | 25 ++++ tests/test_ci_macos_compat_test_slice.sh | 44 +++++-- tests/test_ci_retry_output_gating.sh | 122 ++++++++++++------ 8 files changed, 252 insertions(+), 112 deletions(-) create mode 100755 scripts/verify_retry_result.sh mode change 100644 => 100755 tests/test_ci_retry_output_gating.sh diff --git a/.github/actions/run-display-resolution-regression/action.yml b/.github/actions/run-display-resolution-regression/action.yml index f0317a2e1159..c3e51c127d81 100644 --- a/.github/actions/run-display-resolution-regression/action.yml +++ b/.github/actions/run-display-resolution-regression/action.yml @@ -4,6 +4,9 @@ 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 }} @@ -97,12 +100,19 @@ runs: 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() }} diff --git a/.github/actions/run-lag-regression/action.yml b/.github/actions/run-lag-regression/action.yml index 84d209104d62..0e95b9352055 100644 --- a/.github/actions/run-lag-regression/action.yml +++ b/.github/actions/run-lag-regression/action.yml @@ -4,6 +4,9 @@ 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 }} @@ -142,6 +145,7 @@ runs: 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 \ @@ -152,6 +156,12 @@ runs: 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() }} diff --git a/.github/actions/run-unit-test-shard/action.yml b/.github/actions/run-unit-test-shard/action.yml index 56261c44d0d6..d816a56a0ae4 100644 --- a/.github/actions/run-unit-test-shard/action.yml +++ b/.github/actions/run-unit-test-shard/action.yml @@ -15,6 +15,9 @@ 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 }} @@ -157,6 +160,8 @@ runs: 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 diff --git a/.github/workflows/build-ghosttykit.yml b/.github/workflows/build-ghosttykit.yml index 621475274999..67a157998178 100644 --- a/.github/workflows/build-ghosttykit.yml +++ b/.github/workflows/build-ghosttykit.yml @@ -15,6 +15,7 @@ jobs: 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 @@ -71,19 +72,7 @@ jobs: 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" - 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 + ./scripts/install-zig.sh cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast - name: Package xcframework @@ -108,12 +97,17 @@ jobs: 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.build_started != 'true' }} + 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 @@ -121,6 +115,7 @@ jobs: 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 @@ -177,19 +172,7 @@ jobs: 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" - 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 + ./scripts/install-zig.sh cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast - name: Package xcframework @@ -214,6 +197,11 @@ jobs: 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" @@ -232,9 +220,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.build-ghosttykit-attempt-1.result }}" ATTEMPT_1_BUILD_STARTED="${{ needs.build-ghosttykit-attempt-1.outputs.build_started }}" + ATTEMPT_1_COMPLETED="${{ needs.build-ghosttykit-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.build-ghosttykit-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.build-ghosttykit-attempt-2.result }}" ATTEMPT_2_BUILD_STARTED="${{ needs.build-ghosttykit-attempt-2.outputs.build_started }}" + ATTEMPT_2_COMPLETED="${{ needs.build-ghosttykit-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.build-ghosttykit-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -243,6 +233,6 @@ jobs: fi echo "GhosttyKit build did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, build_started=${ATTEMPT_1_BUILD_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, build_started=${ATTEMPT_2_BUILD_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, build_started=${ATTEMPT_1_BUILD_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, build_started=${ATTEMPT_2_BUILD_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2950accd39c..71f84c7145b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,8 @@ jobs: timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} - passed: ${{ steps.unit-test-shard.outputs.passed }} + completed: ${{ steps.mark-attempt-complete.outputs.value }} + passed: ${{ steps.mark-attempt-pass.outputs.value }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -120,9 +121,17 @@ jobs: 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.test_started != 'true' }} + 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 @@ -130,7 +139,8 @@ jobs: timeout-minutes: 10 outputs: test_started: ${{ steps.unit-test-shard.outputs.test_started }} - passed: ${{ steps.unit-test-shard.outputs.passed }} + completed: ${{ steps.mark-attempt-complete.outputs.value }} + passed: ${{ steps.mark-attempt-pass.outputs.value }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -163,6 +173,14 @@ jobs: 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: if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} @@ -178,9 +196,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-shard-1-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-1-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-shard-1-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-shard-1-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-shard-1-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-1-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-shard-1-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-1-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -189,8 +209,8 @@ jobs: fi echo "Unit test shard did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 tests-shard-2-attempt-1: @@ -200,6 +220,7 @@ jobs: 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 @@ -214,7 +235,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -222,6 +243,7 @@ jobs: 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 @@ -249,9 +271,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-shard-2-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-2-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-shard-2-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-shard-2-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-shard-2-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-2-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-shard-2-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-2-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -260,8 +284,8 @@ jobs: fi echo "Unit test shard did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 tests-shard-3-attempt-1: @@ -271,6 +295,7 @@ jobs: 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 @@ -285,7 +310,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -293,6 +318,7 @@ jobs: 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 @@ -320,9 +346,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-shard-3-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-3-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-shard-3-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-shard-3-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-shard-3-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-3-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-shard-3-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-3-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -331,8 +359,8 @@ jobs: fi echo "Unit test shard did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 tests-shard-4-attempt-1: @@ -342,6 +370,7 @@ jobs: 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 @@ -356,7 +385,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -364,6 +393,7 @@ jobs: 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 @@ -391,9 +421,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-shard-4-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-4-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-shard-4-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-shard-4-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-shard-4-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-4-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-shard-4-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-4-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -402,8 +434,8 @@ jobs: fi echo "Unit test shard did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 tests-shard-5-attempt-1: @@ -413,6 +445,7 @@ jobs: 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 @@ -427,7 +460,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -435,6 +468,7 @@ jobs: 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 @@ -462,9 +496,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-shard-5-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-5-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-shard-5-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-shard-5-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-shard-5-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-5-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-shard-5-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-5-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -473,8 +509,8 @@ jobs: fi echo "Unit test shard did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 tests-shard-6-attempt-1: @@ -484,6 +520,7 @@ jobs: 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 @@ -498,7 +535,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -506,6 +543,7 @@ jobs: 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 @@ -533,9 +571,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-shard-6-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-shard-6-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-shard-6-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-shard-6-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-shard-6-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-shard-6-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-shard-6-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-shard-6-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -544,8 +584,8 @@ jobs: fi echo "Unit test shard did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 tests: @@ -601,6 +641,7 @@ jobs: 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 @@ -612,7 +653,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -620,6 +661,7 @@ jobs: 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 @@ -644,9 +686,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.tests-build-and-lag-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.tests-build-and-lag-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.tests-build-and-lag-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.tests-build-and-lag-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.tests-build-and-lag-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.tests-build-and-lag-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.tests-build-and-lag-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.tests-build-and-lag-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -655,8 +699,8 @@ jobs: fi echo "Lag regression did not pass." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 ui-display-resolution-regression-attempt-1: @@ -666,6 +710,7 @@ jobs: 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 @@ -677,7 +722,7 @@ jobs: 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.test_started != 'true' }} + 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 @@ -685,6 +730,7 @@ jobs: 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 @@ -709,9 +755,11 @@ jobs: ATTEMPT_1_RESULT="${{ needs.ui-display-resolution-regression-attempt-1.result }}" ATTEMPT_1_TEST_STARTED="${{ needs.ui-display-resolution-regression-attempt-1.outputs.test_started }}" + ATTEMPT_1_COMPLETED="${{ needs.ui-display-resolution-regression-attempt-1.outputs.completed }}" ATTEMPT_1_PASSED="${{ needs.ui-display-resolution-regression-attempt-1.outputs.passed }}" ATTEMPT_2_RESULT="${{ needs.ui-display-resolution-regression-attempt-2.result }}" ATTEMPT_2_TEST_STARTED="${{ needs.ui-display-resolution-regression-attempt-2.outputs.test_started }}" + ATTEMPT_2_COMPLETED="${{ needs.ui-display-resolution-regression-attempt-2.outputs.completed }}" ATTEMPT_2_PASSED="${{ needs.ui-display-resolution-regression-attempt-2.outputs.passed }}" if [ "$ATTEMPT_1_PASSED" = "true" ] || [ "$ATTEMPT_2_PASSED" = "true" ]; then @@ -720,6 +768,6 @@ jobs: fi echo "Display resolution regression did not pass in either WarpBuild attempt." >&2 - echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 - echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 + echo "Attempt 1: result=$ATTEMPT_1_RESULT, test_started=${ATTEMPT_1_TEST_STARTED:-}, completed=${ATTEMPT_1_COMPLETED:-}, passed=${ATTEMPT_1_PASSED:-}" >&2 + echo "Attempt 2: result=$ATTEMPT_2_RESULT, test_started=${ATTEMPT_2_TEST_STARTED:-}, completed=${ATTEMPT_2_COMPLETED:-}, passed=${ATTEMPT_2_PASSED:-}" >&2 exit 1 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