From 8e7c668f47a4f7050eaa880dfe48a4e63b5c97ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Krzemie=C5=84?= Date: Mon, 13 Nov 2023 13:40:51 +0100 Subject: [PATCH] [eas-cli] [ENG-10555] Fix device provsioning (#2119) * [eas-cli] Fix provisioning Updates provisioning profile in EAS before using it if the profile id matches the one from apple but the list of devices does not See: https://linear.app/expo/issue/ENG-10555/ios-devices-get-stuck-in-failed-to-provision-state-until-the-developer * [eas-cli] Add test Added test for a newly added case See: https://linear.app/expo/issue/ENG-10555/ios-devices-get-stuck-in-failed-to-provision-state-until-the-developer * update CHANGELOG.md * [eas-cli] Remove unused import Remove unused import See: https://linear.app/expo/issue/ENG-10555/ios-devices-get-stuck-in-failed-to-provision-state-until-the-developer --- CHANGELOG.md | 2 + .../actions/SetUpAdhocProvisioningProfile.ts | 75 ++++++++++++++----- .../SetUpAdhocProvisioningProfile-test.ts | 75 +++++++++++++------ 3 files changed, 111 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ba1d492bb..cc0721e876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- Fixed provisioning of new devices into an existing profile. ([#2119](https://github.com/expo/eas-cli/pull/2119) by [@radoslawkrzemien](https://github.com/radoslawkrzemien)) + ### ๐Ÿงน Chores - Update `@expo/package-manager` to `1.1.2` to change package manager resolution order. ([#2118](https://github.com/expo/eas-cli/pull/2118) by [@szdziedzic](https://github.com/szdziedzic)) diff --git a/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts b/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts index 0e63a37f40..f6ef7ca2c9 100644 --- a/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts +++ b/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts @@ -5,6 +5,8 @@ import nullthrows from 'nullthrows'; import DeviceCreateAction, { RegistrationMethod } from '../../../devices/actions/create/action'; import { + AppleAppIdentifierFragment, + AppleDevice, AppleDeviceFragment, AppleDistributionCertificateFragment, AppleProvisioningProfileFragment, @@ -19,6 +21,7 @@ import differenceBy from '../../../utils/expodash/differenceBy'; import { CredentialsContext } from '../../context'; import { MissingCredentialsNonInteractiveError } from '../../errors'; import { AppLookupParams } from '../api/graphql/types/AppLookupParams'; +import { ProvisioningProfile } from '../appstore/Credentials.types'; import { ApplePlatform } from '../appstore/constants'; import { Target } from '../types'; import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile'; @@ -146,25 +149,14 @@ export class SetUpAdhocProvisioningProfile { ); let appleProvisioningProfile: AppleProvisioningProfileFragment | null = null; if (currentBuildCredentials?.provisioningProfile) { - if ( - currentBuildCredentials.provisioningProfile.developerPortalIdentifier !== - provisioningProfileStoreInfo.provisioningProfileId - ) { - await ctx.ios.deleteProvisioningProfilesAsync(ctx.graphqlClient, [ - currentBuildCredentials.provisioningProfile.id, - ]); - appleProvisioningProfile = await ctx.ios.createProvisioningProfileAsync( - ctx.graphqlClient, - app, - appleAppIdentifier, - { - appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile, - developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId, - } - ); - } else { - appleProvisioningProfile = currentBuildCredentials.provisioningProfile; - } + appleProvisioningProfile = await this.reuseCurrentProvisioningProfileAsync( + currentBuildCredentials.provisioningProfile, + provisioningProfileStoreInfo, + ctx, + app, + appleAppIdentifier, + chosenDevices + ); } else { appleProvisioningProfile = await ctx.ios.createProvisioningProfileAsync( ctx.graphqlClient, @@ -183,7 +175,7 @@ export class SetUpAdhocProvisioningProfile { appleProvisioningProfile.appleDevices, 'identifier' ); - if (diffList && diffList.length > 0) { + if (diffList.length > 0) { Log.warn(`Failed to provision ${diffList.length} of the selected devices:`); for (const missingDevice of diffList) { Log.warn(`- ${formatDeviceLabel(missingDevice)}`); @@ -205,6 +197,49 @@ export class SetUpAdhocProvisioningProfile { ); } + private async reuseCurrentProvisioningProfileAsync( + currentProvisioningProfile: AppleProvisioningProfileFragment, + provisioningProfileStoreInfo: ProvisioningProfile, + ctx: CredentialsContext, + app: AppLookupParams, + appleAppIdentifier: AppleAppIdentifierFragment, + chosenDevices: AppleDevice[] + ): Promise { + if ( + currentProvisioningProfile.developerPortalIdentifier !== + provisioningProfileStoreInfo.provisioningProfileId + ) { + // If IDs don't match, the profile needs to be deleted and re-created + await ctx.ios.deleteProvisioningProfilesAsync(ctx.graphqlClient, [ + currentProvisioningProfile.id, + ]); + return await ctx.ios.createProvisioningProfileAsync( + ctx.graphqlClient, + app, + appleAppIdentifier, + { + appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile, + developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId, + } + ); + } else if ( + differenceBy(chosenDevices, currentProvisioningProfile.appleDevices, 'identifier').length > 0 + ) { + // If IDs match, but the devices lists don't, the profile needs to be updated first + return await ctx.ios.updateProvisioningProfileAsync( + ctx.graphqlClient, + currentProvisioningProfile.id, + { + appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile, + developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId, + } + ); + } else { + // Otherwise the current profile can be reused + return currentProvisioningProfile; + } + } + private async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise { const { app, target } = this.options; const buildCredentials = await getBuildCredentialsAsync(ctx, app, IosDistributionType.AdHoc); diff --git a/packages/eas-cli/src/credentials/ios/actions/__tests__/SetUpAdhocProvisioningProfile-test.ts b/packages/eas-cli/src/credentials/ios/actions/__tests__/SetUpAdhocProvisioningProfile-test.ts index d81e5bef55..40f623385c 100644 --- a/packages/eas-cli/src/credentials/ios/actions/__tests__/SetUpAdhocProvisioningProfile-test.ts +++ b/packages/eas-cli/src/credentials/ios/actions/__tests__/SetUpAdhocProvisioningProfile-test.ts @@ -57,30 +57,63 @@ describe('runWithDistributionCertificateAsync', () => { }); describe('compare chosen and provisioned devices', () => { describe('not all devices provisioned', () => { - it('displays warning to the user and lists the missing devices', async () => { - const { ctx, distCert } = setUpTest(); - jest.mocked(getBuildCredentialsAsync).mockResolvedValue({ - provisioningProfile: { + describe('still not provisioned after an update', () => { + it('displays warning to the user and lists the missing devices', async () => { + const { ctx, distCert } = setUpTest(); + jest.mocked(getBuildCredentialsAsync).mockResolvedValue({ + provisioningProfile: { + appleTeam: {}, + appleDevices: [{ identifier: 'id1' }], + developerPortalIdentifier: 'provisioningProfileId', + }, + } as IosAppBuildCredentialsFragment); + ctx.ios.updateProvisioningProfileAsync = jest.fn().mockResolvedValue({ appleTeam: {}, appleDevices: [{ identifier: 'id1' }], developerPortalIdentifier: 'provisioningProfileId', - }, - } as IosAppBuildCredentialsFragment); - const LogWarnSpy = jest.spyOn(Log, 'warn'); - const LogLogSpy = jest.spyOn(Log, 'log'); - const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync( - ctx, - distCert - ); - expect(result).toEqual({} as IosAppBuildCredentialsFragment); - expect(LogWarnSpy).toHaveBeenCalledTimes(3); - expect(LogWarnSpy).toHaveBeenCalledWith('Failed to provision 2 of the selected devices:'); - expect(LogWarnSpy).toHaveBeenCalledWith('- id2 (iPhone) (Device 2)'); - expect(LogWarnSpy).toHaveBeenCalledWith('- id3 (Mac) (Device 3)'); - expect(LogLogSpy).toHaveBeenCalledTimes(1); - expect(LogLogSpy).toHaveBeenCalledWith( - 'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet' - ); + }); + const LogWarnSpy = jest.spyOn(Log, 'warn'); + const LogLogSpy = jest.spyOn(Log, 'log'); + const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync( + ctx, + distCert + ); + expect(result).toEqual({} as IosAppBuildCredentialsFragment); + expect(LogWarnSpy).toHaveBeenCalledTimes(3); + expect(LogWarnSpy).toHaveBeenCalledWith('Failed to provision 2 of the selected devices:'); + expect(LogWarnSpy).toHaveBeenCalledWith('- id2 (iPhone) (Device 2)'); + expect(LogWarnSpy).toHaveBeenCalledWith('- id3 (Mac) (Device 3)'); + expect(LogLogSpy).toHaveBeenCalledTimes(1); + expect(LogLogSpy).toHaveBeenCalledWith( + 'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet' + ); + }); + }); + describe('all devices provisioned after an update', () => { + it('does not display warning', async () => { + const { ctx, distCert } = setUpTest(); + jest.mocked(getBuildCredentialsAsync).mockResolvedValue({ + provisioningProfile: { + appleTeam: {}, + appleDevices: [{ identifier: 'id1' }], + developerPortalIdentifier: 'provisioningProfileId', + }, + } as IosAppBuildCredentialsFragment); + ctx.ios.updateProvisioningProfileAsync = jest.fn().mockResolvedValue({ + appleTeam: {}, + appleDevices: [{ identifier: 'id1' }, { identifier: 'id2' }, { identifier: 'id3' }], + developerPortalIdentifier: 'provisioningProfileId', + }); + const LogWarnSpy = jest.spyOn(Log, 'warn'); + const LogLogSpy = jest.spyOn(Log, 'log'); + const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync( + ctx, + distCert + ); + expect(result).toEqual({} as IosAppBuildCredentialsFragment); + expect(LogWarnSpy).not.toHaveBeenCalled(); + expect(LogLogSpy).not.toHaveBeenCalled(); + }); }); }); describe('all devices provisioned', () => {