Fix TCC dialog, trim black frames, add macos-26 runner option (#784)

- Grant kTCCServiceScreenCapture in system-level TCC database (sudo)
  and pre-date ScreenCaptureApprovals.plist to suppress Sequoia's
  private window picker dialog
- Move recording start to right before xcodebuild test (skip build time)
- Trim leading black frames from video using ffmpeg blackdetect
- Add runner input: macos-15 (Sequoia) or macos-26 (Tahoe)
This commit is contained in:
Lawrence Chen 2026-03-02 23:14:27 -08:00 committed by GitHub
parent d77299c220
commit 37dc43a6de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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 </dev/null >/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 </dev/null >/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)