When a UI test fails on a cloud Mac, assertions and screenshots alone are often insufficient. A dialog may appear and immediately disappear, a page transition may freeze midway, or the keyboard may cover a button. Only continuous video can reveal the order in which these events occurred. The reliable approach is not to retain every recording indefinitely. Instead, start recording the designated simulator before the test, delete the video when the test passes, and archive it with the logs and result bundle only when the test fails.
Pin the Test Device and Define the Evidence Scope
Do not pass booted directly to the recording command. Concurrent jobs, devices left running after an abnormal exit, or manual debugging can leave multiple simulators booted at the same time, causing the recording to come from the wrong device. First create or locate a device dedicated to the current job, obtain its UDID, and then pass that UDID to the boot, test, recording, and cleanup steps.
DEVICE_NAME="CI-iPhone"
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-0"
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"
UDID="$(xcrun simctl list devices available -j |
plutil -extract devices json -o - - |
jq -r --arg name "$DEVICE_NAME" '
.. | objects | select(.name? == $name) | .udid
' | head -n 1)"
if [ -z "$UDID" ]; then
UDID="$(xcrun simctl create "$DEVICE_NAME" "$DEVICE_TYPE" "$RUNTIME")"
fi
xcrun simctl boot "$UDID" 2>/dev/null || true
xcrun simctl bootstatus "$UDID" -b
Runtime identifiers vary with the installed Xcode version. The pipeline should validate them with xcrun simctl list runtimes before execution instead of assuming that every machine has the same runtime installed. When several jobs run concurrently, include the job number in the device name to prevent shared state.
A recording should complete the failure timeline, not replace the logs. The video answers “what happened in the interface first,” while the logs and xcresult answer “what the test framework recorded.”
Stop the Recording Process Cleanly
simctl io recordVideo continues running until it is stopped, so the script must save its PID. Do not send a forceful termination signal immediately after the test finishes. Send INT first so the tool can finish writing the video container index, then wait for the process to exit.
ARTIFACTS="${PWD}/artifacts"
VIDEO="${ARTIFACTS}/ui-test.mp4"
RESULT="${ARTIFACTS}/UITests.xcresult"
LOG="${ARTIFACTS}/xcodebuild.log"
mkdir -p "$ARTIFACTS"
rm -rf "$RESULT"
rm -f "$VIDEO" "$LOG"
xcrun simctl io "$UDID" recordVideo \
--codec=h264 \
--force "$VIDEO" &
RECORDER_PID=$!
finish_recording() {
if kill -0 "$RECORDER_PID" 2>/dev/null; then
kill -INT "$RECORDER_PID"
wait "$RECORDER_PID" || true
fi
}
trap finish_recording EXIT INT TERM
H.264 is generally convenient to preview in browsers and issue-tracker attachments. If the team later needs frame-by-frame analysis, another codec can be used after confirming compatibility with the artifact system. Do not mix formats within the same pipeline, however, or playback tools and retention policies will become unpredictable.
Preserve the Actual Test Exit Code
Recording runs as a side process and must not mask the result from xcodebuild. The safest approach is to temporarily disable exit-on-error, save the test exit code separately, and then stop the recording normally. If tee is also used to display the log, read the exit status of xcodebuild from the pipeline rather than mistakenly using the successful status returned by tee.
set +e
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination "platform=iOS Simulator,id=${UDID}" \
-resultBundlePath "$RESULT" \
>"$LOG" 2>&1
TEST_STATUS=$?
set -e
finish_recording
trap - EXIT INT TERM
if [ "$TEST_STATUS" -eq 0 ]; then
rm -f "$VIDEO"
exit 0
fi
exit "$TEST_STATUS"
The script deliberately does not exit immediately after a test failure because the recording still needs to be finalized. Use a job-specific path for resultBundlePath and remove any existing directory before execution; otherwise, results from an earlier run may be mixed with the current failure.
Validate the Video Instead of Only Checking for a File
A zero-byte file, a short file containing only a container header, or a recording interrupted by force has no diagnostic value. Before archiving, check at least the file size and use a system media tool to read its duration. A very short video can be legitimate when the test fails immediately after launch, so the minimum duration threshold should not be set too high.
| Check | Recommended action | Action on failure |
|---|---|---|
| File exists | test -f |
Retain the log and mark the recording as missing |
| File is not empty | test -s |
Check the recording PID and available disk space |
| Duration is readable | Query media metadata | Treat it as corrupt and do not upload an empty shell |
| Device matches | Compare the recorded UDID | Stop the job and clean up the incorrect device |
| Test result bundle | Check the xcresult directory | Retain the raw log as a fallback |
Write the UDID, test start time, and recording path to a plain-text manifest and archive it with the video. Do not include access credentials, the complete set of environment variables, or unsanitized command lines in the manifest. The video itself may also expose notifications, test account names, or business data, so test fixtures should use dedicated, non-sensitive sample data.
Handle Concurrency, Timeouts, and Leftover Processes
Each job should have its own simulator, result directory, and recording filename. Sharing booted, a fixed artifacts/ui-test.mp4, or a common DerivedData directory allows concurrent jobs to overwrite one another. Generate a job-specific directory from the unique identifier supplied by the pipeline, and remove temporary files from successful jobs on exit.
If an outer timeout terminates the test, the cleanup function must still have a chance to run. First send the test process a termination signal it can handle, allow several seconds for the trap to finalize the recording, and only then perform forced cleanup. Before the next job starts, check for simctl io recording processes whose parent process has already exited. Do not use a broad killall, because it may terminate other jobs running on the same machine.
A stable archive normally contains only three items: the failure video, the xcresult result bundle, and the raw build log. Name all three with the same job number and apply the same retention period to each. After opening a specific run from the failure list, you can first watch the video to identify the UI state, then use the result bundle to locate assertions and attachments, and finally consult the log to verify the command, target device, and exit code. This creates a complete evidence chain without retaining unnecessary artifacts.
Frequently asked questions
Why should the script avoid the booted simulator alias?
Parallel jobs may leave several simulators running. An explicit UDID ensures booting, testing, recording, and cleanup all target the same device.
Why can a recorded video become unplayable?
A forced termination may interrupt the container index write. Send INT to the recording process and wait for it to exit before validating the file.
Should successful test recordings be retained?
Usually not. Delete recordings after successful runs and archive the video, test log, and xcresult bundle only when the test command fails.
Run your next build on a dedicated Apple Silicon physical node
Choose a rental term and node region, review the Mac Mini M4, 16GB RAM, 256GB SSD, and USD amount due, then place your order.