Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Auto-Upload dSYMs in CI for Crash Reporting

If a crash report arrives without readable stack frames, you are debugging blind. The fix is mechanical: every CI release build should upload its dSYM bundles immediately after xcodebuild archive, before the IPA leaves the pipeline. This guide gives you the exact shell steps for Firebase Crashlytics and Sentry, backed by Apple's Building your app to include debugging information and Firebase's Get deobfuscated crash reports.

Buying recommendation: run this pipeline on a Figment M-series Mac (from $95/month; exact chip, RAM and disk are confirmed in the quote — see /mac-hosting). M-series is the right call because recent iOS simulator images and Xcode 27 require Apple silicon. The $29 virtual Intel macOS VM (2 vCPU, 8 GiB RAM, 80 GiB disk) can run Xcode 26 or earlier and will archive and sign iOS targets that build with those toolchains; use it when you want a cheap shadow runner or a validation job, and promote release archives to the M-series host.

Why Symbolication Timing Matters

Xcode emits a dSYM alongside every binary. It maps raw memory addresses to file names, function names, and line numbers. Without the matching dSYM, Crashlytics and Sentry show hex offsets instead of Swift frames.

The hard constraint is ordering: the dSYM must reach the crash reporter before the first crash from that build version is ingested. If a TestFlight tester crashes within minutes of install, the window to upload after the fact is already closing. Pushing from CI right after the archive step removes the human loop.

Bitcode is no longer a reason to defer the upload. Apple deprecated Bitcode submissions starting with Xcode 14 (see Apple's Xcode 14 release notes), so the dSYM your runner produces is the final dSYM — there is no App Store recompilation step that invalidates it.

Where Xcode Writes dSYMs in a CI Archive

An .xcarchive has a fixed layout:

MyApp.xcarchive/
  dSYMs/
    MyApp.app.dSYM/
      Contents/
        Resources/
          DWARF/
            MyApp
    MyFramework.framework.dSYM/
      ...
  Products/
    Applications/
      MyApp.app

In CI, pin the archive path with -archivePath so later steps can find it deterministically:

xcodebuild archive \
  -scheme "MyApp" \
  -destination "generic/platform=iOS" \
  -archivePath "$RUNNER_TEMP/MyApp.xcarchive"

Replace MyApp with the scheme name in your own .xcodeproj/.xcworkspace. Export ARCHIVE_PATH="$RUNNER_TEMP/MyApp.xcarchive" so the upload step consumes the same value.

Finding dSYM Bundles Programmatically

Do not hard-code one .dSYM path. Embedded frameworks and SwiftPM dependencies emit their own:

find "$ARCHIVE_PATH/dSYMs" -name "*.dSYM" -type d

That gives one line per bundle. Pipe it into xargs to upload them all in a single pass.

The Complete Upload Script

Drop this in at ci/upload-dsyms.sh and call it as a post-archive step. It handles Firebase Crashlytics (via the upload-symbols helper bundled with the SDK) and Sentry (via sentry-cli debug-files upload). set -euo pipefail makes a failed upload fail the pipeline — no silent "green" build with missing symbols.

#!/usr/bin/env bash
# ci/upload-dsyms.sh
# Usage: bash ci/upload-dsyms.sh <path-to.xcarchive>
#
# Crashlytics env:
#   GOOGLE_SERVICE_INFO_PLIST  absolute path to GoogleService-Info.plist
#   FIREBASE_UPLOAD_SYMBOLS    absolute path to the upload-symbols binary
# Sentry env (set in CI secret manager, not in the repo):
#   SENTRY_AUTH_TOKEN
#   SENTRY_ORG
#   SENTRY_PROJECT
set -euo pipefail

ARCHIVE_PATH="${1:?Usage: $0 <path-to.xcarchive>}"
DSYM_DIR="${ARCHIVE_PATH}/dSYMs"

if [[ ! -d "${DSYM_DIR}" ]]; then
  echo "ERROR: dSYMs directory not found at ${DSYM_DIR}" >&2
  exit 1
fi

echo "==> dSYM bundles:"
find "${DSYM_DIR}" -name "*.dSYM" -type d

if [[ -n "${FIREBASE_UPLOAD_SYMBOLS:-}" && -n "${GOOGLE_SERVICE_INFO_PLIST:-}" ]]; then
  echo "==> Uploading to Firebase Crashlytics..."
  find "${DSYM_DIR}" -name "*.dSYM" -type d -print0 | \
    xargs -0 -I {} "${FIREBASE_UPLOAD_SYMBOLS}" \
      -gsp "${GOOGLE_SERVICE_INFO_PLIST}" \
      -p ios \
      {}
  echo "==> Firebase upload complete."
else
  echo "INFO: Crashlytics env not set; skipping."
fi

if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then
  echo "==> Uploading to Sentry..."
  sentry-cli debug-files upload \
    --auth-token "${SENTRY_AUTH_TOKEN}" \
    --org "${SENTRY_ORG:?SENTRY_ORG is required}" \
    --project "${SENTRY_PROJECT:?SENTRY_PROJECT is required}" \
    "${DSYM_DIR}"
  echo "==> Sentry upload complete."
else
  echo "INFO: SENTRY_AUTH_TOKEN not set; skipping."
fi

The script is an unexecuted example; validate it against your own project before trusting the exit codes. Credentials stay in the CI secret manager — nothing is written to disk, and no environment dump is produced.

Uploading to Firebase Crashlytics

The upload-symbols binary ships with FirebaseCrashlytics. Its path depends on how you integrate Firebase:

Integrationupload-symbols path
CocoaPods${PODS_ROOT}/FirebaseCrashlytics/upload-symbols
Swift Package Manager…/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols
Manual xcframeworkalongside the FirebaseCrashlytics.xcframework bundle

Point FIREBASE_UPLOAD_SYMBOLS at that path in your CI config. Required flags per the Firebase docs: -gsp <path-to-GoogleService-Info.plist> and -p ios, with the dSYM path as the trailing argument. Firebase's recommended invocation pattern is exactly the find … | xargs … upload-symbols -gsp … -p ios {} shape used above.

Uploading to Sentry

Install sentry-cli on the runner:

brew install getsentry/tools/sentry-cli
# or: curl -sL https://sentry.io/get-sentry-cli | bash

Sentry's current command is sentry-cli debug-files upload (the older upload-dif alias is being retired — see Sentry's Apple debug information files guide). The authoritative invocation:

sentry-cli debug-files upload \
  --auth-token "$SENTRY_AUTH_TOKEN" \
  --org "$SENTRY_ORG" \
  --project "$SENTRY_PROJECT" \
  "$ARCHIVE_PATH/dSYMs"

Add --wait on release jobs if you want the step to block until Sentry finishes processing and returns a definitive status.

Store dSYMs as a CI Artifact (Fallback)

Upload calls fail: tokens expire, services have incidents, networks hiccup. Archive the dSYMs/ directory as a CI artifact first so you can re-upload later without rerunning the whole build:

- name: Upload dSYMs as artifact
  uses: actions/upload-artifact@v4
  with:
    name: dsyms-${{ github.sha }}
    path: ${{ runner.temp }}/MyApp.xcarchive/dSYMs
    retention-days: 30

Tag the artifact with the commit SHA so you can match it to a specific build version weeks later.

Verifying Symbolication After the First CI Release

After the pipeline publishes a build:

  1. Install the TestFlight build on a device.
  2. Trigger a known crash gated behind a debug menu (e.g., fatalError("test crash")).
  3. Open the Crashlytics or Sentry dashboard a few minutes later.
  4. Confirm frames show file names and line numbers, not hex offsets.

If frames are still raw, check two things before anything else:

  • dwarfdump --uuid MyApp.app.dSYM matches the binary's UUID from the archive.
  • The CI job for that build reported a zero exit from the upload step.

A "Missing dSYM" banner in Crashlytics means the upload never landed or the wrong bundle was pushed for that UUID.

Pipeline Summary

StepAction
xcodebuild archiveWrites .xcarchive with dSYMs/
Store CI artifactCaptures dSYMs/ before any network call
upload-dsyms.shFinds every .dSYM and pushes to Crashlytics and/or Sentry
VerifyTest-crash on TestFlight shows symbolicated frames

Running archive, dSYM capture, and uploads on the same Figment Mac keeps intermediate paths local — no cross-runner artifact shuffling. Pick the M-series plan when you need Xcode 27 or Apple-silicon-only simulators; use the $29 Intel VM as an auxiliary runner for Xcode 26-or-earlier builds. Compare plans at /mac-hosting or start provisioning at /mac-hosting/start.

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms