
Keep release IPAs and dSYMs on cheap S3-compatible storage, delete non-release artifacts older than 30 days from the Mac's local disk with the find -mtime script below, and run it as a nightly CI job. If your current runner is already thrashing against a full disk, the Figment Managed Build Mac at $249/month includes one M-series Mac, CI setup, backups, monitoring, and 30 minutes of monthly support; specify the disk size you need in your configuration request at /mac-hosting/start.
A busy iOS CI Mac produces gigabytes of artifacts every day: compiled binaries, test result bundles, crash symbols, and archives. Without a retention policy the disk fills up and builds start failing in the middle of the night. The right fix is not simply a bigger disk — it is a policy that keeps the data you actually need and deletes the rest on a schedule.
Understanding what takes the most space helps you target the right directories:
| Artifact type | Typical size per build | Safe to delete after? |
|---|---|---|
| DerivedData intermediates | 1–4 GB | Yes, after build completes |
.xcarchive bundle | 200–800 MB | After IPA is exported and uploaded |
.ipa file | 20–200 MB | After 30 days (keep release builds) |
.app.dSYM bundle | 10–100 MB | Never delete release dSYMs; 30 days for PR builds |
.xcresult test bundle | 100 MB–2 GB | After 7–14 days |
| Simulator runtimes | 5–12 GB each | Only when no longer needed |
DerivedData can be regenerated by rebuilding, so it is the safest to prune aggressively. dSYMs for released builds, however, must survive as long as users run that version — they map binary addresses back to source file and line and are required to symbolicate any crash log from that build. Apple describes the generation and layout of the debug symbol bundle in Building your app to include debugging information.
A practical three-tier policy:
| Tier | Rule | Storage location |
|---|---|---|
| PR / branch builds | Delete all artifacts after 14 days | Local disk only |
| Main branch builds | Delete after 30 days, keep dSYMs for 90 days | Local disk; dSYMs copied to S3 at upload |
| Release-tagged builds | Keep forever | Upload IPA + dSYM + xcresult to S3; delete local copy after 7 days |
The point: upload release artifacts to object storage before you delete them locally. S3-compatible storage (AWS S3, Backblaze B2, Cloudflare R2) is published at roughly $0.004–$0.023 per GB-month depending on class and provider — far cheaper than an SSD upgrade on any Mac host.
Save this on your CI Mac and run it nightly. It walks a build output directory, skips any subdirectory containing a .release-tag marker file, deletes the rest if the directory mtime is older than N days, and prints the reclaimed total. It is an unexecuted example; test it against a throwaway directory before pointing it at real artifacts.
#!/usr/bin/env bash
# cleanup-artifacts.sh
# Usage: ./cleanup-artifacts.sh [BUILD_DIR] [DAYS]
#
# Deletes non-release build artifacts older than DAYS days.
# Any subdirectory containing a '.release-tag' file is preserved.
# Exits non-zero on any error; prints total disk space reclaimed.
set -euo pipefail
BUILD_DIR="${1:-$HOME/ci-artifacts}"
DAYS="${2:-30}"
if [[ ! -d "$BUILD_DIR" ]]; then
echo "ERROR: BUILD_DIR '$BUILD_DIR' does not exist." >&2
exit 1
fi
echo "cleanup-artifacts.sh"
echo " Directory : $BUILD_DIR"
echo " Retention : $DAYS days (release-tagged builds exempt)"
RECLAIMED_KB=0
DELETED_COUNT=0
SKIPPED_COUNT=0
while IFS= read -r -d '' dir; do
if [[ -f "$dir/.release-tag" ]]; then
echo " KEEP $dir"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
continue
fi
SIZE_KB=$(du -sk "$dir" 2>/dev/null | awk '{print $1}')
rm -rf -- "$dir"
RECLAIMED_KB=$((RECLAIMED_KB + SIZE_KB))
DELETED_COUNT=$((DELETED_COUNT + 1))
echo " DEL $dir (${SIZE_KB} KB)"
done < <(find "$BUILD_DIR" \
-mindepth 1 -maxdepth 1 \
-type d \
-mtime "+${DAYS}" \
-print0)
RECLAIMED_MB=$(( RECLAIMED_KB / 1024 ))
echo "Done. Deleted $DELETED_COUNT directories, skipped $SKIPPED_COUNT release builds."
echo "Disk reclaimed: ${RECLAIMED_MB} MB (${RECLAIMED_KB} KB)"
How to mark a release build. After a successful release archive, have the release job write the marker:
touch "$CI_ARTIFACTS_DIR/$BUILD_TAG/.release-tag"
Replace $CI_ARTIFACTS_DIR and $BUILD_TAG with your pipeline's output directory and the tag (for example v3.4.1-release).
Why -mtime on the directory, not -atime. Writing any file into a directory updates that directory's modification time, so the directory mtime is a reliable "last build wrote here" stamp. atime is updated on read and is often disabled or noatime-mounted for performance; do not rely on it.
Before the cleanup job runs, push release-tagged IPAs and dSYMs to object storage. This example uses the AWS CLI; rclone or mc work the same way against Backblaze B2 or Cloudflare R2.
#!/usr/bin/env bash
# upload-release-artifacts.sh
# Run immediately after a successful release archive, before cleanup.
set -euo pipefail
BUILD_TAG="${1:?Usage: upload-release-artifacts.sh <build-tag>}"
LOCAL_DIR="$HOME/ci-artifacts/$BUILD_TAG"
S3_BUCKET="s3://your-bucket/releases/$BUILD_TAG"
[[ -d "$LOCAL_DIR" ]] || { echo "Missing $LOCAL_DIR" >&2; exit 1; }
aws s3 cp "$LOCAL_DIR/MyApp.ipa" "$S3_BUCKET/MyApp.ipa" \
--storage-class STANDARD_IA
aws s3 cp "$LOCAL_DIR/MyApp.app.dSYM" "$S3_BUCKET/MyApp.app.dSYM" \
--recursive --storage-class STANDARD_IA
aws s3 cp "$LOCAL_DIR/TestResults.xcresult" "$S3_BUCKET/TestResults.xcresult" \
--recursive --storage-class STANDARD_IA
STANDARD_IA is cheaper per GB than STANDARD for data you read only during incident investigation; confirm the current per-class price for your region before you budget.
GitHub Actions can also hold a copy via actions/upload-artifact with a per-artifact retention-days, which cannot exceed the limit set at the repository, organization, or enterprise level. See GitHub's artifact retention documentation for the current defaults and maximums in your plan.
# .github/workflows/nightly-cleanup.yml
name: Nightly artifact cleanup
on:
schedule:
- cron: '0 3 * * *' # 03:00 UTC daily
jobs:
cleanup:
runs-on: [self-hosted, macOS]
steps:
- name: Run artifact cleanup
run: |
chmod +x "$HOME/scripts/cleanup-artifacts.sh"
"$HOME/scripts/cleanup-artifacts.sh" "$HOME/ci-artifacts" 30
steps:
- label: ":broom: Nightly artifact cleanup"
command: "$HOME/scripts/cleanup-artifacts.sh $HOME/ci-artifacts 30"
agents:
queue: nightly
Schedule the pipeline at a low-traffic hour using your CI system's cron feature.
Use the per-build sizes in the first table above with your own build count. For a team doing 50 iOS builds/day where each PR build drops ~400 MB of retained artifacts after DerivedData is cleaned:
| Stream | Per-day retained | 30-day accumulation |
|---|---|---|
| PR IPAs + xcresults (14-day) | 50 × 400 MB = 20 GB/day retained | ~280 GB rolling peak (14 days) |
| Main dSYMs (90-day, ~5 main builds/day × 50 MB) | 250 MB/day | ~22.5 GB (90 days) |
| Release archives (local, 7-day, ~1/week × 1 GB) | ~140 MB/day avg | ~1 GB rolling |
| Suggested headroom | — | ~320 GB working set |
Divide each row by your actual build count to scale. If your rolling peak exceeds the disk you have, either tighten the PR window (14 → 7 days roughly halves the PR row) or move PR xcresults to S3 at upload time and keep only pointers locally.
For teams that need more headroom than a single machine can give, the Figment Managed Build Mac ($249/month) includes CI setup and monitoring on an M-series Mac; specify disk sizing in your request. Physical M-series starts at $95/month with configuration confirmed in the quote. For a cheaper build-pipeline sandbox, the $29 virtual Intel macOS environment runs compatible Intel Mac toolchains — including supported Xcode 26-or-earlier versions — and is useful for the cleanup/upload scripts themselves, which do not require Apple silicon. ARM device targets and Xcode 27 require an M-series machine.
For the related cache tactic that stops DerivedData from ever reaching this script, see Incremental Xcode Builds via Derived Data Cache. For tvOS output quirks that change the size table above, see Automate tvOS App Builds in a CI Pipeline. If your developers drive these runners from Windows, see Xcode on Windows Remote Setup.
Request a managed Mac CI runner → · See all Mac hosting plans