mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
834ace1828 |
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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@expo/eas-build-job': patch
|
||||
---
|
||||
|
||||
Add types and validation for documented GitHub workflow context fields.
|
||||
@@ -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.'
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -871,7 +871,7 @@ describe('StaticWorkflowInterpolationContextZ', () => {
|
||||
});
|
||||
|
||||
describe('GitHub context event payload passthrough', () => {
|
||||
it('preserves webhook fields and validates documented pull request details', () => {
|
||||
it('accepts arbitrary fields at all event levels', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
@@ -895,19 +895,7 @@ 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 * * *',
|
||||
@@ -933,34 +921,6 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -207,7 +207,6 @@ const GitHubContextZ = z.object({
|
||||
.record(z.string(), z.unknown())
|
||||
.and(
|
||||
z.object({
|
||||
action: z.string().optional(),
|
||||
label: z
|
||||
.looseObject({
|
||||
name: z.string(),
|
||||
@@ -222,19 +221,6 @@ 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(),
|
||||
|
||||
Reference in new issue
Block a user