From 67df0e278705b807daaeed95e9c6ae89617c7065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Sat, 24 Oct 2026 17:56:57 +0200 Subject: [PATCH 1/2] [build-tools] configure and enforce ASC processing deadlines --- .changeset/asc-processing-timeout.md | 5 +++++ .../waitForAscBuildProcessing.test.ts | 20 ++++++++++++++++--- .../functions/waitForAscBuildProcessing.ts | 20 +++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 .changeset/asc-processing-timeout.md diff --git a/.changeset/asc-processing-timeout.md b/.changeset/asc-processing-timeout.md new file mode 100644 index 0000000000..394c983c25 --- /dev/null +++ b/.changeset/asc-processing-timeout.md @@ -0,0 +1,5 @@ +--- +'@expo/build-tools': minor +--- + +Add a configurable timeout to `eas/wait_for_asc_build_processing` and fail when Apple does not finish processing before the deadline. diff --git a/packages/build-tools/src/steps/functions/__tests__/waitForAscBuildProcessing.test.ts b/packages/build-tools/src/steps/functions/__tests__/waitForAscBuildProcessing.test.ts index a0ea0d8dab..6af6ed827a 100644 --- a/packages/build-tools/src/steps/functions/__tests__/waitForAscBuildProcessing.test.ts +++ b/packages/build-tools/src/steps/functions/__tests__/waitForAscBuildProcessing.test.ts @@ -24,7 +24,7 @@ const api = () => nock('https://api.appstoreconnect.apple.com'); const logger = createMockLogger(); const globalContext = () => createGlobalContextMock({ projectTargetDirectory: '/project', logger }); -async function waitAsync(diagnostics: Record = {}): Promise { +async function waitAsync(diagnostics: Record = {}): Promise { const fn = getEasFunctions(createCustomBuildContextMock()).find( fn => fn.getFullId() === 'eas/wait_for_asc_build_processing' )!; @@ -139,6 +139,18 @@ it('polls an existing upload through awaiting and processing states until comple await expect(waitAsync()).resolves.toBeUndefined(); }); +it('reports a processing timeout instead of success when Apple is still processing', async () => { + let now = 0; + jest.spyOn(Date, 'now').mockImplementation(() => now); + jest.mocked(setTimeout).mockImplementation(async () => { + now += 2000; + }); + mockProcessing('PROCESSING'); + await expect(waitAsync({ wait_processing_timeout_seconds: 1 })).rejects.toMatchObject({ + errorCode: 'EAS_UPLOAD_TO_ASC_PROCESSING_TIMEOUT', + }); +}); + it('uses the supplied bundle identifier in mismatch errors without an IPA', async () => { mockProcessing( 'FAILED', @@ -226,7 +238,7 @@ it.each([undefined, false, true])( ); it.each(['upload', 'wait'])( - 'preserves the successful timeout return for the %s step', + 'reports the default processing timeout for the %s step', async step => { mockProcessing('PROCESSING'); const start = Date.now(); @@ -234,6 +246,8 @@ it.each(['upload', 'wait'])( jest.mocked(setTimeout).mockImplementationOnce(async () => { jest.spyOn(Date, 'now').mockReturnValue(start + 30 * 60 * 1000); }); - await expect(step === 'upload' ? uploadAsync(true) : waitAsync()).resolves.toBeUndefined(); + await expect(step === 'upload' ? uploadAsync(true) : waitAsync()).rejects.toMatchObject({ + errorCode: 'EAS_UPLOAD_TO_ASC_PROCESSING_TIMEOUT', + }); } ); diff --git a/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts b/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts index 882629d00c..e4116af0b2 100644 --- a/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts +++ b/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts @@ -15,6 +15,12 @@ export function createWaitForAscBuildProcessingBuildFunction(): BuildFunction { name: 'Wait for App Store Connect processing', __metricsId: 'eas/wait_for_asc_build_processing', inputProviders: [ + BuildStepInput.createProvider({ + id: 'wait_processing_timeout_seconds', + required: false, + defaultValue: 30 * 60, + allowedValueTypeName: BuildStepInputValueTypeName.NUMBER, + }), BuildStepInput.createProvider({ id: 'asc_api_key_path', required: true, @@ -46,6 +52,7 @@ export function createWaitForAscBuildProcessingBuildFunction(): BuildFunction { .object({ asc_api_key_path: z.string(), build_upload_id: z.string(), + wait_processing_timeout_seconds: z.number().int().positive(), bundle_identifier: z.string().optional(), apple_app_identifier: z.string().optional(), bundle_short_version: z.string().optional(), @@ -53,6 +60,7 @@ export function createWaitForAscBuildProcessingBuildFunction(): BuildFunction { .parse({ asc_api_key_path: inputs.asc_api_key_path.value, build_upload_id: inputs.build_upload_id.value, + wait_processing_timeout_seconds: inputs.wait_processing_timeout_seconds.value, bundle_identifier: inputs.bundle_identifier.value, apple_app_identifier: inputs.apple_app_identifier.value, bundle_short_version: inputs.bundle_short_version.value, @@ -63,6 +71,7 @@ export function createWaitForAscBuildProcessingBuildFunction(): BuildFunction { await waitForAscBuildProcessingAsync({ client, buildUploadId: parsedInputs.build_upload_id, + processingTimeoutSeconds: parsedInputs.wait_processing_timeout_seconds, logger: ctx.logger, ipaBundleIdentifier: parsedInputs.bundle_identifier, appleAppIdentifier: parsedInputs.apple_app_identifier, @@ -81,6 +90,7 @@ export async function waitForAscBuildProcessingAsync({ appleAppIdentifier, ascAppBundleIdentifier, bundleShortVersion, + processingTimeoutSeconds = 30 * 60, }: { client: AscApiClient; buildUploadId: string; @@ -89,13 +99,14 @@ export async function waitForAscBuildProcessingAsync({ appleAppIdentifier: string | undefined; ascAppBundleIdentifier: string | undefined; bundleShortVersion: string | undefined; + processingTimeoutSeconds?: number; }): Promise { logger.info('Checking build upload status...'); const waitingForBuildStartedAt = Date.now(); const waitingLogIntervalMs = 10 * 1000; let lastWaitLogTime = 0; let lastWaitLogState: string | null = null; - while (Date.now() - waitingForBuildStartedAt < 30 * 60 * 1000 /* 30 minutes */) { + while (Date.now() - waitingForBuildStartedAt < processingTimeoutSeconds * 1000) { const { data: { attributes: { state }, @@ -205,9 +216,14 @@ export async function waitForAscBuildProcessingAsync({ throw new Error(`Build upload (ID: ${buildUploadId}) failed.`); } else if (state.state === 'COMPLETE') { logger.info(`Build upload (ID: ${buildUploadId}) complete!`); + return; } - break; } + throw new UserError( + 'EAS_UPLOAD_TO_ASC_PROCESSING_TIMEOUT', + `Apple did not finish processing build upload ${buildUploadId} within ${processingTimeoutSeconds} seconds. ` + + 'The binary was uploaded. Check its status in App Store Connect before uploading it again, or increase wait_processing_timeout_seconds.' + ); } function isClosedVersionTrainError(messages: { code: string }[]): boolean { From a107f30620b8027ae45f6431ba88c4a65f2cb5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Sat, 24 Oct 2026 18:00:08 +0200 Subject: [PATCH 2/2] [build-tools] accept positive fractional processing timeouts --- .../src/steps/functions/waitForAscBuildProcessing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts b/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts index e4116af0b2..ad43ee37ee 100644 --- a/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts +++ b/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts @@ -52,7 +52,7 @@ export function createWaitForAscBuildProcessingBuildFunction(): BuildFunction { .object({ asc_api_key_path: z.string(), build_upload_id: z.string(), - wait_processing_timeout_seconds: z.number().int().positive(), + wait_processing_timeout_seconds: z.number().finite().positive(), bundle_identifier: z.string().optional(), apple_app_identifier: z.string().optional(), bundle_short_version: z.string().optional(),