diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index fd10a563..48b5e4de 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -19,10 +19,18 @@ on: required: false default: true type: boolean + runner: + description: "Runner OS (macos-15 or macos-26)" + required: false + default: "macos-15" + type: choice + options: + - macos-15 + - macos-26 jobs: e2e: - runs-on: macos-15 + runs-on: ${{ inputs.runner || 'macos-15' }} env: TEST_REF: ${{ inputs.ref || github.ref }} steps: @@ -107,56 +115,38 @@ jobs: echo "FFMPEG_PATH=$FFMPEG_PATH" >> "$GITHUB_ENV" - name: Grant TCC screen recording permission + if: ${{ inputs.record_video }} continue-on-error: true run: | - TCC_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db" - if [ -f "$TCC_DB" ]; then - for client in /usr/sbin/screencapture "${FFMPEG_PATH:-/opt/homebrew/bin/ffmpeg}" /opt/homebrew/bin/ffmpeg /usr/local/bin/ffmpeg; do - sqlite3 "$TCC_DB" "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version) VALUES ('kTCCServiceScreenCapture', '$client', 1, 2, 4, 1);" 2>/dev/null || true + FFMPEG_BIN="${FFMPEG_PATH:-/opt/homebrew/bin/ffmpeg}" + + # System-level TCC database (where kTCCServiceScreenCapture lives) + SYS_TCC="/Library/Application Support/com.apple.TCC/TCC.db" + if [ -f "$SYS_TCC" ]; then + echo "Granting screen capture in system TCC database" + for client in "$FFMPEG_BIN" /opt/homebrew/bin/ffmpeg /usr/local/bin/ffmpeg /usr/sbin/screencapture; do + sudo sqlite3 "$SYS_TCC" \ + "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, csreq, policy_id, indirect_object_identifier_type, indirect_object_identifier, indirect_object_code_identity, flags, last_modified) VALUES ('kTCCServiceScreenCapture', '$client', 1, 2, 4, 1, NULL, NULL, 0, 'UNUSED', NULL, 0, $(date +%s));" 2>&1 || echo " (failed for $client)" done fi - - name: Start screen recording - if: ${{ inputs.record_video }} - run: | - # Detect screen capture device index. ffmpeg -list_devices always - # exits non-zero; redirect noise to a temp file and parse it. - DEVLIST=$( ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true ) - echo "Available devices:" - echo "$DEVLIST" | grep -E "AVFoundation|Capture screen" - - SCREEN_INDEX=$( echo "$DEVLIST" | grep "Capture screen" | head -1 \ - | sed 's/.*\[\([0-9]*\)\].*/\1/' ) - SCREEN_INDEX="${SCREEN_INDEX:-0}" - echo "Using screen device index: $SCREEN_INDEX" - - # Start recording. Try detected index, fall back to 1 if it dies immediately. - start_recording() { - ffmpeg -f avfoundation -framerate 10 -capture_cursor 1 \ - -i "$1:none" \ - -c:v libx264 -preset ultrafast -pix_fmt yuv420p \ - /tmp/test-recording.mp4 /tmp/ffmpeg.log 2>&1 & - echo $! - } - - RECORD_PID=$(start_recording "$SCREEN_INDEX") - sleep 2 - - if ! kill -0 "$RECORD_PID" 2>/dev/null; then - echo "Index $SCREEN_INDEX failed, trying index 1" - cat /tmp/ffmpeg.log - rm -f /tmp/test-recording.mp4 - RECORD_PID=$(start_recording 1) - sleep 2 + # User-level TCC database (fallback) + USER_TCC="$HOME/Library/Application Support/com.apple.TCC/TCC.db" + if [ -f "$USER_TCC" ]; then + echo "Granting screen capture in user TCC database" + for client in "$FFMPEG_BIN" /opt/homebrew/bin/ffmpeg /usr/local/bin/ffmpeg; do + sqlite3 "$USER_TCC" \ + "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, csreq, policy_id, indirect_object_identifier_type, indirect_object_identifier, indirect_object_code_identity, flags, last_modified) VALUES ('kTCCServiceScreenCapture', '$client', 1, 2, 4, 1, NULL, NULL, 0, 'UNUSED', NULL, 0, $(date +%s));" 2>&1 || echo " (failed for $client)" + done fi - if kill -0 "$RECORD_PID" 2>/dev/null; then - echo "Recording started (PID $RECORD_PID)" - else - echo "::error::ffmpeg screen recording failed to start" - cat /tmp/ffmpeg.log + # Suppress Sequoia's ScreenCaptureApprovals prompt by pre-dating approval + APPROVALS_PLIST="$HOME/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plist" + if [ -d "$(dirname "$APPROVALS_PLIST")" ]; then + echo "Pre-dating ScreenCaptureApprovals" + # Set approval date far in the future so the monthly prompt never fires + defaults write "$APPROVALS_PLIST" "$FFMPEG_BIN" -date "3000-01-01T00:00:00Z" 2>&1 || echo " (failed)" fi - echo "RECORD_PID=$RECORD_PID" >> "$GITHUB_ENV" - name: Clean DerivedData run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* @@ -186,12 +176,39 @@ jobs: env: TEST_FILTER: ${{ inputs.test_filter }} TEST_TIMEOUT: ${{ inputs.test_timeout || '120' }} + RECORD_VIDEO: ${{ inputs.record_video }} run: | set -euo pipefail SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - ONLY_TESTING="-only-testing:cmuxUITests/$TEST_FILTER" + # Start recording right before the test (after build/resolve) + if [ "$RECORD_VIDEO" = "true" ]; then + DEVLIST=$( ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true ) + echo "Available devices:" + echo "$DEVLIST" | grep -E "AVFoundation|Capture screen" + + SCREEN_INDEX=$( echo "$DEVLIST" | grep "Capture screen" | head -1 \ + | sed 's/.*\[\([0-9]*\)\].*/\1/' ) + SCREEN_INDEX="${SCREEN_INDEX:-0}" + echo "Using screen device index: $SCREEN_INDEX" + + ffmpeg -f avfoundation -framerate 10 -capture_cursor 1 \ + -i "${SCREEN_INDEX}:none" \ + -c:v libx264 -preset ultrafast -pix_fmt yuv420p \ + /tmp/test-recording-raw.mp4 /tmp/ffmpeg.log 2>&1 & + RECORD_PID=$! + echo "RECORD_PID=$RECORD_PID" >> "$GITHUB_ENV" + sleep 2 + + if kill -0 "$RECORD_PID" 2>/dev/null; then + echo "Recording started (PID $RECORD_PID)" + else + echo "::warning::ffmpeg screen recording failed to start" + cat /tmp/ffmpeg.log + fi + fi + set +e OUTPUT=$(xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ @@ -225,10 +242,10 @@ jobs: exit 1 fi - - name: Stop screen recording + - name: Stop recording and trim if: ${{ always() && inputs.record_video && env.RECORD_PID != '' }} run: | - # Send quit signal to ffmpeg for clean finalization + # Stop ffmpeg cleanly kill -INT "$RECORD_PID" 2>/dev/null || true for i in $(seq 1 15); do if ! kill -0 "$RECORD_PID" 2>/dev/null; then @@ -238,10 +255,33 @@ jobs: sleep 1 done kill -9 "$RECORD_PID" 2>/dev/null || true - echo "=== ffmpeg log ===" - cat /tmp/ffmpeg.log 2>/dev/null || true - echo "=== recording file ===" - ls -lh /tmp/test-recording.mp4 2>/dev/null || echo "No recording file found" + + echo "=== raw recording ===" + ls -lh /tmp/test-recording-raw.mp4 2>/dev/null || { echo "No recording file"; exit 0; } + + # Trim: detect first non-black frame and cut from there + BLACK_END=$(ffmpeg -i /tmp/test-recording-raw.mp4 \ + -vf "blackdetect=d=0.3:pic_th=0.95:pix_th=0.1" \ + -an -f null - 2>&1 \ + | grep "black_end" | tail -1 \ + | sed 's/.*black_end:\([0-9.]*\).*/\1/' || true) + + if [ -n "$BLACK_END" ] && [ "$BLACK_END" != "0" ]; then + echo "Trimming ${BLACK_END}s of black frames from start" + ffmpeg -y -i /tmp/test-recording-raw.mp4 -ss "$BLACK_END" \ + -c:v libx264 -preset ultrafast -pix_fmt yuv420p \ + /tmp/test-recording.mp4 2>/dev/null + else + echo "No black frames detected, using raw recording" + mv /tmp/test-recording-raw.mp4 /tmp/test-recording.mp4 + fi + + echo "=== final recording ===" + ls -lh /tmp/test-recording.mp4 + # Print duration + ffprobe -v error -show_entries format=duration \ + -of default=noprint_wrappers=1:nokey=1 /tmp/test-recording.mp4 2>/dev/null \ + | xargs -I{} echo "Duration: {}s" - name: Upload recording artifact if: ${{ always() && inputs.record_video }} @@ -276,7 +316,6 @@ jobs: RUN_URL="https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" ARTIFACT_URL="$RUN_URL#artifacts" - # Build issue body (no leading whitespace) BODY="**Status:** $STATUS_EMOJI **Ref:** \`$REF_DISPLAY\` **SHA:** [\`${COMMIT_SHA:0:12}\`](https://github.com/${{ github.repository }}/commit/$COMMIT_SHA)