diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b30dd53b3..9268eb6cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- [eas-cli] Stop `eas build` from looping forever printing "There are still no registered devices." when a device registration method other than Website registers no devices. ([#4254](https://github.com/expo/eas-cli/pull/4254) by [@dennytosp](https://github.com/dennytosp)) + ### ๐Ÿงน Chores ## [22.2.0](https://github.com/expo/eas-cli/releases/tag/v22.2.0) - 2026-08-20 diff --git a/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts b/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts index 20c5ae4a42..120d288702 100644 --- a/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts +++ b/packages/eas-cli/src/credentials/ios/actions/SetUpAdhocProvisioningProfile.ts @@ -388,15 +388,24 @@ export class SetUpAdhocProvisioningProfile { const devices = await ctx.ios.getDevicesForAppleTeamAsync(ctx.graphqlClient, app, appleTeam, { useCache: false, }); - if (devices.length === 0) { - Log.warn('There are still no registered devices.'); - // if the user used the input method there should be some devices available - if (method === RegistrationMethod.INPUT) { - throw new Error('Input registration method has failed'); - } - } else { + if (devices.length > 0) { return devices; } + + Log.warn('There are still no registered devices.'); + // if the user used the input method there should be some devices available + if (method === RegistrationMethod.INPUT) { + throw new Error('Input registration method has failed'); + } + // Only the website method registers devices out of band, so only there does waiting and + // reading the list again stand a chance of returning something. Every other method is done + // registering by the time it resolves - with nothing to wait for, looping would just reread + // the same empty list forever and leave the user no way out but killing the process. + if (method !== RegistrationMethod.WEBSITE) { + throw new Error( + `No devices were registered. Run 'eas device:create' to register your devices first.` + ); + } } } 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 3eebe7bfb2..e5a69d5e2e 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 @@ -3,6 +3,7 @@ import { EasJson } from '@expo/eas-json'; import { Analytics } from '../../../../analytics/AnalyticsManager'; import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient'; +import DeviceCreateAction, { RegistrationMethod } from '../../../../devices/actions/create/action'; import { Account, AppleAppIdentifierFragment, @@ -10,11 +11,12 @@ import { AppleDeviceClass, AppleDeviceFragment, AppleDistributionCertificateFragment, + AppleTeamFragment, IosAppBuildCredentialsFragment, } from '../../../../graphql/generated'; import Log from '../../../../log'; import { getApplePlatformFromTarget } from '../../../../project/ios/target'; -import { selectAsync } from '../../../../prompts'; +import { pressAnyKeyToContinueAsync, selectAsync } from '../../../../prompts'; import { Actor } from '../../../../user/User'; import { Client } from '../../../../vcs/vcs'; import { CredentialsContext, CredentialsContextProjectInfo } from '../../../context'; @@ -56,6 +58,11 @@ jest.mock('../SetUpDistributionCertificate', () => ({ import { SetUpDistributionCertificate } from '../SetUpDistributionCertificate'; jest.mock('../../../../project/ios/target'); jest.mock('../../../../prompts'); +jest.mock('../../../../devices/actions/create/action', () => ({ + __esModule: true, + ...jest.requireActual('../../../../devices/actions/create/action'), + default: jest.fn(), +})); describe(doUDIDsMatch, () => { it('return false if UDIDs do not match', () => { @@ -228,6 +235,73 @@ describe('runAsync', () => { }); }); +describe('registerDevicesAsync', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const setUpAdhocProvisioningProfile = new SetUpAdhocProvisioningProfile({ + app: { account: {} as Account, projectName: 'projName', bundleIdentifier: 'bundleId' }, + target: { targetName: 'targetName', bundleIdentifier: 'bundleId', entitlements: {} }, + }); + + async function registerDevicesAsync( + ctx: CredentialsContext, + method: RegistrationMethod + ): Promise { + jest + .mocked(DeviceCreateAction) + .mockImplementation(() => ({ runAsync: jest.fn().mockResolvedValue(method) }) as any); + return await (setUpAdhocProvisioningProfile as any).registerDevicesAsync( + ctx, + {} as AppleTeamFragment + ); + } + + it('returns the devices registered by the chosen method', async () => { + const { ctx } = setUpTest(); + + await expect(registerDevicesAsync(ctx, RegistrationMethod.DEVELOPER_PORTAL)).resolves.toEqual([ + { identifier: 'id1' }, + { identifier: 'id2' }, + { identifier: 'id3' }, + ]); + }); + + it('gives up when the developer portal method registers nothing', async () => { + const { ctx } = setUpTest(); + ctx.ios.getDevicesForAppleTeamAsync = jest.fn().mockResolvedValue([]); + + await expect(registerDevicesAsync(ctx, RegistrationMethod.DEVELOPER_PORTAL)).rejects.toThrow( + `No devices were registered. Run 'eas device:create' to register your devices first.` + ); + expect(ctx.ios.getDevicesForAppleTeamAsync).toHaveBeenCalledTimes(1); + }); + + it('reports the input method failing on its own', async () => { + const { ctx } = setUpTest(); + ctx.ios.getDevicesForAppleTeamAsync = jest.fn().mockResolvedValue([]); + + await expect(registerDevicesAsync(ctx, RegistrationMethod.INPUT)).rejects.toThrow( + 'Input registration method has failed' + ); + }); + + it('waits for the website method until the devices show up', async () => { + const { ctx } = setUpTest(); + ctx.ios.getDevicesForAppleTeamAsync = jest + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValue([{ identifier: 'id1' }] as AppleDeviceFragment[]); + + await expect(registerDevicesAsync(ctx, RegistrationMethod.WEBSITE)).resolves.toEqual([ + { identifier: 'id1' }, + ]); + expect(pressAnyKeyToContinueAsync).toHaveBeenCalledTimes(2); + }); +}); + describe('refresh ad-hoc provisioning profile', () => { beforeEach(() => { jest.clearAllMocks();