From ad4ee7b85801fb1f7955e4f6d491592879e508bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Sat, 24 Oct 2026 22:12:48 +0200 Subject: [PATCH] [build-tools] retire upload processing wait after producer migration --- .changeset/remove-asc-upload-wait.md | 5 + .../waitForAscBuildProcessing.test.ts | 45 +-- .../functions/updateTestFlightMetadata.ts | 2 +- .../src/steps/functions/uploadToAsc.ts | 23 -- .../functions/waitForAscBuildProcessing.ts | 257 ++++++++---------- 5 files changed, 137 insertions(+), 195 deletions(-) create mode 100644 .changeset/remove-asc-upload-wait.md diff --git a/.changeset/remove-asc-upload-wait.md b/.changeset/remove-asc-upload-wait.md new file mode 100644 index 0000000000..a541a1ec82 --- /dev/null +++ b/.changeset/remove-asc-upload-wait.md @@ -0,0 +1,5 @@ +--- +'@expo/build-tools': major +--- + +Remove `wait_for_processing` from `eas/upload_to_asc`; use a separate `eas/wait_for_asc_build_processing` step to wait for App Store Connect processing. 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..03c4f4e246 100644 --- a/packages/build-tools/src/steps/functions/__tests__/waitForAscBuildProcessing.test.ts +++ b/packages/build-tools/src/steps/functions/__tests__/waitForAscBuildProcessing.test.ts @@ -35,7 +35,7 @@ async function waitAsync(diagnostics: Record = {}): Promise { +async function uploadAsync(): Promise { await fs.outputFile('/project/app.ipa', 'ipa'); jest.mocked(readIpaInfoAsync).mockResolvedValue({ bundleIdentifier: 'com.test.ipa', @@ -51,7 +51,6 @@ async function uploadAsync(waitForProcessing?: boolean): Promise { apple_app_identifier: 'app', bundle_version: '1', bundle_short_version: '1.0', - ...(waitForProcessing === undefined ? {} : { wait_for_processing: waitForProcessing }), }, }); await step.executeAsync(); @@ -208,32 +207,16 @@ it('reports missing privacy purpose strings when Apple does not name a key', asy }); }); -it.each([undefined, false, true])( - 'keeps upload wait_for_processing=%s compatible', - async waitForProcessing => { - if (waitForProcessing) { - mockProcessing('PROCESSING'); - mockProcessing('FAILED', [{ code: '90725', description: 'SDK is too old' }]); - } - if (waitForProcessing) { - await expect(uploadAsync(waitForProcessing)).rejects.toMatchObject({ - errorCode: 'EAS_UPLOAD_TO_ASC_SDK_VERSION_ISSUE', - }); - } else { - await expect(uploadAsync(waitForProcessing)).resolves.toBeUndefined(); - } - } -); - -it.each(['upload', 'wait'])( - 'preserves the successful timeout return for the %s step', - async step => { - mockProcessing('PROCESSING'); - const start = Date.now(); - jest.spyOn(Date, 'now').mockReturnValue(start); - jest.mocked(setTimeout).mockImplementationOnce(async () => { - jest.spyOn(Date, 'now').mockReturnValue(start + 30 * 60 * 1000); - }); - await expect(step === 'upload' ? uploadAsync(true) : waitAsync()).resolves.toBeUndefined(); - } -); +it('finishes the upload without waiting for processing', async () => { + await expect(uploadAsync()).resolves.toBeUndefined(); +}); + +it('preserves the successful timeout return for the wait step', async () => { + mockProcessing('PROCESSING'); + const start = Date.now(); + jest.spyOn(Date, 'now').mockReturnValue(start); + jest.mocked(setTimeout).mockImplementationOnce(async () => { + jest.spyOn(Date, 'now').mockReturnValue(start + 30 * 60 * 1000); + }); + await expect(waitAsync()).resolves.toBeUndefined(); +}); diff --git a/packages/build-tools/src/steps/functions/updateTestFlightMetadata.ts b/packages/build-tools/src/steps/functions/updateTestFlightMetadata.ts index 2d9b39632c..c241eca93c 100644 --- a/packages/build-tools/src/steps/functions/updateTestFlightMetadata.ts +++ b/packages/build-tools/src/steps/functions/updateTestFlightMetadata.ts @@ -93,7 +93,7 @@ export async function updateTestFlightMetadataAsync({ if (upload.attributes?.state?.state !== 'COMPLETE' || !buildId) { throw new UserError( 'EAS_TESTFLIGHT_BUILD_NOT_READY', - 'The uploaded build is not ready for TestFlight metadata. Run eas/wait_for_asc_build_processing first, or use eas/upload_to_asc with wait_for_processing: true.' + 'The uploaded build is not ready for TestFlight metadata. Run eas/wait_for_asc_build_processing first.' ); } logger.info(`Updating TestFlight metadata for Apple build ${buildId}...`); diff --git a/packages/build-tools/src/steps/functions/uploadToAsc.ts b/packages/build-tools/src/steps/functions/uploadToAsc.ts index 8d9f75aa17..b312636885 100644 --- a/packages/build-tools/src/steps/functions/uploadToAsc.ts +++ b/packages/build-tools/src/steps/functions/uploadToAsc.ts @@ -14,7 +14,6 @@ import { z } from 'zod'; import { AscApiClient, AscApiClientPostApi } from '../utils/ios/AscApiClient'; import { AscApiUtils } from '../utils/ios/AscApiUtils'; import { readIpaInfoAsync } from './readIpaInfo'; -import { waitForAscBuildProcessingAsync } from './waitForAscBuildProcessing'; export function createUploadToAscBuildFunction(): BuildFunction { return new BuildFunction({ @@ -49,12 +48,6 @@ export function createUploadToAscBuildFunction(): BuildFunction { required: true, allowedValueTypeName: BuildStepInputValueTypeName.STRING, }), - BuildStepInput.createProvider({ - id: 'wait_for_processing', - required: false, - defaultValue: false, - allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, - }), ], outputProviders: [ BuildStepOutput.createProvider({ @@ -72,7 +65,6 @@ export function createUploadToAscBuildFunction(): BuildFunction { const appleAppIdentifier = z.string().parse(inputs.apple_app_identifier.value); const bundleVersion = z.string().parse(inputs.bundle_version.value); const bundleShortVersion = z.string().parse(inputs.bundle_short_version.value); - const waitForProcessing = Boolean(inputs.wait_for_processing.value); const ipaPath = path.resolve(stepsCtx.workingDirectory, ipaPathInput); if (!(await fs.pathExists(ipaPath))) { @@ -202,21 +194,6 @@ export function createUploadToAscBuildFunction(): BuildFunction { } stepsCtx.logger.info(`See your build in App Store Connect: ${buildUploadUrl}`); - - if (!waitForProcessing) { - stepsCtx.logger.info('Skipping waiting for processing.'); - return; - } - - await waitForAscBuildProcessingAsync({ - client, - buildUploadId, - logger: stepsCtx.logger, - ipaBundleIdentifier: ipaInfoResult.ok ? ipaInfoResult.value.bundleIdentifier : undefined, - appleAppIdentifier: undefined, - ascAppBundleIdentifier, - bundleShortVersion, - }); }, }); } diff --git a/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts b/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts index 882629d00c..44fa880d53 100644 --- a/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts +++ b/packages/build-tools/src/steps/functions/waitForAscBuildProcessing.ts @@ -1,5 +1,4 @@ import { UserError } from '@expo/eas-build-job'; -import { bunyan } from '@expo/logger'; import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps'; import path from 'node:path'; import { setTimeout } from 'node:timers/promises'; @@ -60,154 +59,132 @@ export function createWaitForAscBuildProcessingBuildFunction(): BuildFunction { const keyPath = path.resolve(ctx.workingDirectory, parsedInputs.asc_api_key_path); const key = await AscApiUtils.loadApiKeyAsync({ keyPath }); const client = new AscApiClient({ key, logger: ctx.logger }); - await waitForAscBuildProcessingAsync({ - client, - buildUploadId: parsedInputs.build_upload_id, - logger: ctx.logger, - ipaBundleIdentifier: parsedInputs.bundle_identifier, - appleAppIdentifier: parsedInputs.apple_app_identifier, - ascAppBundleIdentifier: undefined, - bundleShortVersion: parsedInputs.bundle_short_version, - }); - }, - }); -} - -export async function waitForAscBuildProcessingAsync({ - client, - buildUploadId, - logger, - ipaBundleIdentifier, - appleAppIdentifier, - ascAppBundleIdentifier, - bundleShortVersion, -}: { - client: AscApiClient; - buildUploadId: string; - logger: bunyan; - ipaBundleIdentifier: string | undefined; - appleAppIdentifier: string | undefined; - ascAppBundleIdentifier: string | undefined; - bundleShortVersion: string | undefined; -}): 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 */) { - const { - data: { - attributes: { state }, - }, - } = await client.getAsync( - `/v1/buildUploads/:id`, - { 'fields[buildUploads]': ['state', 'build'], include: ['build'] }, - { id: buildUploadId } - ); - - if (state.state === 'AWAITING_UPLOAD' || state.state === 'PROCESSING') { - const now = Date.now(); - if (lastWaitLogState !== state.state || now - lastWaitLogTime >= waitingLogIntervalMs) { - logger.info(`Waiting for build upload to complete... (status = ${state.state})`); - lastWaitLogTime = now; - lastWaitLogState = state.state; - } - await setTimeout(2000); - continue; - } + const buildUploadId = parsedInputs.build_upload_id; + const ipaBundleIdentifier = parsedInputs.bundle_identifier; + const appleAppIdentifier = parsedInputs.apple_app_identifier; + const bundleShortVersion = parsedInputs.bundle_short_version; + const logger = ctx.logger; + let ascAppBundleIdentifier: string | undefined; + 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 */) { + const { + data: { + attributes: { state }, + }, + } = await client.getAsync( + `/v1/buildUploads/:id`, + { 'fields[buildUploads]': ['state', 'build'], include: ['build'] }, + { id: buildUploadId } + ); - logger.info('\n'); + if (state.state === 'AWAITING_UPLOAD' || state.state === 'PROCESSING') { + const now = Date.now(); + if (lastWaitLogState !== state.state || now - lastWaitLogTime >= waitingLogIntervalMs) { + logger.info(`Waiting for build upload to complete... (status = ${state.state})`); + lastWaitLogTime = now; + lastWaitLogState = state.state; + } + await setTimeout(2000); + continue; + } - const { errors = [], warnings = [], infos = [] } = state; - if (infos.length > 0) { - logger.info(`Infos:\n${AscApiUtils.itemizeMessages(infos)}\n`); - } - if (warnings.length > 0) { - logger.warn(`Warnings:\n${AscApiUtils.itemizeMessages(warnings)}\n`); - } - if (errors.length > 0) { - logger.error(`Errors:\n${AscApiUtils.itemizeMessages(errors)}\n`); - } + logger.info('\n'); - if (state.state === 'FAILED') { - if (isSdkVersionIssueError(errors)) { - throw new UserError( - 'EAS_UPLOAD_TO_ASC_SDK_VERSION_ISSUE', - 'Build upload was rejected by App Store Connect because the IPA was built with an iOS SDK that is too old for current App Store Connect requirements. ' + - 'In an Expo project, this usually means the build used an outdated EAS iOS image or the project still depends on an Expo SDK / native setup that does not support the required Xcode toolchain. ' + - 'Upgrade to a supported Expo SDK if needed and/or select a newer iOS image in `eas.json`, rebuild, and submit again.', - { - docsUrl: 'https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/', - } - ); - } - if (isInvalidBundleIdentifierError(errors)) { - if (!ascAppBundleIdentifier && appleAppIdentifier) { - try { - const app = await AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier }); - ascAppBundleIdentifier = app.data.attributes.bundleId; - } catch { - // Keep the processing error if app details are unavailable. - } + const { errors = [], warnings = [], infos = [] } = state; + if (infos.length > 0) { + logger.info(`Infos:\n${AscApiUtils.itemizeMessages(infos)}\n`); + } + if (warnings.length > 0) { + logger.warn(`Warnings:\n${AscApiUtils.itemizeMessages(warnings)}\n`); } - let visibleAppsSummary: string | null = null; - try { - const apps = await AscApiUtils.getAppsAsync({ client, limit: 5 }); - visibleAppsSummary = AscApiUtils.formatAppsList(apps); - } catch { - // Ok to fail, this is just trying to be helpful. + if (errors.length > 0) { + logger.error(`Errors:\n${AscApiUtils.itemizeMessages(errors)}\n`); } - throw new UserError( - 'EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID', - `Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` + - `IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` + - `App Store Connect app bundle identifier: ${ascAppBundleIdentifier ?? '(unavailable)'}\n\n` + - 'Bundle identifier cannot be changed for an existing App Store Connect app. ' + - 'If you selected the wrong app, change the Apple app identifier in the submit profile. ' + - 'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).' + - '\n\nOther App Store Connect apps visible to this API key:\n' + - (visibleAppsSummary ?? ' (unable to retrieve)'), - { - docsUrl: 'https://expo.fyi/asc-app-id', + if (state.state === 'FAILED') { + if (isSdkVersionIssueError(errors)) { + throw new UserError( + 'EAS_UPLOAD_TO_ASC_SDK_VERSION_ISSUE', + 'Build upload was rejected by App Store Connect because the IPA was built with an iOS SDK that is too old for current App Store Connect requirements. ' + + 'In an Expo project, this usually means the build used an outdated EAS iOS image or the project still depends on an Expo SDK / native setup that does not support the required Xcode toolchain. ' + + 'Upgrade to a supported Expo SDK if needed and/or select a newer iOS image in `eas.json`, rebuild, and submit again.', + { + docsUrl: 'https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/', + } + ); } - ); - } - if (isMissingPurposeStringError(errors)) { - const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors); - throw new UserError( - 'EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING', - `Build upload was rejected by App Store Connect because Info.plist is missing one or more privacy purpose strings.\n\n` + - `${ - missingUsageDescriptionKeys.length > 0 - ? `Missing keys reported by App Store Connect:\n- ${missingUsageDescriptionKeys.join( - '\n- ' - )}\n\n` - : '' - }` + - 'Add the missing keys with clear user-facing explanations, then rebuild and submit again.\n' + - 'If you use Continuous Native Generation (CNG), update `ios.infoPlist` in app.json/app.config.js.\n' + - 'If you do not use CNG, update your app target Info.plist directly.', - { - docsUrl: 'https://docs.expo.dev/guides/permissions/#ios', + if (isInvalidBundleIdentifierError(errors)) { + if (!ascAppBundleIdentifier && appleAppIdentifier) { + try { + const app = await AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier }); + ascAppBundleIdentifier = app.data.attributes.bundleId; + } catch { + // Keep the processing error if app details are unavailable. + } + } + let visibleAppsSummary: string | null = null; + try { + const apps = await AscApiUtils.getAppsAsync({ client, limit: 5 }); + visibleAppsSummary = AscApiUtils.formatAppsList(apps); + } catch { + // Ok to fail, this is just trying to be helpful. + } + + throw new UserError( + 'EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID', + `Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` + + `IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` + + `App Store Connect app bundle identifier: ${ascAppBundleIdentifier ?? '(unavailable)'}\n\n` + + 'Bundle identifier cannot be changed for an existing App Store Connect app. ' + + 'If you selected the wrong app, change the Apple app identifier in the submit profile. ' + + 'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).' + + '\n\nOther App Store Connect apps visible to this API key:\n' + + (visibleAppsSummary ?? ' (unable to retrieve)'), + { + docsUrl: 'https://expo.fyi/asc-app-id', + } + ); } - ); - } - if (isClosedVersionTrainError(errors)) { - throw new UserError( - 'EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', - `Build upload was rejected by App Store Connect because the ${bundleShortVersion ?? 'selected'} app version is not accepted for new build submissions. ` + - 'This usually means the version train is closed or lower than a previously approved version. ' + - 'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version, then rebuild and submit again.' - ); + if (isMissingPurposeStringError(errors)) { + const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors); + throw new UserError( + 'EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING', + `Build upload was rejected by App Store Connect because Info.plist is missing one or more privacy purpose strings.\n\n` + + `${ + missingUsageDescriptionKeys.length > 0 + ? `Missing keys reported by App Store Connect:\n- ${missingUsageDescriptionKeys.join( + '\n- ' + )}\n\n` + : '' + }` + + 'Add the missing keys with clear user-facing explanations, then rebuild and submit again.\n' + + 'If you use Continuous Native Generation (CNG), update `ios.infoPlist` in app.json/app.config.js.\n' + + 'If you do not use CNG, update your app target Info.plist directly.', + { + docsUrl: 'https://docs.expo.dev/guides/permissions/#ios', + } + ); + } + if (isClosedVersionTrainError(errors)) { + throw new UserError( + 'EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', + `Build upload was rejected by App Store Connect because the ${bundleShortVersion ?? 'selected'} app version is not accepted for new build submissions. ` + + 'This usually means the version train is closed or lower than a previously approved version. ' + + 'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version, then rebuild and submit again.' + ); + } + throw new Error(`Build upload (ID: ${buildUploadId}) failed.`); + } else if (state.state === 'COMPLETE') { + logger.info(`Build upload (ID: ${buildUploadId}) complete!`); + } + break; } - throw new Error(`Build upload (ID: ${buildUploadId}) failed.`); - } else if (state.state === 'COMPLETE') { - logger.info(`Build upload (ID: ${buildUploadId}) complete!`); - } - break; - } + }, + }); } function isClosedVersionTrainError(messages: { code: string }[]): boolean {