Engineering Notes

Reset Cloud Mac Workspaces with APFS Clones

Reset Cloud Mac Workspaces with APFS Clones

When the same cloud Mac runs dependency upgrades, project migrations, and destructive scripts in sequence, the hardest part is often not the build itself but reliably returning to a clean workspace. Checking out a large repository again means rereading a vast number of small files, while reusing an old directory can reintroduce untracked files, generated artifacts, and incorrect permissions. If the working directory resides on an APFS volume, you can maintain a validated baseline and use copy-on-write clones to create a modifiable copy for each job.

What APFS clones solve

APFS clones initially allow two files to share the same underlying data blocks. Only when one copy is modified does the file system allocate new blocks for the changed data. A baseline containing source code, dependencies, and static tools can therefore be copied into an independent directory relatively quickly, while subsequent changes remain isolated from the baseline.

This differs from hard links: cloned files have separate directory entries, so modifying the copy does not change the original file. A clone is not a backup either, because the baseline and its copies still reside on the same volume. Clones provide no off-device recovery if the volume is damaged, the baseline is accidentally deleted, or the entire machine loses its data.

Treat APFS cloning as a low-cost way to create working copies, not as a cache-hit guarantee, a snapshot system, or a data protection strategy.

du -sh reports the logical size of a directory. It does not directly show how much additional physical space a clone consumes. When monitoring capacity, also check the free space on the APFS volume and define cleanup thresholds that account for continued writes during a job.

Prepare a verifiable baseline first

A baseline should not be a directory casually left behind after a build. Check out a fixed commit, restore dependencies, and then remove all state that belongs only to an individual job. At a minimum, confirm that the working tree is clean, verify the submodule state, and record the baseline commit.

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
mkdir -p "$ROOT"

git -C "$BASE" status --porcelain
git -C "$BASE" submodule status --recursive
git -C "$BASE" rev-parse HEAD > "$BASE/.baseline-revision"
test -z "$(git -C "$BASE" status --porcelain)"

Because .baseline-revision itself becomes an untracked file, store it outside the repository or add it to an ignore rule approved by the team. A safer layout keeps the commit ID under $ROOT/metadata, allowing $BASE to pass the clean-working-tree check at all times.

What to exclude from the baseline

Do not include DerivedData, test result bundles, archives, temporary keychains, runtime logs, or active sockets in the baseline. These items often contain absolute paths, process state, or job-specific credentials. Whether dependency directories belong in the baseline depends on whether lockfiles fully determine their contents and whether installation scripts write to machine-level paths.

Only the baseline update job should have permission to write to the baseline. Regular build accounts may read it but should not run build commands inside it. Otherwise, a single mistake can contaminate every subsequent clone.

Create an independent clone for each job

First confirm that the root directory actually resides on APFS, then use the job ID to create a unique directory. The destination path must not already exist, and the job ID must be restricted to safe characters so that external input is never inserted directly into a deletion path.

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
JOB_ID="${BUILD_ID:?BUILD_ID is required}"

case "$JOB_ID" in
  *[!A-Za-z0-9._-]*) exit 64 ;;
esac

test "$(stat -f %T "$ROOT")" = "apfs"

WORK="$ROOT/jobs/$JOB_ID"
DERIVED="$ROOT/derived/$JOB_ID"

test ! -e "$WORK"
mkdir -p "$ROOT/jobs" "$DERIVED"
cp -cR "$BASE" "$WORK"

xcodebuild \
  -workspace "$WORK/App.xcworkspace" \
  -scheme App \
  -derivedDataPath "$DERIVED" \
  build

cp -cR requests a clone copy, but the actual behavior still depends on the source path, destination path, and file system. The source and destination directories must reside on the same APFS volume. If the operation crosses volumes, it should not be treated as a copy-on-write workflow. Before deployment, use a test directory containing a large file to observe the copy time and changes in volume capacity.

Isolate concurrent jobs and mutable state

An independent workspace does not mean that all state is isolated. Build tools may also read and write caches, temporary directories, and configuration files under the user’s home directory. At a minimum, assign separate DerivedData and result directories to every job, and prevent multiple jobs from sharing the same simulator device set or output path.

Set a capacity budget for concurrency

A newly created clone consumes little additional space, but compiled objects, rewritten dependencies, and archives quickly allocate new blocks. The concurrency limit should be based on the worst-case writable delta, not the logical size of the baseline. Check volume capacity before starting a job and record the change after the build completes:

df -h "$ROOT"
du -sh "$DERIVED" "$WORK"

If a job rewrites many dependency files, the clone’s storage advantage will gradually decline. In that case, keep immutable dependencies in the baseline and move frequently changing generated directories into job-specific paths. Do not share writable caches merely to preserve a high proportion of shared clone blocks.

Clean up safely and refresh the baseline regularly

The primary goal of a cleanup script is not to delete quickly, but to ensure that deletion never escapes the job root. Before deleting anything, validate the path prefix, directory existence, and job ID together. Never run recursive deletion directly against a variable that might be empty.

set -euo pipefail

ROOT="$HOME/ci-workspaces"
WORK="$ROOT/jobs/${BUILD_ID:?BUILD_ID is required}"
DERIVED="$ROOT/derived/${BUILD_ID:?BUILD_ID is required}"

case "$WORK" in
  "$ROOT"/jobs/*) rm -rf -- "$WORK" ;;
  *) exit 64 ;;
esac

case "$DERIVED" in
  "$ROOT"/derived/*) rm -rf -- "$DERIVED" ;;
  *) exit 64 ;;
esac

Update the baseline through a new directory: check out the target commit, install deterministic dependencies, run acceptance checks, and then replace the current baseline. Do not pull updates in place and continue serving clones from the existing baseline. An interrupted update can leave a mixed state in which old and new data cannot be distinguished reliably.

The final review covers four points: the baseline commit is traceable and its working tree remains clean; every job has a unique workspace and generated-data directory; the volume has enough free space for the maximum concurrent write load; and the cleanup logic accepts only controlled paths. Once these conditions are met, APFS cloning becomes a repeatable engineering step rather than a copy trick that merely appears fast.

Frequently asked questions

Can an APFS clone replace a build cache or backup?

No. It creates a fast, writable copy on the same APFS volume, but changed blocks consume additional space. Important data still needs an independent backup.

Why should DerivedData stay out of the baseline?

DerivedData contains absolute paths, indexes, and job-specific state. Giving each build its own directory provides clearer isolation and more reproducible results.

Dedicated physical node

Continue validating this workflow on a cloud Mac

Choose the right memory, storage, node, and rental period from three Apple Silicon configurations.

Choose a configuration and order