skip to content

Your iOS build_app export picks the wrong provisioning profile — how do you pin it?

level: seniorimportance: must knowfreq 56%

answer

  1. several installed profiles can match
  2. make the export deterministic, not lucky
  3. explicit map from bundle id to profile
  4. export_options plus skip_profile_detection
  5. match publishes the mapping to lane context

basics

~20 s

Stop the guessing and state the mapping: set skip_profile_detection in fastlane's build_app, pin the Apple team with export_team_id, and pass export_options carrying an explicit bundle-identifier-to-profile map, ideally the one sync_code_signing published into the lane context.

solid answer

~40 s

Xcode's export step will happily choose an installed iOS profile that merely *matches* the bundle identifier and `export_method`, and on a shared macOS build machine holding several teams' profiles that match is ambiguous. The fix is to leave nothing to detection. Set `skip_profile_detection: true` so `build_app` stops running its own detection pass, set `export_team_id` so the Apple Developer team is unambiguous, and pass `export_options` with an explicit map from each bundle identifier to a named profile. You rarely write that map by hand: `sync_code_signing` (`match`) publishes it as `Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]`, so the lane feeds one action's output into the next. Remember every embedded bundle — our lighthouse maintenance app's widget and notification extensions each need their own entry.

code

ruby · 14 lines
ruby
lane :ipa_for_store do
  sync_code_signing(type: "appstore", readonly: true)
  build_app(
    scheme: "LighthouseMaintenance",
    configuration: "Release",
    export_method: "app-store",
    skip_profile_detection: true,
    export_team_id: ENV['APPLE_TEAM_ID'],
    export_options: {
      signingStyle: "manual",
      provisioningProfiles: lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]
    }
  )
end

go deeper

for a junior

Know that the export needs a provisioning profile and that build_app does not create one. Recognise export_options as the place where profile choices are stated rather than left to Xcode.

for a middle

Explain why automatic detection is ambiguous on a machine holding several profiles, and name the three levers - skip_profile_detection, export_team_id, and an explicit provisioningProfiles map inside export_options.

for a senior

Wire it end to end: run sync_code_signing first, read its mapping out of the lane context, and make the export fail loudly on a missing profile rather than substitute one. Then say how you verify the artifact.

for a principal

Own the standard across apps - manual signing with an explicit mapping everywhere, profile names never hardcoded, and evidence retained per build so a signing question is answerable without a rebuild.

## Why the export picks the wrong profile at all When `build_app` (alias `gym`) reaches its export phase, Xcode has to decide which Apple provisioning profile to re-sign the archived iOS app with. Left to itself it searches the profiles installed on the machine for one that matches the app's bundle identifier and the chosen `export_method`. On a developer's laptop that search usually has one answer. On a shared macOS build machine it frequently has several: two Apple Developer teams, an expired profile that still matches, a wildcard profile, a profile left over from the app's previous bundle identifier. Xcode picks one, the build goes green, and the mismatch surfaces later as an entitlement that does not work or an upload that is refused. The cure is not to install fewer profiles and hope. It is to make the export **deterministic**, so the run either uses the profile you named or fails loudly. ## Pin the mapping through `export_options` `export_options` is `build_app`'s hand-off to Xcode's export-options property list. You can give it a path to a `.plist` file or an inline hash. Two of Xcode's keys do the work here: - `provisioningProfiles` — a map from **bundle identifier** to **profile name**. This is the explicit statement that removes the ambiguity. - `signingStyle` — set to manual signing, so Xcode is not permitted to go looking for something else on your behalf. Those key names belong to Xcode's export-options plist rather than to fastlane's own option list, so treat `xcodebuild -help` as the authority on the currently accepted set rather than trusting a memorised snippet. ## Let `sync_code_signing` hand you the map The honest lane does not hardcode profile names, because they change whenever a certificate is regenerated. `sync_code_signing` (`match`) — the action that installs the team's Apple signing assets — publishes the mapping it just installed into fastlane's shared lane context under `SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING`. Read it straight back out in the next action: 1. Call `sync_code_signing` for the type that matches your channel, in read-only mode on an unattended machine. 2. Call `build_app` with `export_options` whose `provisioningProfiles` value is that lane-context mapping. 3. The export now signs with exactly the profiles installed seconds earlier, by name, with no search. That single wiring removes a whole class of "it built with the wrong profile" incidents, because there is no longer a step in which anything gets chosen. ## Turn off the remaining guessing - `skip_profile_detection` — stops `build_app` running its own profile-detection pass before handing over to Xcode. Once you supply the mapping yourself, detection can only disagree with you. - `export_team_id` — names the Apple Developer team the export signs under. Essential when the machine's keychain and profiles span more than one team, because two teams can hold profiles for similar identifiers. - `codesigning_identity` — pins the signing certificate by name, a separate axis from the profile. - `skip_codesigning` — the opposite intent: archive without signing at all, for a compile-only check. It produces nothing distributable, so it is not a fix for a signing mismatch. ## Extensions are separate bundle identifiers This is the detail that catches people. An iOS app that embeds extensions is **several** signed bundles, and each needs its own entry in the mapping. Our lighthouse maintenance app ships: - the main app, - a lamp-status widget extension, - a notification-service extension for outage alerts. That is three bundle identifiers and three profiles. A mapping naming only the main app leaves Xcode guessing for the other two, which reproduces the original problem somewhere nobody looks. When `sync_code_signing` created profiles for all three, the mapping it publishes already contains all three — another reason to pass the mapping rather than type it. ## How to prove it actually worked Do not accept a green build as evidence. Check the artifact and the log: - Read the export phase in the log at `buildlog_path`; it names the profile that was used. - Inspect the `embedded.mobileprovision` file inside the exported `.ipa` — Apple's name for the profile actually baked in. - Make the failure mode loud: with manual signing and an explicit mapping, a missing or renamed profile fails the export instead of silently substituting another. That is the whole point of the change. - Keep `archive_path` and the log as retained artifacts, so a signing question weeks later is answered from evidence rather than from a rebuild. The senior framing to say out loud: automatic profile selection trades determinism for setup time, and on a shared build machine that trade is a bad one. Name the team, name the profile per bundle identifier, switch detection off, and let the export fail rather than improvise.

  • Why is passing the mapping from the lane context better than hardcoding profile names?
    Because profile names change whenever certificates are regenerated or a new device forces a new profile. `sync_code_signing` publishes the mapping for the profiles it just installed, so the export always references what is genuinely on the machine. A hardcoded name silently drifts, then surfaces as a signing error inside a completely unrelated change.
  • The app embeds two extensions. What breaks if the mapping names only the main app?
    Xcode still has to sign the extensions, so it falls back to searching for profiles matching their bundle identifiers. You get a build that is partly pinned and partly guessed, which can pass locally and fail on a shared machine holding another team's profiles. Every embedded bundle identifier needs its own entry.

saying these in an interview costs you the question

  • Says just delete the extra profiles from the machine
  • Hardcodes a profile name that changes on certificate renewal
  • Maps only the main app and forgets embedded extensions
  • Thinks skip_codesigning fixes a wrong-profile export
  • Assumes automatic signing is safe on a shared build machine
  • Treats a green build as proof the right profile was used