Compare commits

...
3 changed files with 101 additions and 2 deletions

No files matched your search

@@ -0,0 +1,5 @@
---
'@expo/build-tools': patch
---
Allow an App Store Connect processing timeout to be configured and report an error when processing does not finish before the deadline.
@@ -0,0 +1,78 @@
import fs from 'fs-extra';
import nock from 'nock';
import { generateKeyPairSync } from 'node:crypto';
import { createGlobalContextMock } from '../../../__tests__/utils/context';
import { createUploadToAscBuildFunction } from '../uploadToAsc';
jest.unmock('node-fetch');
jest.mock('../readIpaInfo', () => ({
readIpaInfoAsync: async () => ({ dtPlatformName: 'iphoneos' }),
}));
beforeAll(() => nock.disableNetConnect());
afterAll(() => nock.enableNetConnect());
afterEach(() => {
try {
expect(nock.pendingMocks()).toEqual([]);
} finally {
nock.cleanAll();
}
});
it.each(['COMPLETE', 'PROCESSING'] as const)(
'only reports success when Apple processing is COMPLETE (received %s)',
async state => {
await fs.ensureDir('/tmp');
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
await fs.writeJson('/tmp/key.json', {
key_id: 'TESTKEY',
key: privateKey.export({ type: 'pkcs8', format: 'pem' }),
});
await fs.writeFile('/tmp/app.ipa', 'test');
const api = nock('https://api.appstoreconnect.apple.com');
api
.get('/v1/apps/app')
.query(true)
.reply(200, {
data: { id: 'app', type: 'apps', attributes: { name: 'App', bundleId: 'dev.test' } },
});
api.post('/v1/buildUploads').reply(201, { data: { id: 'upload', type: 'buildUploads' } });
api.post('/v1/buildUploadFiles').reply(201, {
data: { id: 'file', type: 'buildUploadFiles', attributes: { uploadOperations: [] } },
});
const file = {
data: {
id: 'file',
type: 'buildUploadFiles',
attributes: { assetDeliveryState: { state: 'COMPLETE' } },
},
};
api.get('/v1/buildUploadFiles/file').query(true).twice().reply(200, file);
api
.get('/v1/buildUploads/upload')
.query(true)
.reply(200, {
data: { id: 'upload', type: 'buildUploads', attributes: { state: { state } } },
});
const step = createUploadToAscBuildFunction().createBuildStepFromFunctionCall(
createGlobalContextMock(),
{
callInputs: {
ipa_path: '/tmp/app.ipa',
asc_api_key_path: '/tmp/key.json',
apple_app_identifier: 'app',
bundle_version: '1',
bundle_short_version: '1.0',
wait_for_processing: true,
wait_processing_timeout_seconds: 1,
},
}
);
if (state === 'COMPLETE') {
await expect(step.executeAsync()).resolves.toBeUndefined();
} else {
await expect(step.executeAsync()).rejects.toThrow('Apple did not finish processing');
}
}
);
@@ -55,6 +55,12 @@ export function createUploadToAscBuildFunction(): BuildFunction {
defaultValue: false,
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
}),
BuildStepInput.createProvider({
id: 'wait_processing_timeout_seconds',
required: false,
defaultValue: 30 * 60,
allowedValueTypeName: BuildStepInputValueTypeName.NUMBER,
}),
],
outputProviders: [
BuildStepOutput.createProvider({
@@ -73,6 +79,11 @@ export function createUploadToAscBuildFunction(): BuildFunction {
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 processingTimeoutSeconds = z
.number()
.int()
.positive()
.parse(inputs.wait_processing_timeout_seconds.value);
const ipaPath = path.resolve(stepsCtx.workingDirectory, ipaPathInput);
if (!(await fs.pathExists(ipaPath))) {
@@ -213,7 +224,7 @@ export function createUploadToAscBuildFunction(): BuildFunction {
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 },
@@ -322,9 +333,14 @@ export function createUploadToAscBuildFunction(): BuildFunction {
throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
} else if (state.state === 'COMPLETE') {
stepsCtx.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.'
);
},
});
}