The fastlane Matchfile Bundle ID Trap That Killed My CI After Adding a Widget
TL;DR: If you add a widget / app extension / Action Extension to your iOS project, fastlane match will silently skip its provisioning profile unless you explicitly list the widget's bundle ID in Matchfile. CI will fail at build_app with "Provisioning profile doesn't support the App Group" — and the error message points you at the main app's profile, not the missing widget profile, so you spend a day chasing the wrong bug.
How It Killed My CI
DaysUntil v1.0.7 shipped fine. v1.0.8 added a widget extension. Pushed tag. CI failed at build_app:
::error file=...DaysUntil.xcodeproj::Provisioning profile
"match AppStore com.jiejuefuyou.daysuntil 1778731087" doesn't
support the group.com.jiejuefuyou.daysuntil App Group.
(in target 'DaysUntilWidget' from project 'DaysUntil')
The error names the main app's provisioning profile. My first interpretation: the App Group capability wasn't registered. So I logged into the Apple Developer Portal, added App Group, regenerated profiles. CI still failed.
Then I added the App Group capability via the ASC API:
curl -X POST https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities \
-H "Authorization: Bearer $JWT" \
-d '{
"data": {
"type": "bundleIdCapabilities",
"attributes": { "capabilityType": "APP_GROUPS" },
"relationships": { "bundleId": { "data": { "type": "bundleIds", "id": "6J52R36XL5" } } }
}
}'
# 201 success
Verified via API the capability was attached. CI still failed.
I ran gh workflow run init_signing.yml -f force=true to regenerate match profiles. CI still failed.
The fix turned out to be in fastlane's own config:
# fastlane/Matchfile (before)
app_identifier([ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil"])
# fastlane/Matchfile (after)
app_identifier([
ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil",
"com.jiejuefuyou.daysuntil.widget"
])
Plus the Fastfile sync_code_signing and update_code_signing_settings calls, which only referenced the main bundle.
The Mental Model That Was Wrong
I had assumed fastlane match was project-aware. That is, when you run match, it parses your Xcode project, finds every target with a bundle ID, and fetches profiles for all of them.
It does not.
fastlane match only manages profiles for bundles you explicitly list in Matchfile / pass to sync_code_signing.
So when you add a widget target via xcodegen (or Xcode GUI), fastlane is blind to it. The widget target gets no profile in your match storage. At build time, Xcode looks for a profile matching the widget's bundle ID + capabilities — and finds nothing. The next-best match it finds is the main app's profile (similar bundle ID prefix), which it tries to use, then fails because the main app's profile doesn't include the widget's bundle ID.
The error message names the main app's profile because that's the one Xcode tried. The actual problem is the widget profile that doesn't exist. The error message is technically true but maximally misleading.
The Complete Fix
For DaysUntil (and any project with a widget / app extension):
1. fastlane/Matchfile
app_identifier([
ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil",
"com.jiejuefuyou.daysuntil.widget"
])
2. fastlane/Fastfile
Define a WIDGET_BUNDLE_ID constant:
BUNDLE_ID = ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil"
WIDGET_BUNDLE_ID = "#{BUNDLE_ID}.widget"
Update init_signing and beta lanes' sync_code_signing:
sync_code_signing(
type: "appstore",
readonly: is_ci,
api_key: api_key,
app_identifier: [BUNDLE_ID, WIDGET_BUNDLE_ID]
)
Update update_code_signing_settings (call twice — once per bundle):
real_profile_name = ENV.fetch("sigh_#{BUNDLE_ID}_appstore_profile-name") {
"match AppStore #{BUNDLE_ID}"
}
widget_profile_name = ENV.fetch("sigh_#{WIDGET_BUNDLE_ID}_appstore_profile-name") {
"match AppStore #{WIDGET_BUNDLE_ID}"
}
update_code_signing_settings(
use_automatic_signing: false,
path: PROJECT,
team_id: ENV.fetch("TEAM_ID"),
bundle_identifier: BUNDLE_ID,
profile_name: real_profile_name,
code_sign_identity: "Apple Distribution"
)
update_code_signing_settings(
use_automatic_signing: false,
path: PROJECT,
team_id: ENV.fetch("TEAM_ID"),
bundle_identifier: WIDGET_BUNDLE_ID,
profile_name: widget_profile_name,
code_sign_identity: "Apple Distribution"
)
Update build_app's provisioningProfiles:
build_app(
# ... other options ...
export_options: {
method: "app-store",
provisioningProfiles: {
BUNDLE_ID => real_profile_name,
WIDGET_BUNDLE_ID => widget_profile_name
},
signingStyle: "manual",
teamID: ENV.fetch("TEAM_ID")
}
)
3. Regenerate match profiles
# Force-regen to pick up new bundle ID + current capabilities
gh workflow run init_signing.yml -f type=appstore -f force=true
# Wait for green, then push the next tag
git tag v1.0.9
git push origin v1.0.9
How to Audit Your Own Repos
I wrote a Python script that cross-checks fastlane config against xcodegen's project.yml. It flags any bundle in project.yml that's missing from Matchfile or sync_code_signing.
python orchestrator/lib/match_audit.py --all repos/
Output for my 5 repos before fixing:
=== autoapp-days-until ===
project.yml targets: 4 (['com.jiejuefuyou.daysuntil', 'com.jiejuefuyou.daysuntil.widget', ...])
Matchfile bundles: ['com.jiejuefuyou.daysuntil']
- error: Bundle(s) in project.yml but NOT in Matchfile: ['com.jiejuefuyou.daysuntil.widget']
=== autoapp-prompt-vault ===
project.yml targets: 4 (['com.jiejuefuyou.promptvault', 'com.jiejuefuyou.promptvault.ActionExtension', ...])
Matchfile bundles: ['com.jiejuefuyou.promptvault']
- error: Bundle(s) in project.yml but NOT in Matchfile: ['com.jiejuefuyou.promptvault.ActionExtension']
It correctly identified two repos with the same trap. The script is MIT under my autoapp repo.
Why This Bites Indie Devs Specifically
Big teams with full-time DevOps notice this on day 1 because they have a runbook for adding new targets. Indie devs adding a widget for the first time hit this on day N (the first time the widget needs a signed build, which is when you push the next TestFlight tag).
The error message points away from the real bug. Fastlane docs mention app_identifier accepts an array, but don't say "you MUST list every signed target."
I lost ~3 days of CI failures to this. The fix was 15 lines across 2 files.
If you're about to add your first widget / Live Activity / Today Extension / Watch app:
- Add the target via xcodegen.
- Open
fastlane/Matchfile. Add the new bundle ID. - Open
fastlane/Fastfile. Updatesync_code_signing+update_code_signing_settings+build_appfor the new bundle. - Run
init_signing.yml -f force=trueto regen profiles. - Then push your tag.
Or: drop my match_audit.py into your CI as a pre-commit / pre-push check.
Reusable Audit Script (excerpt)
import re, yaml
from pathlib import Path
def project_yml_bundles(repo: Path) -> set[str]:
data = yaml.safe_load((repo / "project.yml").read_text())
bundles = set()
for tgt, defn in (data.get("targets") or {}).items():
bid = (defn.get("settings") or {}).get("base", {}).get("PRODUCT_BUNDLE_IDENTIFIER")
if bid and not bid.endswith((".tests", ".uitests")):
bundles.add(bid)
return bundles
def matchfile_bundles(repo: Path) -> set[str]:
text = (repo / "fastlane" / "Matchfile").read_text()
m = re.search(r'app_identifier\s*\(\s*\[(.*?)\]\s*\)', text, re.DOTALL)
if not m: return set()
return set(re.findall(r'"(com\.[\w\.-]+)"', m.group(1)))
def audit(repo: Path):
missing = project_yml_bundles(repo) - matchfile_bundles(repo)
if missing:
print(f"{repo.name}: missing from Matchfile: {sorted(missing)}")
50 lines of Python catches a category of bugs that fastlane itself won't tell you about.
Cover Image Prompt (1000×420)
"A Xcode project navigator with three target icons: main app, widget extension, and Apple Watch app. A red 'missing' badge floats over the widget icon, pointing to a small Matchfile thumbnail labeled 'app_identifier: [...main...]' — a thread connecting the missing badge to the Matchfile, with a magnifying glass over the [...] showing the absent bundle. Editorial illustration style, muted colors."
Tags: fastlane, ios, ci, widget, gotcha
Internal cross-links:
- Previous article: dev.to 96 "Swift truncatingRemainder Trap"
- Series: "Indie iOS Lessons from 4 Apps in 75 Days"
- Repo: github.com/jiejuefuyou/autoapp
Top comments (0)