DEV Community

Todd Sullivan
Todd Sullivan

Posted on

The Fastlane gym Export Options Trap (and Why Your Provisioning Profile Is Being Silently Ignored)

Spent a few hours last week debugging a CI failure that had no right to be as subtle as it was. The build archived fine, but exportArchive kept dying with:

error: exportArchive: requires a provisioning profile with the App Groups feature.
Enter fullscreen mode Exit fullscreen mode

The frustrating part: the AppStore provisioning profile was correct. I had just renewed it, decrypted it on the runner, and confirmed the App Group entitlement was in there. The keychain had it. So why was xcodebuild not finding it?

The Trap

The Fastlane gym action accepts export_options: in two forms:

  1. A path to an existing .plist file
  2. A Hash of options it will write to a temp plist

I was passing a Hash — and inside that Hash I had a plist: key pointing to my own plist file, thinking gym would merge or defer to it. It does not.

When you pass a Hash, gym writes that Hash to a temp plist and hands it directly to xcodebuild. The plist: key inside the Hash is not special — xcodebuild does not recognise it, ignores it silently, and you end up with a minimal plist that has no provisioningProfiles key at all.

The temp plist gym generated looked like this:

<dict>
  <key>method</key>
  <string>app-store</string>
  <key>uploadSymbols</key>
  <true/>
  <key>plist</key>
  <string>RELEASE_exportOptionsPlist_Store.plist</string>
</dict>
Enter fullscreen mode Exit fullscreen mode

No provisioningProfiles. Under manual signing, xcodebuild fell back to automatic profile resolution at export time — which on a clean GitHub Actions runner cannot find the app-group-bearing profile you carefully installed. Build fails. Misleading error. Whole thing looks like a profile problem when the profile was never consulted.

The Fix

Pass export_options: as a path string, not a Hash:

gym(
  scheme: "MyApp",
  configuration: "Release",
  export_options: "./fastlane/RELEASE_exportOptionsPlist_Store.plist"
)
Enter fullscreen mode Exit fullscreen mode

Your plist should include explicit provisioningProfiles:

<key>provisioningProfiles</key>
<dict>
  <key>com.example.myapp</key>
  <string>MyApp AppStore Profile</string>
</dict>
Enter fullscreen mode Exit fullscreen mode

Gym passes the path straight to xcodebuild -exportOptionsPlist. Your file is read. No temp plist, no silent key stripping.

Why This Catches People Out

The Hash form is in basically every Fastlane tutorial. It looks clean. Gym does not warn you when it discards unrecognised keys. The only signal is in verbose gym output — if you compare the temp plist it writes against what you expected, the provisioningProfiles block is missing.

App Groups make the failure mode worse because they require an exact profile match. Without entitlements like App Groups, xcodebuild automatic selection might accidentally find something usable. With App Groups, it always fails hard.

What I Do Now

For any iOS app with entitlements — App Groups, Push Notifications, iCloud, anything — I keep an explicit export_options.plist checked into the repo and pass it as a path. The Hash form is fine for a basic app. The moment signing gets complicated, you want the plist under version control and gym out of the business of generating it.

One less thing the CI runner has to figure out on its own.

Top comments (0)