Repository navigation
Add macOS compatibility CI: unit tests + smoke test on macos-14/15 #769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d3f4124
62db3d1
b2333bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| name: macOS Compatibility | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| compat-tests: | ||
| # Only run for the repo itself, not forks (GhosttyKit download needs repo access). | ||
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [macos-14, macos-15] | ||
| runs-on: ${{ matrix.os }} | ||
| 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 | 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 | ||
| xcrun --sdk macosx --show-sdk-path | ||
| sw_vers | ||
|
|
||
| - name: Download pre-built GhosttyKit.xcframework | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| set -euo pipefail | ||
| GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD) | ||
| TAG="xcframework-$GHOSTTY_SHA" | ||
| URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz" | ||
| echo "Downloading xcframework for ghostty $GHOSTTY_SHA" | ||
| MAX_RETRIES=30 | ||
| RETRY_DELAY=20 | ||
| for i in $(seq 1 $MAX_RETRIES); do | ||
| if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then | ||
| echo "Download succeeded on attempt $i" | ||
| break | ||
| fi | ||
| if [ "$i" -eq "$MAX_RETRIES" ]; then | ||
| echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..." | ||
| sleep $RETRY_DELAY | ||
| done | ||
| tar xzf GhosttyKit.xcframework.tar.gz | ||
| rm GhosttyKit.xcframework.tar.gz | ||
| test -d GhosttyKit.xcframework | ||
|
|
||
| - name: Clean DerivedData | ||
| run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* | ||
|
|
||
| - name: Resolve Swift packages | ||
| run: | | ||
| set -euo pipefail | ||
| SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" | ||
| rm -rf "$SOURCE_PACKAGES_DIR" | ||
| 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 | ||
| } | ||
|
|
||
| set +e | ||
| OUTPUT=$(run_unit_tests) | ||
| EXIT_CODE=$? | ||
| 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 | ||
| 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 | ||
|
|
||
| - name: Create virtual display | ||
| run: | | ||
| set -euo pipefail | ||
| echo "=== Display before ===" | ||
| system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)" | ||
| echo "" | ||
| clang -framework Foundation -framework CoreGraphics \ | ||
| -o /tmp/create-virtual-display scripts/create-virtual-display.m | ||
| /tmp/create-virtual-display & | ||
| VDISPLAY_PID=$! | ||
| echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" | ||
| sleep 3 | ||
| echo "=== Display after ===" | ||
| system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)" | ||
|
|
||
| - name: Build app for smoke test | ||
| 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 | ||
| run: | | ||
| set -euo pipefail | ||
| chmod +x scripts/smoke-test-ci.sh | ||
| scripts/smoke-test-ci.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| #!/usr/bin/env bash | ||
| # Smoke test for CI: launch the app, send a command, verify it stays alive for 15 seconds. | ||
| set -euo pipefail | ||
|
|
||
| SOCKET_PATH="/tmp/cmux-debug.sock" | ||
| STABILITY_WAIT=15 | ||
|
|
||
| echo "=== Smoke Test ===" | ||
|
|
||
| # --- Find the built app --- | ||
| APP=$(find ~/Library/Developer/Xcode/DerivedData -path "*/Build/Products/Debug/cmux DEV.app" -print -quit 2>/dev/null || true) | ||
| if [ -z "$APP" ]; then | ||
| echo "ERROR: Built app not found in DerivedData" | ||
| exit 1 | ||
| fi | ||
| echo "App: $APP" | ||
|
|
||
| # --- Check display availability --- | ||
| if system_profiler SPDisplaysDataType 2>/dev/null | grep -q "Resolution"; then | ||
| echo "Display: available" | ||
| else | ||
| echo "WARNING: No display detected, app may fail to launch" | ||
| fi | ||
|
|
||
| # --- Clean up stale socket --- | ||
| rm -f "$SOCKET_PATH" | ||
|
|
||
| # --- Launch the app --- | ||
| echo "Launching app..." | ||
| open "$APP" --env CMUX_SOCKET_MODE=allowAll | ||
|
|
||
| # --- Wait for socket (up to 30s) --- | ||
| echo "Waiting for socket at $SOCKET_PATH..." | ||
| SOCKET_READY=false | ||
| for i in $(seq 1 60); do | ||
| if [ -S "$SOCKET_PATH" ]; then | ||
| echo "Socket ready after $((i / 2))s" | ||
| SOCKET_READY=true | ||
| break | ||
| fi | ||
| sleep 0.5 | ||
| done | ||
| if [ "$SOCKET_READY" != "true" ]; then | ||
| echo "ERROR: Socket not ready after 30s" | ||
| # Dump any crash logs | ||
| ls -la /tmp/cmux-debug* 2>/dev/null || true | ||
| pgrep -la "cmux" || echo "No cmux processes found" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # --- Ping the socket --- | ||
| echo "Pinging socket..." | ||
| PING_RESPONSE=$(python3 -c " | ||
| import socket | ||
| s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| s.connect('$SOCKET_PATH') | ||
| s.settimeout(5.0) | ||
| s.sendall(b'ping\n') | ||
| data = s.recv(1024).decode().strip() | ||
| s.close() | ||
| print(data) | ||
| ") | ||
| echo "Ping response: $PING_RESPONSE" | ||
| if [ "$PING_RESPONSE" != "PONG" ]; then | ||
| echo "ERROR: Expected PONG, got: $PING_RESPONSE" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # --- Send a command to the terminal --- | ||
| echo "Sending 'time' command to terminal..." | ||
| SEND_RESPONSE=$(python3 -c " | ||
| import socket | ||
| s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| s.connect('$SOCKET_PATH') | ||
| s.settimeout(5.0) | ||
| s.sendall(b'send time\\\n\n') | ||
| data = s.recv(1024).decode().strip() | ||
| s.close() | ||
| print(data) | ||
| ") | ||
| echo "Send response: $SEND_RESPONSE" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The smoke test captures Useful? React with 👍 / 👎. |
||
|
|
||
| # --- Get app PID --- | ||
| APP_PID=$(pgrep -x "cmux DEV" | head -1 || true) | ||
| if [ -z "$APP_PID" ]; then | ||
| echo "ERROR: App process not found" | ||
| exit 1 | ||
| fi | ||
| echo "App PID: $APP_PID" | ||
|
|
||
| # --- Wait and verify stability --- | ||
| echo "Waiting ${STABILITY_WAIT}s to verify stability..." | ||
| sleep "$STABILITY_WAIT" | ||
|
|
||
| if ! kill -0 "$APP_PID" 2>/dev/null; then | ||
| echo "ERROR: App crashed during ${STABILITY_WAIT}s stability check" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # --- Final ping --- | ||
| FINAL_PING=$(python3 -c " | ||
| import socket | ||
| s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| s.connect('$SOCKET_PATH') | ||
| s.settimeout(5.0) | ||
| s.sendall(b'ping\n') | ||
| data = s.recv(1024).decode().strip() | ||
| s.close() | ||
| print(data) | ||
| ") | ||
| echo "Final ping: $FINAL_PING" | ||
| if [ "$FINAL_PING" != "PONG" ]; then | ||
| echo "ERROR: App not responsive after ${STABILITY_WAIT}s" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "=== Smoke test passed ===" | ||
|
|
||
| # --- Cleanup --- | ||
| pkill -x "cmux DEV" 2>/dev/null || true | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Python bytes literal here sends
send time\followed by two real newlines, notsend time\\nplus the command terminator. InSources/TerminalController.swift(sendInput), only the literal sequence\nis converted to Enter, so this payload does not actually executetimein the terminal and the smoke test can pass without validating terminal command delivery.Useful? React with 👍 / 👎.