Reliable disk image mounting and cleanup in Cloud Mac CI

Reliable disk image mounting and cleanup in Cloud Mac CI

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:

  1. Volumes owned by the current job: unmount them normally through the exit trap.
  2. Volumes owned by other active jobs: record and skip them; never clean up across job boundaries.
  3. 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 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.

Dedicated physical node

Choose a cloud Mac for continuous builds

Compare SDKMac M4 and SDKMac M4 Pro configurations, regions, and four billing cycles, then create your order.

Choose a rental plan