Xcode Archive can ship the wrong display name despite correct source settings
A production iOS archive had the production bundle ID and backend environment, but its CFBundleDisplayName was the beta name. The tagged source said otherwise, and App Store Connect never shows that name, so only an installed build revealed it. Add a Run Script phase that checks the processed app Info.plist, so a manual Product > Archive also rejects the wrong identity.
Using synthetic names, the mismatch was:
CFBundleIdentifier = com.example.notebook
CFBundleDisplayName = NotebookBeta
Source checks missed the artifact #
The investigation initially ruled out the mismatch from project settings and runtime configuration checks. That conclusion was too strong. The runtime check validated the product variant against the backend environment; it did not validate the display name. A production bundle with a beta display name could satisfy it.
Reading the archived app’s plist settled the question. Its name disagreed with the committed production settings. An uncommitted local edit was the likely explanation, but the archive alone could not establish exactly how the value changed.
The release flow had no step that showed the name either. App Store Connect metadata covers the store listing’s app name, not the bundle’s CFBundleDisplayName, which only appears under the home screen icon. The only check is to install the production build from TestFlight, or from the App Store after release, and read that label by hand. It is easy to overlook when the bundle ID and backend already look right.
Source-level regression tests still help, but manual Archive does not run the scheme’s Test action. A check confined to a deployment lane also misses someone archiving directly in Xcode.
Check the processed plist during the build #
Add a Run Script phase named Verify App Identity to the app target, after its existing phases. Use /bin/sh. Leave Run script only when installing unchecked.
Declare this Input File:
$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)
Declare this Output File:
$(DERIVED_FILE_DIR)/verified-app-identity.stamp
Uncheck Based on dependency analysis for this small guard. It should validate every build, including repeated archives. The input declaration still establishes the dependency on plist processing; visual phase order alone is insufficient. Apple’s Run Script documentation explains how declared inputs and outputs determine task ordering.
Paste this script, adapting the explicit configuration allowlist:
set -eu
: "${SCRIPT_OUTPUT_FILE_0:?Declare the stamp Output File}"
stamp="$SCRIPT_OUTPUT_FILE_0"
rm -f "$stamp"
: "${TARGET_BUILD_DIR:?}" "${INFOPLIST_PATH:?}" "${CONFIGURATION:?}"
plist="$TARGET_BUILD_DIR/$INFOPLIST_PATH"
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
case "$CONFIGURATION" in
Release)
expected_id=com.example.notebook
expected_name=Notebook
;;
Debug)
expected_id=com.example.notebook.beta
expected_name=NotebookBeta
;;
*) fail "No identity policy for configuration $CONFIGURATION" ;;
esac
[ -f "$plist" ] || fail "Missing processed Info.plist: $plist"
id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$plist") ||
fail 'Cannot read CFBundleIdentifier'
name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleDisplayName' "$plist") ||
fail 'Cannot read CFBundleDisplayName'
[ "$id" = "$expected_id" ] && [ "$name" = "$expected_name" ] ||
fail "$CONFIGURATION requires $expected_id / $expected_name; got $id / $name"
printf 'Verified app identity: %s / %s\n' "$id" "$name"
: > "$stamp"
Do not substitute the source INFOPLIST_FILE: generated keys and build-setting expansion can change the packaged values. Keep the expected identity independent of the settings under test.
The declared paths also describe the script’s file access for user-script sandboxing. Keep sandboxing enabled where possible and test these declarations with your Xcode version. The original target had it disabled. The stamp lives outside the signed app and is created only after validation. Removing it first, failing on missing values, and running unconditionally prevent an old stamp from becoming a false success after a failed check.
Prove that wrong builds fail #
The historical implementation passed normal builds across all four original configurations. Four deliberate mutations failed: the production ID, production name, beta ID, and beta name, changed separately. Each failure reported the mismatched identity; the project file was restored afterward. These were build checks, not evidence of a fresh Archive after the fix.
For your project, repeat those negative controls, then restore the settings and archive twice without cleaning. Confirm the guard runs both times and inspect the resulting archive’s app plist. Also test a missing key and an unrecognized configuration.
This checks the app’s base plist only. Localized InfoPlist.strings can affect the displayed name, and extensions need separate policies. Check the Archive action’s selected configuration in your scheme, then deliberately break its display name and confirm Archive fails before trusting the guard.