apple signing: App Group association fails early under automatic signing too (API capability + newest profile, steps and deep link, exit before the archive); console plan/verify apple-app-group

This commit is contained in:
CRTkafa 2026-10-25 22:34:14 +07:00
parent d978bb7a8b
commit 304407e523
20 changed files with 590 additions and 60 deletions

View file

@ -6,8 +6,8 @@
// [--keychain <path>] [--keychain-unlock] [--allow-ssh] [--allow-app-group-gap]
// Over SSH (no GUI session) codesign needs an unlocked keychain: --keychain-unlock unlocks it with the password in
// STOREBOUND_KEYCHAIN_PASSWORD (never printed) and sets its key partition list; --keychain <path> signs with a build
// keychain instead of the login keychain. Manual signing: stops before the build when a target's App Group is not in its
// installed profile (the developer portal assigns groups; storebound apple signing status/ensure names the steps).
// keychain instead of the login keychain. App Groups: stops before the archive when a target's App Group is not proven (manual: its
// installed profile; automatic: the App ID's capability and the newest profile the API lists; the portal assigns groups: console plan apple-app-group).
// First the prebuild (build.prebuild / apple.prebuild: e.g. the web build + npx cap sync; once per flow run), then a
// Capacitor project's copied web assets (ios/App/App/public) must be newer than the source, else it stops.
// Finds the workspace / project and scheme (native, Capacitor ios/App, React Native ios/<Name>, Flutter ios/Runner,
@ -30,13 +30,13 @@ import { parseMobileprovision, toXmlPlist } from '../lib/plist';
import { nextBuildNumber } from '../lib/release';
import { exportOptions, profileDirs } from '../lib/signing';
import { keychainPassword, keychainSigningArgs, parseKeychainList, showSecurity, sshBuildRefusal, unlockSteps } from '../lib/keychain';
import { appGroupGapsFor, appGroupSteps } from '../lib/targets';
import { appGroupSteps, appGroupVerdicts, type GroupEvidence } from '../lib/targets';
import { capacitorProject, webStep } from '../lib/web-assets';
import { archiveArgs, type AscAuth, distributionErrors, exportArgs, foreignRsync, remoteBuildSession, xcodePath, flutterConfigArgs, ipaInfo, ipaVersionProblem, pickXcode, scanXcode, showCmd } from '../lib/xcode';
import { bundleTrackingIssues, ipaManifests } from '../lib/privacy-files';
import { ascClient } from './asc';
import { parsePlatform } from './platform';
import { discoverTargets, findBundleId } from './signing';
import { discoverTargets, findBundleId, readGroupEvidence } from './signing';
import * as rt from '../lib/rt';
import { shipPhase } from '../lib/ship-step';
@ -179,12 +179,27 @@ export async function run(ctx: Ctx): Promise<void> {
if (p) profiles[id] = p.uuid; else if (!dry) throw new Error(`no installed App Store profile for ${id}: storebound apple signing ensure`);
else warn(TAG, `no installed App Store profile for ${id} (storebound apple signing ensure)`);
}
// App Groups: a manual-signing profile carries a group only when the portal assigned it to the bundle id
const groupGaps = style === 'manual' ? appGroupGapsFor([...(scan.main ? [scan.main] : []), ...scan.extensions], id => { const p = installedProfiles(id, teamId)[0]; return p ? p.entitlements : null; }, () => null) : [];
for (const g of groupGaps) {
const msg = `${g.bundleId}: the installed profile lacks App Group(s) ${g.groups.join(', ')}`;
if (a.has('allow-app-group-gap') || dry) warn(TAG, msg);
else throw new Error(`${msg}; codesign would refuse the build.\n${appGroupSteps(g).slice(1).join('\n')}\n(--allow-app-group-gap builds anyway)`);
// App Groups: a profile carries a group only when the portal assigned it to the bundle id (the API cannot). Manual
// signing: the installed profile; automatic: the App ID's capability and the newest profile the API lists. A gap stops
// the build before the archive; an association nothing can prove is warned with the same steps.
const groupTargets = [...(scan.main ? [scan.main] : []), ...scan.extensions];
let groupEvidence: ((id: string) => GroupEvidence | null) | null = null;
if (groupTargets.some(t => t.appGroups.length)) {
if (style === 'manual') groupEvidence = id => { const p = installedProfiles(id, teamId)[0]; return p ? { resourceId: null, capabilityOn: null, profileEnt: p.entitlements } : null; };
else {
try { groupEvidence = await readGroupEvidence(await ascClient(root, bundleId, config.apple?.appId), groupTargets, 'automatic', signing.profileType ?? 'IOS_APP_STORE'); }
catch (e) { warn(TAG, `App Group association not read from App Store Connect (${(e as Error).message.split('\n')[0]?.slice(0, 120)})`); }
}
}
const groupVerdicts = appGroupVerdicts(groupTargets, groupEvidence ?? (() => null));
for (const g of groupVerdicts.unproven) for (const l of appGroupSteps(g)) warn(TAG, l);
for (const g of groupVerdicts.gaps) {
const msg = `${g.bundleId}: ${g.why}`;
if (a.has('allow-app-group-gap') || dry) { warn(TAG, msg); for (const l of appGroupSteps(g).slice(1)) warn(TAG, l); }
else {
const auto = style === 'automatic' ? 'Automatic signing: Xcode renews its managed profile at archive time, so if you just ticked the group and the API still lists the old profile, build with --allow-app-group-gap.' : '';
throw new Error([`${msg}; codesign would refuse the build (stopped before the archive).`, ...appGroupSteps(g).slice(1), ...(auto ? [auto] : []), '(--allow-app-group-gap builds anyway)'].join(String.fromCharCode(10)));
}
}
const opts = exportOptions({ teamId: teamId ?? '<TEAM_ID>', style, profiles: style === 'manual' ? (Object.keys(profiles).length ? profiles : { [bundleId]: '<profile uuid>' }) : undefined });
const plist = toXmlPlist(opts);