Make iOS release signing deterministic

This commit is contained in:
2026-08-15 14:18:09 -04:00
parent b9c7fb325a
commit 5fa30f365f
4 changed files with 75 additions and 27 deletions
+3
View File
@@ -9,6 +9,9 @@ APPLE_TEAM_ID=
# #
# If export fails with "profile doesn't include signing certificate", regenerate App Store # If export fails with "profile doesn't include signing certificate", regenerate App Store
# profiles at developer.apple.com for com.beenvoice.app and com.beenvoice.app.ExpoWidgetsTarget, # profiles at developer.apple.com for com.beenvoice.app and com.beenvoice.app.ExpoWidgetsTarget,
# Optional overrides when those App Store profile names differ from the bundle IDs:
# IOS_MAIN_APPSTORE_PROFILE_NAME=com.beenvoice.app
# IOS_WIDGET_APPSTORE_PROFILE_NAME=com.beenvoice.app.ExpoWidgetsTarget
# then re-run the full release (not --export-only). # then re-run the full release (not --export-only).
# Production API baked into the JS bundle (App Store / TestFlight) # Production API baked into the JS bundle (App Store / TestFlight)
+38 -16
View File
@@ -2,21 +2,24 @@
const { withXcodeProject } = require("@expo/config-plugins"); const { withXcodeProject } = require("@expo/config-plugins");
const RELEASE_SIGN_KEY = '"CODE_SIGN_IDENTITY[sdk=iphoneos*]"'; const RELEASE_SIGN_KEY = '"CODE_SIGN_IDENTITY[sdk=iphoneos*]"';
const MAIN_BUNDLE_ID = "com.beenvoice.app";
const WIDGET_BUNDLE_ID = "com.beenvoice.app.ExpoWidgetsTarget";
/** /**
* RN / Expo sets Release CODE_SIGN_IDENTITY to "iPhone Developer", which forces * Keep App Store archives on distribution signing. Automatic signing can select
* development-signed archives. Remove it so automatic signing picks Distribution * an Apple Development identity for the widget target when archiving from the CLI,
* for App Store archives. * so both release targets use their explicit App Store profiles instead.
*/ */
/** @type {import('@expo/config-plugins').ConfigPlugin} */ function configureReleaseSigning(project) {
function withAppStoreSigning(config) {
return withXcodeProject(config, (config) => {
const project = config.modResults;
const configurations = project.pbxXCBuildConfigurationSection(); const configurations = project.pbxXCBuildConfigurationSection();
for (const key of Object.keys(configurations)) { for (const key of Object.keys(configurations)) {
const buildConfig = configurations[key]; const buildConfig = configurations[key];
if (!buildConfig || typeof buildConfig !== "object" || !buildConfig.buildSettings) { if (
!buildConfig ||
typeof buildConfig !== "object" ||
!buildConfig.buildSettings
) {
continue; continue;
} }
@@ -24,19 +27,38 @@ function withAppStoreSigning(config) {
continue; continue;
} }
const identity = buildConfig.buildSettings[RELEASE_SIGN_KEY]; const bundleId = String(
if ( buildConfig.buildSettings.PRODUCT_BUNDLE_IDENTIFIER ?? "",
identity === "iPhone Developer" || ).replaceAll('"', "");
identity === '"iPhone Developer"' || if (bundleId !== MAIN_BUNDLE_ID && bundleId !== WIDGET_BUNDLE_ID) {
identity === "Apple Distribution" || continue;
identity === '"Apple Distribution"' }
) {
delete buildConfig.buildSettings[RELEASE_SIGN_KEY]; const profileName =
bundleId === WIDGET_BUNDLE_ID
? (process.env.IOS_WIDGET_APPSTORE_PROFILE_NAME ?? WIDGET_BUNDLE_ID)
: (process.env.IOS_MAIN_APPSTORE_PROFILE_NAME ?? MAIN_BUNDLE_ID);
buildConfig.buildSettings[RELEASE_SIGN_KEY] = '"Apple Distribution"';
buildConfig.buildSettings.CODE_SIGN_STYLE = "Manual";
buildConfig.buildSettings.PROVISIONING_PROFILE_SPECIFIER = `"${profileName}"`;
if (process.env.APPLE_TEAM_ID) {
buildConfig.buildSettings.DEVELOPMENT_TEAM = process.env.APPLE_TEAM_ID;
} }
} }
return project;
}
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withAppStoreSigning(config) {
return withXcodeProject(config, (config) => {
configureReleaseSigning(config.modResults);
return config; return config;
}); });
} }
module.exports = withAppStoreSigning; module.exports = withAppStoreSigning;
module.exports.configureReleaseSigning = configureReleaseSigning;
+20
View File
@@ -0,0 +1,20 @@
// @ts-check
const fs = require("fs");
const path = require("path");
const xcode = require("xcode");
const { configureReleaseSigning } = require("../plugins/withAppStoreSigning");
const projectPath = process.argv[2];
if (!projectPath) {
throw new Error(
"Usage: node scripts/configure-ios-signing.js <project.pbxproj>",
);
}
const resolvedPath = path.resolve(projectPath);
const project = xcode.project(resolvedPath);
project.parseSync();
configureReleaseSigning(project);
fs.writeFileSync(resolvedPath, project.writeSync());
console.log("Configured manual App Store signing for iOS release targets.");
+4 -1
View File
@@ -185,6 +185,10 @@ prepare_native_project() {
fi fi
) )
if [[ -f "$ROOT/$PROJECT/project.pbxproj" ]]; then
node "$ROOT/scripts/configure-ios-signing.js" "$ROOT/$PROJECT/project.pbxproj"
fi
resolve_xcode_workspace resolve_xcode_workspace
} }
@@ -241,7 +245,6 @@ archive_app() {
-archivePath "$ARCHIVE_PATH" \ -archivePath "$ARCHIVE_PATH" \
-destination "generic/platform=iOS" \ -destination "generic/platform=iOS" \
-allowProvisioningUpdates \ -allowProvisioningUpdates \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \ DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
"${API_AUTH_ARGS[@]}" \ "${API_AUTH_ARGS[@]}" \
archive archive