Skip to content

Commit 012e745

Browse files
committed
[eas-cli] Best-effort provisioning profile refresh with ASC API key in non-interactive iOS build
1 parent b5592e1 commit 012e745

9 files changed

Lines changed: 421 additions & 55 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages.
1010

1111
### 🐛 Bug fixes
1212

13+
- [eas-cli] Best-effort provisioning profile regeneration in non-interactive iOS builds using an App Store Connect API Key: when a profile is expired or invalid, the CLI attempts to authenticate with the key stored in EAS credentials or `EXPO_ASC_*` environment variables and regenerate it without requiring an interactive Apple login. ([#3805](https://github.com/expo/eas-cli/pull/3805) by [@sswrk](https://github.com/sswrk))
14+
1315
### 🧹 Chores
1416

1517
## [20.0.0](https://github.com/expo/eas-cli/releases/tag/v20.0.0) - 2026-05-29

packages/eas-cli/src/credentials/ios/actions/AscApiKeyUtils.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import path from 'path';
55
import { UserRole } from '@expo/apple-utils';
66

77
import { formatAppleTeam } from './AppleTeamFormatting';
8+
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
89
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
10+
import { AppStoreConnectApiKeyQuery } from '../../../graphql/queries/AppStoreConnectApiKeyQuery';
911
import Log, { learnMore } from '../../../log';
1012
import { confirmAsync, promptAsync, selectAsync } from '../../../prompts';
1113
import { fromNow } from '../../../utils/date';
@@ -14,7 +16,11 @@ import {
1416
getCredentialsFromUserAsync,
1517
shouldAutoGenerateCredentialsAsync,
1618
} from '../../utils/promptForCredentials';
19+
import { getAscApiKeyForAppSubmissionsAsync } from '../api/GraphqlClient';
20+
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
1721
import { AscApiKey } from '../appstore/Credentials.types';
22+
import { AppleTeamType, AuthenticationMode } from '../appstore/authenticateTypes';
23+
import { hasAscEnvVars } from '../appstore/resolveCredentials';
1824
import {
1925
AscApiKeyPath,
2026
MinimalAscApiKey,
@@ -277,3 +283,77 @@ export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): strin
277283
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
278284
return line;
279285
}
286+
287+
export async function resolveAscApiKeyForAppCredentialsAsync({
288+
graphqlClient,
289+
app,
290+
}: {
291+
graphqlClient: ExpoGraphqlClient;
292+
app: AppLookupParams;
293+
}): Promise<{
294+
ascApiKey: MinimalAscApiKey;
295+
teamId?: string;
296+
teamName?: string;
297+
} | null> {
298+
const ascKeyFragment = await getAscApiKeyForAppSubmissionsAsync(graphqlClient, app);
299+
if (!ascKeyFragment) {
300+
return null;
301+
}
302+
const fullKey = await AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, ascKeyFragment.id);
303+
return {
304+
ascApiKey: {
305+
keyP8: fullKey.keyP8,
306+
keyId: fullKey.keyIdentifier,
307+
issuerId: fullKey.issuerIdentifier,
308+
},
309+
teamId: ascKeyFragment.appleTeam?.appleTeamIdentifier,
310+
teamName: ascKeyFragment.appleTeam?.appleTeamName ?? undefined,
311+
};
312+
}
313+
314+
/**
315+
* Best-effort, side-effecting helper that ensures `ctx.appStore` is authenticated
316+
* with an App Store Connect API key when running in non-interactive mode.
317+
*
318+
* Returns true if `ctx.appStore.authCtx` is set after the call, false otherwise.
319+
* Never throws.
320+
*/
321+
export async function tryAuthenticateAppStoreWithEasAscApiKeyAsync(
322+
ctx: CredentialsContext,
323+
app: AppLookupParams,
324+
teamType: AppleTeamType
325+
): Promise<boolean> {
326+
if (ctx.appStore.authCtx) {
327+
return true;
328+
}
329+
try {
330+
if (hasAscEnvVars()) {
331+
await ctx.appStore.ensureAuthenticatedAsync({
332+
mode: AuthenticationMode.API_KEY,
333+
teamType,
334+
});
335+
return !!ctx.appStore.authCtx;
336+
}
337+
const resolvedKey = await resolveAscApiKeyForAppCredentialsAsync({
338+
graphqlClient: ctx.graphqlClient,
339+
app,
340+
});
341+
if (!resolvedKey) {
342+
return false;
343+
}
344+
Log.log('Using App Store Connect API Key from EAS credentials service.');
345+
await ctx.appStore.ensureAuthenticatedAsync({
346+
mode: AuthenticationMode.API_KEY,
347+
ascApiKey: resolvedKey.ascApiKey,
348+
teamId: resolvedKey.teamId,
349+
teamName: resolvedKey.teamName,
350+
teamType,
351+
});
352+
return !!ctx.appStore.authCtx;
353+
} catch (err: any) {
354+
Log.warn(
355+
`Failed to authenticate with the App Store Connect API key from EAS credentials service: ${err.message ?? err}`
356+
);
357+
return false;
358+
}
359+
}

packages/eas-cli/src/credentials/ios/actions/ConfigureProvisioningProfile.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
1717
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
1818
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
19-
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
19+
import { AuthCtx } from '../appstore/authenticateTypes';
2020
import { Target } from '../types';
2121

2222
export class ConfigureProvisioningProfile {
@@ -34,12 +34,9 @@ export class ConfigureProvisioningProfile {
3434
throw new ForbidCredentialModificationError(
3535
'Remove the --freeze-credentials flag to configure a Provisioning Profile.'
3636
);
37-
} else if (
38-
ctx.nonInteractive &&
39-
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
40-
) {
37+
} else if (ctx.nonInteractive && !ctx.appStore.authCtx) {
4138
throw new InsufficientAuthenticationNonInteractiveError(
42-
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
39+
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. Either set the EXPO_ASC_API_KEY_PATH/EXPO_ASC_KEY_ID/EXPO_ASC_ISSUER_ID environment variables, or configure an App Store Connect API Key for this app on EAS. ${learnMore(
4340
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
4441
)}`
4542
);

packages/eas-cli/src/credentials/ios/actions/CreateProvisioningProfile.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
1414
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
1515
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
1616
import { ProvisioningProfile } from '../appstore/Credentials.types';
17-
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
17+
import { AuthCtx } from '../appstore/authenticateTypes';
1818
import { provisioningProfileSchema } from '../credentials';
1919
import { Target } from '../types';
2020

@@ -30,12 +30,9 @@ export class CreateProvisioningProfile {
3030
throw new ForbidCredentialModificationError(
3131
'Run this command again without the --freeze-credentials flag in order to generate a new Provisioning Profile.'
3232
);
33-
} else if (
34-
ctx.nonInteractive &&
35-
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
36-
) {
33+
} else if (ctx.nonInteractive && !ctx.appStore.authCtx) {
3734
throw new InsufficientAuthenticationNonInteractiveError(
38-
`In order to generate a new Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
35+
`In order to generate a new Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. Either set the EXPO_ASC_API_KEY_PATH/EXPO_ASC_KEY_ID/EXPO_ASC_ISSUER_ID environment variables, or configure an App Store Connect API Key for this app on EAS. ${learnMore(
3936
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
4037
)}`
4138
);

packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
filterDevicesForApplePlatform,
1212
formatDeviceLabel,
1313
} from './DeviceUtils';
14+
import { resolveAscApiKeyForAppCredentialsAsync } from './AscApiKeyUtils';
1415
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
1516
import DeviceCreateAction, { RegistrationMethod } from '../../../devices/actions/create/action';
1617
import {
@@ -22,8 +23,6 @@ import {
2223
IosAppBuildCredentialsFragment,
2324
IosDistributionType,
2425
} from '../../../graphql/generated';
25-
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
26-
import { AppStoreConnectApiKeyQuery } from '../../../graphql/queries/AppStoreConnectApiKeyQuery';
2726
import Log from '../../../log';
2827
import { getApplePlatformFromTarget } from '../../../project/ios/target';
2928
import {
@@ -35,13 +34,11 @@ import {
3534
import differenceBy from '../../../utils/expodash/differenceBy';
3635
import { CredentialsContext } from '../../context';
3736
import { MissingCredentialsNonInteractiveError } from '../../errors';
38-
import { getAscApiKeyForAppSubmissionsAsync } from '../api/GraphqlClient';
3937
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
4038
import { AppleTeamType, AuthenticationMode } from '../appstore/authenticateTypes';
4139
import { ProvisioningProfile } from '../appstore/Credentials.types';
4240
import { ApplePlatform } from '../appstore/constants';
4341
import { hasAscEnvVars } from '../appstore/resolveCredentials';
44-
import { MinimalAscApiKey } from '../credentials';
4542
import { Target } from '../types';
4643
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
4744

@@ -424,6 +421,7 @@ export class SetUpAdhocProvisioningProfile {
424421
);
425422
}
426423

424+
Log.log('Using App Store Connect API Key from EAS credentials service.');
427425
await ctx.appStore.ensureAuthenticatedAsync({
428426
mode: AuthenticationMode.API_KEY,
429427
ascApiKey: resolvedKey.ascApiKey,
@@ -436,35 +434,6 @@ export class SetUpAdhocProvisioningProfile {
436434
}
437435
}
438436

439-
async function resolveAscApiKeyForAppCredentialsAsync({
440-
graphqlClient,
441-
app,
442-
}: {
443-
graphqlClient: ExpoGraphqlClient;
444-
app: AppLookupParams;
445-
}): Promise<{
446-
ascApiKey: MinimalAscApiKey;
447-
teamId?: string;
448-
teamName?: string;
449-
} | null> {
450-
const ascKeyFragment = await getAscApiKeyForAppSubmissionsAsync(graphqlClient, app);
451-
if (!ascKeyFragment) {
452-
return null;
453-
}
454-
455-
Log.log('Using App Store Connect API Key from EAS credentials service.');
456-
const fullKey = await AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, ascKeyFragment.id);
457-
return {
458-
ascApiKey: {
459-
keyP8: fullKey.keyP8,
460-
keyId: fullKey.keyIdentifier,
461-
issuerId: fullKey.issuerIdentifier,
462-
},
463-
teamId: ascKeyFragment.appleTeam?.appleTeamIdentifier,
464-
teamName: ascKeyFragment.appleTeam?.appleTeamName ?? undefined,
465-
};
466-
}
467-
468437
export function doUDIDsMatch(udidsA: string[], udidsB: string[]): boolean {
469438
const setA = new Set(udidsA);
470439
const setB = new Set(udidsB);

packages/eas-cli/src/credentials/ios/actions/SetUpProvisioningProfile.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import nullthrows from 'nullthrows';
22

3+
import { tryAuthenticateAppStoreWithEasAscApiKeyAsync } from './AscApiKeyUtils';
34
import {
45
assignBuildCredentialsAsync,
56
getBuildCredentialsAsync,
@@ -15,7 +16,7 @@ import {
1516
IosAppBuildCredentialsFragment,
1617
IosDistributionType,
1718
} from '../../../graphql/generated';
18-
import { learnMore } from '../../../log';
19+
import Log, { learnMore } from '../../../log';
1920
import { getApplePlatformFromTarget } from '../../../project/ios/target';
2021
import { confirmAsync } from '../../../prompts';
2122
import { CredentialsContext } from '../../context';
@@ -25,7 +26,8 @@ import {
2526
} from '../../errors';
2627
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
2728
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
28-
import { AuthenticationMode } from '../appstore/authenticateTypes';
29+
import { resolveAppleTeamTypeFromEnvironment } from '../appstore/resolveCredentials';
30+
import { AppleTeamType } from '../appstore/authenticateTypes';
2931
import { Target } from '../types';
3032
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
3133

@@ -102,21 +104,39 @@ export class SetUpProvisioningProfile {
102104
this.app,
103105
this.distributionType
104106
).runAsync(ctx);
107+
if (ctx.nonInteractive && !ctx.appStore.authCtx) {
108+
await tryAuthenticateAppStoreWithEasAscApiKeyAsync(
109+
ctx,
110+
this.app,
111+
this.resolveTeamTypeForAuthentication()
112+
);
113+
}
105114

106-
const areBuildCredentialsSetup = await this.areBuildCredentialsSetupAsync(ctx);
115+
let areBuildCredentialsSetup: boolean;
116+
try {
117+
areBuildCredentialsSetup = await this.areBuildCredentialsSetupAsync(ctx);
118+
} catch (error: any) {
119+
if (ctx.nonInteractive) {
120+
Log.warn(
121+
`Skipping Provisioning Profile validation on Apple Servers because the Apple API request failed: ${error.toString()}`
122+
);
123+
areBuildCredentialsSetup = true;
124+
} else {
125+
throw error;
126+
}
127+
}
107128
if (areBuildCredentialsSetup) {
108129
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, this.distributionType));
109130
}
110131
if (ctx.freezeCredentials) {
111132
throw new ForbidCredentialModificationError(
112133
'Provisioning profile is not configured correctly. Remove the --freeze-credentials flag to configure it.'
113134
);
114-
} else if (
115-
ctx.nonInteractive &&
116-
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
117-
) {
135+
}
136+
137+
if (ctx.nonInteractive && !ctx.appStore.authCtx) {
118138
throw new InsufficientAuthenticationNonInteractiveError(
119-
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
139+
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. Either set the EXPO_ASC_API_KEY_PATH/EXPO_ASC_KEY_ID/EXPO_ASC_ISSUER_ID environment variables, or configure an App Store Connect API Key for this app on EAS. ${learnMore(
120140
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
121141
)}`
122142
);
@@ -167,6 +187,24 @@ export class SetUpProvisioningProfile {
167187
return updatedProfile;
168188
}
169189

190+
/**
191+
* The team type determines `team.inHouse`, which in turn selects the Apple profile
192+
* type used for every subsequent profile lookup and creation (IOS_APP_INHOUSE for
193+
* enterprise vs IOS_APP_STORE otherwise). We derive it from the distribution
194+
* type, which is exactly what the requested operation needs: enterprise
195+
* builds require an in-house team, other distribution types don't.
196+
* A genuine team/distribution mismatch is rejected by Apple regardless of this value.
197+
*/
198+
private getDerivedTeamTypeForAuthentication(): AppleTeamType {
199+
return this.distributionType === IosDistributionType.Enterprise
200+
? AppleTeamType.IN_HOUSE
201+
: AppleTeamType.COMPANY_OR_ORGANIZATION;
202+
}
203+
204+
private resolveTeamTypeForAuthentication(): AppleTeamType {
205+
return resolveAppleTeamTypeFromEnvironment() ?? this.getDerivedTeamTypeForAuthentication();
206+
}
207+
170208
private getCurrentProfileStoreInfo(
171209
profiles: ProvisioningProfileStoreInfo[],
172210
currentProfile: AppleProvisioningProfileFragment

0 commit comments

Comments
 (0)