Compare commits

..
6 changed files with 62 additions and 102 deletions

No files matched your search

@@ -1,5 +0,0 @@
---
'@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.
+5
View File
@@ -0,0 +1,5 @@
---
'@expo/eas-build-job': patch
---
Add types and validation for documented GitHub workflow context fields.
@@ -1,78 +0,0 @@
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,12 +55,6 @@ 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({
@@ -79,11 +73,6 @@ 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))) {
@@ -224,7 +213,7 @@ export function createUploadToAscBuildFunction(): BuildFunction {
const waitingLogIntervalMs = 10 * 1000;
let lastWaitLogTime = 0;
let lastWaitLogState: string | null = null;
while (Date.now() - waitingForBuildStartedAt < processingTimeoutSeconds * 1000) {
while (Date.now() - waitingForBuildStartedAt < 30 * 60 * 1000 /* 30 minutes */) {
const {
data: {
attributes: { state },
@@ -333,14 +322,9 @@ 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.'
);
},
});
}
@@ -871,7 +871,7 @@ describe('StaticWorkflowInterpolationContextZ', () => {
});
describe('GitHub context event payload passthrough', () => {
it('accepts arbitrary fields at all event levels', () => {
it('preserves webhook fields and validates documented pull request details', () => {
const context = {
after: {},
needs: {},
@@ -895,7 +895,19 @@ describe('GitHub context event payload passthrough', () => {
},
pull_request: {
number: 123,
title: 'Fix the build',
body: null,
state: 'open',
draft: true,
merged: false,
html_url: 'https://github.com/expo/expo/pull/123',
user: { login: 'octocat', id: 1 },
labels: [{ name: 'bug', color: 'red' }],
head: { ref: 'fix-build', sha: 'abc123', repo: { name: 'expo' } },
base: { ref: 'main', sha: 'def456' },
created_at: '2026-10-01T00:00:00Z',
updated_at: '2026-10-02T00:00:00Z',
merged_at: null,
},
number: 123,
schedule: '0 0 * * *',
@@ -921,6 +933,34 @@ describe('GitHub context event payload passthrough', () => {
const parsed = StaticWorkflowInterpolationContextZ.parse(context);
expect(parsed.github?.event).toMatchObject(context.github.event);
expect(() =>
StaticWorkflowInterpolationContextZ.parse({
...context,
github: {
...context.github,
event: {
...context.github.event,
pull_request: { ...context.github.event.pull_request, title: 123 },
},
},
})
).toThrow();
});
it.each([
{ number: 123 },
{ number: 123, body: null, user: null, merged: null, merged_at: null },
])('accepts partial pull request payloads: %j', pullRequest => {
const github = {
event_name: 'pull_request',
sha: 'abc123',
ref: 'refs/heads/main',
ref_name: 'main',
ref_type: 'branch',
event: { pull_request: pullRequest },
};
expect(StaticWorkflowInterpolationContextZ.shape.github.parse(github)).toEqual(github);
});
});
+14
View File
@@ -207,6 +207,7 @@ const GitHubContextZ = z.object({
.record(z.string(), z.unknown())
.and(
z.object({
action: z.string().optional(),
label: z
.looseObject({
name: z.string(),
@@ -221,6 +222,19 @@ const GitHubContextZ = z.object({
pull_request: z
.looseObject({
number: z.number(),
title: z.string().optional(),
body: z.string().nullable().optional(),
state: z.enum(['open', 'closed']).optional(),
draft: z.boolean().optional(),
merged: z.boolean().nullable().optional(),
html_url: z.string().optional(),
user: z.looseObject({ login: z.string() }).nullable().optional(),
labels: z.array(z.looseObject({ name: z.string() })).optional(),
head: z.looseObject({ ref: z.string(), sha: z.string() }).optional(),
base: z.looseObject({ ref: z.string(), sha: z.string() }).optional(),
created_at: z.string().optional(),
updated_at: z.string().optional(),
merged_at: z.string().nullable().optional(),
})
.optional(),
number: z.number().optional(),