Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

CI Artifact Storage and Retention on a Mac Runner

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.

What Fills a CI Mac Disk

Understanding what takes the most space helps you target the right directories:

Artifact typeTypical size per buildSafe to delete after?
DerivedData intermediates1–4 GBYes, after build completes
.xcarchive bundle200–800 MBAfter IPA is exported and uploaded
.ipa file20–200 MBAfter 30 days (keep release builds)
.app.dSYM bundle10–100 MBNever delete release dSYMs; 30 days for PR builds
.xcresult test bundle100 MB–2 GBAfter 7–14 days
Simulator runtimes5–12 GB eachOnly 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.

Retention Policy Design

A practical three-tier policy:

TierRuleStorage location
PR / branch buildsDelete all artifacts after 14 daysLocal disk only
Main branch buildsDelete after 30 days, keep dSYMs for 90 daysLocal disk; dSYMs copied to S3 at upload
Release-tagged buildsKeep foreverUpload 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.

The cleanup-artifacts.sh Script

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.

Upload Release Artifacts Before Deleting Locally

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.

Wiring the Cleanup as a Nightly CI Job

GitHub Actions

# .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

Buildkite

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.

Disk Sizing Worksheet (50 builds/day example)

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:

StreamPer-day retained30-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


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms