CI jobs often need to mount DMG files to inspect installer layouts, read test fixtures, validate signed delivery directories, or compare the contents of two disk images. A single manual run usually works without issue. Problems emerge when jobs run concurrently, scripts exit unexpectedly, or a previous job leaves a volume mounted. In these situations, a script that always reads from /Volumes/Product may silently access another job’s files instead of failing immediately.
Why default mounts break down under concurrency
hdiutil attach App.dmg selects a mount name based on the volume label stored in the image. If a volume with the same name already exists, macOS may create a new path with a numeric suffix. A script that continues to access the original fixed directory can no longer produce trustworthy test results.
Another common mistake is running detach only on the success path. If a validation command fails, the job is terminated, or a later step returns early, cleanup never runs. Reused Cloud Mac nodes gradually accumulate stale volumes, leading to volume-label collisions, files remaining in use, and incorrect working-directory assumptions.
Treat the mount point as a temporary resource owned by the job, not as a shared path on the machine. The same job should be responsible for creating, using, unmounting, and diagnosing it.
Before changing the workflow, record the current state:
/usr/bin/hdiutil info
/bin/df -h
/bin/ls -la /Volumes
hdiutil info shows the relationship among images, devices, and mount paths. df only reports filesystem usage and cannot replace inspection of image mappings.
Assign a unique mount point to every job
The mount directory should include a stable, unique job identifier. Do not use only the repository name, because multiple branches or retry attempts from the same repository may run at the same time. The identifier must also sanitize path characters such as slashes and spaces.
The following script verifies the image first, mounts it read-only, and attempts to unmount it whether the script finishes normally, a command fails, or a termination signal is received:
#!/bin/bash
set -euo pipefail
IMAGE="${1:?usage: mount-image.sh path/to/file.dmg}"
RAW_JOB_ID="${CI_JOB_ID:-local-$$}"
JOB_ID="${RAW_JOB_ID//[^a-zA-Z0-9._-]/_}"
MOUNT="${TMPDIR%/}/sdkmac-image-${JOB_ID}"
ATTACHED=0
cleanup() {
local status=$?
trap - EXIT INT TERM
if (( ATTACHED == 1 )); then
if ! /usr/bin/hdiutil detach "$MOUNT"; then
printf 'unable to detach %s\n' "$MOUNT" >&2
fi
fi
/bin/rmdir "$MOUNT" 2>/dev/null || true
exit "$status"
}
trap cleanup EXIT INT TERM
/usr/bin/hdiutil verify "$IMAGE"
/bin/mkdir -p "$MOUNT"
/usr/bin/hdiutil attach \
-nobrowse \
-readonly \
-mountpoint "$MOUNT" \
"$IMAGE"
ATTACHED=1
test -r "$MOUNT"
/usr/bin/find "$MOUNT" -maxdepth 2 -type f -print
-nobrowse keeps the mounted volume out of the normal graphical browsing flow, while -readonly prevents inspection steps from modifying test data accidentally. The script uses rmdir instead of rm -rf: if unmounting fails, rmdir will not traverse the still-mounted directory and delete its contents.
Read-only is not the answer for every use case
When testing write behavior, do not simply make a shared reference image writable. First copy the image for the current job, verify that the copy resides in a job-specific temporary directory, and then mount that copy. This prevents a failed job from contaminating the reference input for the next run.
| Purpose | Recommended mode | Acceptance criteria |
|---|---|---|
| Inspect installers or test fixtures | Read-only mount | File presence, permissions, and checksums |
| Validate write workflows | Job-specific writable copy | Write results and complete unmounting |
| Compare two images | Two separate read-only mount points | Distinct paths and a fixed comparison order |
Preserve diagnostic evidence when unmounting fails
An unmount failure is usually not a random hdiutil error. More often, a process still has its current directory, an open file, or a working path inside the volume. Do not immediately hide the cause with a forced unmount. Identify the process holding the volume first:
/usr/sbin/lsof +D "$MOUNT" 2>/dev/null || true
/usr/bin/hdiutil info
/bin/ps -axo pid,ppid,command
lsof +D can be slow on large directories, so it should run only on the failure path. Pay particular attention to test processes, log collectors, compression tools, and shells that used cd to enter the mounted directory without leaving it. The usual fix is to wait for child processes to exit, close file handles, and explicitly return to the job’s working directory before unmounting.
The cleanup function should also preserve the original exit code. If an unmount failure overwrites the actual test error, the pipeline will report only the cleanup problem and lose the initial cause. A more reliable approach is to retain both the test status and the cleanup status, then record them separately in the job summary.
Audit orphaned volumes instead of deleting them blindly
Before a node starts a new job, it can inspect mount directories named by this pipeline. A similar-looking name alone is not enough reason to delete one. First confirm that the directory is actually a mount point, then determine whether the corresponding job is still running and whether the image belongs to the current workspace.
Classify audit results into three categories:
- Volumes owned by the current job: unmount them normally through the exit trap.
- Volumes owned by other active jobs: record and skip them; never clean up across job boundaries.
- Suspected orphaned volumes with no active job: collect
hdiutil info, process ownership, and creation context, then unmount only after confirmation.
Forced unmounting should be the last resort after manual confirmation. An automated script that acts only on directory age may interrupt a long-running job that is still operating normally. Directory timestamps are not the same as mount times, and copying files or reading metadata can also change the evidence used for that decision.
Make disk image handling part of pipeline acceptance criteria
A reusable disk image workflow should verify at least the following:
- The input file exists, and
hdiutil verifycompletes before mounting. - Every job uses an independent directory rather than deriving its path from the image’s volume label.
- Mounts are read-only by default; jobs that require writes create their own copies.
- Cleanup traps cover every exit path, and child processes can also receive termination signals.
- The job leaves the mounted directory and waits for processes reading the volume to exit before unmounting.
- If unmounting fails, device, path, and process evidence is retained without recursive deletion.
- After the job finishes, the mount point is gone and no other concurrent job has been cleaned up accidentally.
The same approach works for interactive troubleshooting and unattended pipelines on SDKMac dedicated physical Mac nodes. The goal is not to add more cleanup commands, but to give every mounted volume a clear owner, lifecycle, and failure record. Once those controls are in place, DMG inspections no longer produce hard-to-reproduce results simply because a machine is reused or jobs run concurrently.
Frequently asked questions
Why should CI avoid the default volume name under /Volumes?
Parallel jobs can attach images with the same volume name. macOS may append a number, so a script that reads a fixed path can silently inspect another job's content.
Can a mount directory be deleted when hdiutil detach fails?
No. Identify the process holding the volume and retry a normal detach first. Recursive deletion through a mounted writable path can remove image data and destroy diagnostic evidence.
Should CI mount disk images as read-only by default?
Yes, when inspecting packages, fixtures, or immutable artifacts. Create a separate writable copy for a job only when the test explicitly needs to modify image contents.
Choose a cloud Mac for continuous builds
Compare SDKMac M4 and SDKMac M4 Pro configurations, regions, and four billing cycles, then create your order.