|
1 | 1 | name: Verify recording (real capture) |
2 | 2 |
|
3 | 3 | # Records a marker stimulus through Cap's real Studio and Instant pipelines |
4 | | -# (ScreenCaptureKit + VideoToolbox) on hosted macOS runners and checks every |
| 4 | +# (ScreenCaptureKit + VideoToolbox) on macOS runners and checks every |
5 | 5 | # frame and audio marker against the host-clock truth. See |
6 | 6 | # crates/verify/tests/recording.rs. |
| 7 | +# |
| 8 | +# Runners: Blacksmith macOS (M4 Pro VM, 6 vCPU, logged-in GUI session, no |
| 9 | +# concurrency cap) by default; GitHub-hosted macOS (5 concurrent jobs for the |
| 10 | +# whole org) on dispatch with runners=hosted or both. Blacksmith needs Screen |
| 11 | +# Recording granted in TCC to the binaries that capture and their runner |
| 12 | +# ancestry, and an audio device (BlackHole), which also makes system audio |
| 13 | +# capturable there. |
7 | 14 |
|
8 | 15 | on: |
9 | 16 | workflow_dispatch: |
|
21 | 28 | description: "Preserve recordings and raw crash snapshots for passing cases" |
22 | 29 | type: boolean |
23 | 30 | default: false |
| 31 | + runners: |
| 32 | + description: "blacksmith (default), hosted (GitHub macOS, fallback) or both" |
| 33 | + type: choice |
| 34 | + options: [blacksmith, hosted, both] |
| 35 | + default: blacksmith |
24 | 36 | push: |
25 | 37 | branches: |
26 | 38 | - ci/verify-recording |
|
41 | 53 | strategy: |
42 | 54 | fail-fast: false |
43 | 55 | matrix: |
44 | | - runner: [macos-15, macos-26] |
| 56 | + runner: ${{ fromJSON(inputs.runners == 'hosted' && '["macos-15","macos-26"]' || inputs.runners == 'both' && '["blacksmith-6vcpu-macos-15","blacksmith-6vcpu-macos-26","macos-15","macos-26"]' || '["blacksmith-6vcpu-macos-15","blacksmith-6vcpu-macos-26"]') }} |
45 | 57 | runs-on: ${{ matrix.runner }} |
46 | 58 | timeout-minutes: 45 |
47 | 59 | env: |
|
54 | 66 | CAP_VERIFY_SCRATCH: ${{ github.workspace }}/verify-scratch |
55 | 67 | CAP_VERIFY_REPORT_DIR: ${{ github.workspace }}/verify-report |
56 | 68 | CAP_VERIFY_DUMP: ${{ github.workspace }}/verify-dump |
57 | | - CAP_VERIFY_AUDIO_OUTPUT: ${{ matrix.runner == 'macos-26' && 'Null Audio Device' || '' }} |
| 69 | + # Hosted macos-26 has a virtual output device that gives |
| 70 | + # ScreenCaptureKit only silence, so system audio is not required |
| 71 | + # there; Blacksmith gets BlackHole (set up below), which carries the |
| 72 | + # stimulus's audio to ScreenCaptureKit, so system audio is required |
| 73 | + # and the system-only case runs. |
| 74 | + CAP_VERIFY_AUDIO_OUTPUT: ${{ startsWith(matrix.runner, 'blacksmith-') && 'BlackHole 2ch' || matrix.runner == 'macos-26' && 'Null Audio Device' || '' }} |
| 75 | + CAP_VERIFY_REQUIRE_SYSTEM_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && '1' || '' }} |
58 | 76 | steps: |
59 | 77 | - uses: actions/checkout@v4 |
60 | 78 |
|
@@ -97,6 +115,56 @@ jobs: |
97 | 115 | cargo test --locked -p cap-verify --test recording --no-run |
98 | 116 | echo "build seconds: $(( $(date +%s) - start ))" |
99 | 117 |
|
| 118 | + # Blacksmith runs a logged-in GUI session with SIP disabled, but |
| 119 | + # ScreenCaptureKit is denied until TCC grants the responsible process |
| 120 | + # (Runner.Worker), so grant Screen Recording (and Microphone) to every |
| 121 | + # binary that captures plus the whole runner ancestry, then restart |
| 122 | + # tccd. It has no audio device: BlackHole provides input and output. |
| 123 | + - name: Blacksmith capture permissions and audio device |
| 124 | + if: startsWith(matrix.runner, 'blacksmith-') |
| 125 | + run: | |
| 126 | + set -euo pipefail |
| 127 | + brew install --cask blackhole-2ch |
| 128 | + brew install switchaudio-osx |
| 129 | + sudo killall coreaudiod || true |
| 130 | + for i in $(seq 1 30); do |
| 131 | + SwitchAudioSource -a -t output | grep -qx "BlackHole 2ch" && break |
| 132 | + sleep 1 |
| 133 | + done |
| 134 | + SwitchAudioSource -s "BlackHole 2ch" -t output |
| 135 | + SwitchAudioSource -s "BlackHole 2ch" -t input |
| 136 | + echo "outputs:"; SwitchAudioSource -a -t output |
| 137 | + echo "default output: $(SwitchAudioSource -c -t output)" |
| 138 | +
|
| 139 | + cargo test --locked -p cap-verify --test recording --no-run --message-format=json 2>/dev/null \ |
| 140 | + | python3 -c 'import json, sys |
| 141 | + for line in sys.stdin: |
| 142 | + m = json.loads(line) |
| 143 | + if m.get("reason") == "compiler-artifact" and m.get("executable") and m["target"]["name"] in ("recording", "cap-verify-stimulus"): |
| 144 | + print(m["executable"])' > "$RUNNER_TEMP/capture-binaries" |
| 145 | + test "$(wc -l < "$RUNNER_TEMP/capture-binaries")" -ge 2 |
| 146 | + p=$$ |
| 147 | + while [ "$p" -gt 1 ]; do |
| 148 | + ps -o comm= -p "$p" |
| 149 | + p=$(ps -o ppid= -p "$p" | tr -d ' ') |
| 150 | + done >> "$RUNNER_TEMP/capture-binaries" |
| 151 | + for c in "$(rustup which cargo)" "$HOME/.cargo/bin/cargo"; do echo "$c"; done >> "$RUNNER_TEMP/capture-binaries" |
| 152 | +
|
| 153 | + db="/Library/Application Support/com.apple.TCC/TCC.db" |
| 154 | + approvals="$HOME/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plist" |
| 155 | + mkdir -p "$(dirname "$approvals")" |
| 156 | + now=$(date +%s) |
| 157 | + until=$(date -u -v+1y '+%a %b %d %H:%M:%S GMT %Y') |
| 158 | + sort -u "$RUNNER_TEMP/capture-binaries" | grep '^/' | while read -r client; do |
| 159 | + for service in kTCCServiceScreenCapture kTCCServiceMicrophone; do |
| 160 | + sudo sqlite3 "$db" "INSERT OR REPLACE INTO access (service,client,client_type,auth_value,auth_reason,auth_version,indirect_object_identifier,flags,last_modified) VALUES ('$service','$client',1,2,4,1,'UNUSED',0,$now);" |
| 161 | + done |
| 162 | + /usr/libexec/PlistBuddy -c "Add :$client date $until" "$approvals" >/dev/null 2>&1 || true |
| 163 | + echo "granted $client" |
| 164 | + done |
| 165 | + sudo killall tccd || true |
| 166 | + sleep 2 |
| 167 | +
|
100 | 168 | - name: Real-capture recording oracle |
101 | 169 | run: | |
102 | 170 | mkdir -p "$CAP_VERIFY_DUMP" |
|
0 commit comments