name: CI on: push: branches: - main pull_request: jobs: web-typecheck: runs-on: ubuntu-latest defaults: run: working-directory: web steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install --frozen-lockfile - name: Typecheck run: bun tsc --noEmit ui-tests: runs-on: self-hosted concurrency: group: self-hosted-build cancel-in-progress: false steps: - name: Checkout uses: actions/checkout@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 - name: Build GhosttyKit.xcframework run: | set -euo pipefail if ! command -v zig >/dev/null 2>&1; then if command -v brew >/dev/null 2>&1; then brew install zig else echo "zig is required to build GhosttyKit.xcframework. Install zig and retry." >&2 exit 1 fi fi (cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=native) rm -rf GhosttyKit.xcframework cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework test -d GhosttyKit.xcframework - name: Clean DerivedData run: | # Remove stale build cache to avoid incremental build errors rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* - name: Run UI tests run: | set -euo pipefail # Policy: run macOS UI tests in the UTM VM (never on the host runner). Host GUI # state is frequently non-interactive on CI and causes false failures. vm_user="${CMUX_VM_USER:-cmux}" ssh_run_opts=( -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=15 -o ConnectionAttempts=1 -o NumberOfPasswordPrompts=0 -o PreferredAuthentications=publickey -o PubkeyAuthentication=yes -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no -o ChallengeResponseAuthentication=no -o GSSAPIAuthentication=no -o LogLevel=ERROR ) # Faster opts for probing a bunch of hosts. ssh_check_opts=( "${ssh_run_opts[@]}" -o ConnectTimeout=2 -o ServerAliveInterval=2 -o ServerAliveCountMax=1 ) # Resolve a usable vm host. Prefer explicit env, then ssh config, then a known default. cfg_host="" if command -v ssh >/dev/null 2>&1; then cfg_host="$(ssh -G cmux-vm 2>/dev/null | awk '$1 == "hostname" { print $2; exit }' || true)" fi dhcp_host="" if [ -z "${CMUX_VM_HOST:-}" ] && [ -f /var/db/dhcpd_leases ]; then dhcp_host="$( awk ' BEGIN { RS="}"; FS="\\n" } { name=""; ip=""; for (i=1; i<=NF; i++) { if ($i ~ /^name=/) { name=substr($i, 6) } if ($i ~ /^ip_address=/) { ip=substr($i, 12) } } if (name != "" && ip != "" && tolower(name) ~ /cmux/) { gsub(/[[:space:]]/, "", ip); print ip; exit; } } ' /var/db/dhcpd_leases || true )" fi candidate_hosts=() if [ -n "${CMUX_VM_HOST:-}" ]; then candidate_hosts+=("$CMUX_VM_HOST") else candidate_hosts+=("cmux-vm") if [ -n "$cfg_host" ] && [ "$cfg_host" != "cmux-vm" ]; then candidate_hosts+=("$cfg_host") fi if [ -n "$dhcp_host" ]; then candidate_hosts+=("$dhcp_host") fi # Default from common local UTM/VZ NAT ranges (can be overridden via CMUX_VM_HOST). candidate_hosts+=("192.168.64.73") fi probe_deadline="$(( $(date +%s) + ${CMUX_VM_PROBE_BUDGET_SECS:-30} ))" vm_host="" for h in "${candidate_hosts[@]}"; do if ssh "${ssh_check_opts[@]}" "$vm_user@$h" 'true' >/dev/null 2>&1; then vm_host="$h" break fi done # If the host isn't explicitly configured, fall back to scanning the common UTM/VZ subnet. if [ -z "$vm_host" ] && [ -z "${CMUX_VM_HOST:-}" ]; then subnet="${CMUX_VM_SUBNET:-192.168.64}" port="${CMUX_VM_PORT:-22}" echo "Attempting VM discovery on ${subnet}.0/24 (port ${port})..." # Note: most probes will fail; force a 0 exit status so `set -euo pipefail` doesn't abort. candidates="$( seq 1 254 | xargs -P 48 -I{} sh -c "nc -z -w 1 ${subnet}.{} ${port} >/dev/null 2>&1 && echo ${subnet}.{}; exit 0" | head -n 20 )" for ip in $candidates; do if [ "$(date +%s)" -ge "$probe_deadline" ]; then break fi if ssh "${ssh_check_opts[@]}" "$vm_user@$ip" 'true' >/dev/null 2>&1; then vm_host="$ip" break fi done fi if [ -z "$vm_host" ]; then echo "ERROR: Could not reach the UI-test VM via SSH; UI tests are required." echo "Tried:" for h in "${candidate_hosts[@]}"; do echo " - $vm_user@$h" done echo "ssh -G cmux-vm:" ssh -G cmux-vm 2>/dev/null | head -40 || true echo "Name resolution:" dscacheutil -q host -a name cmux-vm || true echo "Network probe (nc):" nc -z -w 1 cmux-vm 22 || true nc -z -w 1 192.168.64.73 22 || true exit 1 fi echo "Running UI tests on $vm_user@$vm_host..." rsync -e "ssh ${ssh_run_opts[*]}" -a --delete \ --exclude build \ --exclude .git \ --exclude "GhosttyTabs.xcodeproj/project.xcworkspace" \ ./ "$vm_user@$vm_host":/Users/cmux/GhosttyTabs/ ssh "${ssh_run_opts[@]}" "$vm_user@$vm_host" 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:GhosttyTabsUITests/UpdatePillUITests test'