mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586b77aca4 | ||
|
|
a5a8c90d59 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@expo/build-tools': patch
|
||||
---
|
||||
|
||||
Show the final Google Play rejection when a retried release commit has an uncertain outcome.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@expo/build-tools': patch
|
||||
---
|
||||
|
||||
Retry recognized Google Play processing and review-setting rejections after transient request failures, while preserving uncertain commit outcomes.
|
||||
@@ -198,7 +198,10 @@ it('does not delete the edit or publish outputs when commit outcome is unknown',
|
||||
return [204];
|
||||
});
|
||||
const task = submission();
|
||||
await expect(task.run()).rejects.toBeInstanceOf(SystemError);
|
||||
const error = await task.run().catch(error => error);
|
||||
expect(error).toBeInstanceOf(SystemError);
|
||||
expect((error as Error).message).toContain('HTTP 404');
|
||||
expect((error as Error).message).toContain('may still have saved');
|
||||
expect(deleted).toBe(false);
|
||||
expect(task.setters.every(set => set.mock.calls.length === 0)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -144,6 +144,16 @@ export namespace GooglePlayUtils {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): Promise<void> {
|
||||
let uncertainCommitError: GooglePlayCommitOutcomeUnknownError | undefined;
|
||||
const lastCommitError = (error: unknown): unknown => {
|
||||
while (
|
||||
error instanceof GooglePlayCommitOutcomeUnknownError ||
|
||||
error instanceof AggregateError
|
||||
) {
|
||||
error = error.cause;
|
||||
}
|
||||
return error;
|
||||
};
|
||||
const sendCommitAsync = async ({
|
||||
changesNotSentForReview,
|
||||
}: {
|
||||
@@ -165,13 +175,32 @@ export namespace GooglePlayUtils {
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
uncertainCommitError ||
|
||||
!(error instanceof GooglePlayApiError) ||
|
||||
GooglePlayApiError.isRetryableStatus(error.status)
|
||||
) {
|
||||
throw new GooglePlayCommitOutcomeUnknownError(
|
||||
`Could not confirm whether Google Play saved the release for ${packageName}. Check its release status at https://play.google.com/console/ before submitting again.`,
|
||||
{ cause: error, metadata: GooglePlayApiError.getErrorMetadata(error) }
|
||||
const cause = uncertainCommitError
|
||||
? new AggregateError(
|
||||
[uncertainCommitError, error],
|
||||
'Google Play commit attempts failed.',
|
||||
{ cause: error }
|
||||
)
|
||||
: error;
|
||||
const lastError = lastCommitError(error);
|
||||
const lastResponse =
|
||||
lastError instanceof GooglePlayApiError &&
|
||||
lastError.status >= 400 &&
|
||||
lastError.status < 500 &&
|
||||
!GooglePlayApiError.isRetryableStatus(lastError.status)
|
||||
? `Google Play returned HTTP ${lastError.status} for the last commit attempt${lastError.apiMessage ? `: ${lastError.apiMessage}` : '.'}\n`
|
||||
: '';
|
||||
uncertainCommitError = new GooglePlayCommitOutcomeUnknownError(
|
||||
lastResponse
|
||||
? `${lastResponse}An earlier commit attempt failed with a temporary error, so Google Play may still have saved the release for ${packageName}. Check its release status at https://play.google.com/console/. If the release is not there, fix the problem above and submit again.`
|
||||
: `Could not confirm whether Google Play saved the release for ${packageName}. Check its release status at https://play.google.com/console/ before submitting again.`,
|
||||
{ cause, metadata: GooglePlayApiError.getErrorMetadata(error) }
|
||||
);
|
||||
throw uncertainCommitError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -183,10 +212,14 @@ export namespace GooglePlayUtils {
|
||||
};
|
||||
// Google can accept the bundle upload before the edit is ready to commit.
|
||||
// Retry only this explicit rejection, not other HTTP 400 preconditions.
|
||||
const isUploadStillProcessing = (error: unknown): boolean =>
|
||||
error instanceof GooglePlayApiError &&
|
||||
error.status === 400 &&
|
||||
error.apiMessage === 'Some of the Android App Bundle uploads are not completed yet.';
|
||||
const isUploadStillProcessing = (error: unknown): boolean => {
|
||||
const lastError = lastCommitError(error);
|
||||
return (
|
||||
lastError instanceof GooglePlayApiError &&
|
||||
lastError.status === 400 &&
|
||||
lastError.apiMessage === 'Some of the Android App Bundle uploads are not completed yet.'
|
||||
);
|
||||
};
|
||||
|
||||
const commitAsync = async (options: { changesNotSentForReview?: boolean }): Promise<void> => {
|
||||
try {
|
||||
@@ -204,7 +237,21 @@ export namespace GooglePlayUtils {
|
||||
}
|
||||
)(options);
|
||||
} catch (error) {
|
||||
if (isUploadStillProcessing(error)) {
|
||||
if (uncertainCommitError && !(error instanceof GooglePlayCommitOutcomeUnknownError)) {
|
||||
// Cancellation or a later authentication failure cannot establish the earlier outcome.
|
||||
throw new GooglePlayCommitOutcomeUnknownError(uncertainCommitError.message, {
|
||||
cause: new AggregateError(
|
||||
[uncertainCommitError, error],
|
||||
'Google Play commit recovery stopped.',
|
||||
{ cause: error }
|
||||
),
|
||||
metadata: uncertainCommitError.metadata,
|
||||
});
|
||||
}
|
||||
if (
|
||||
isUploadStillProcessing(error) &&
|
||||
!(error instanceof GooglePlayCommitOutcomeUnknownError)
|
||||
) {
|
||||
throw new SystemError(
|
||||
`Google Play did not finish processing the uploaded bundle for ${packageName} within five minutes. Wait a few minutes, then check the release at https://play.google.com/console/ before submitting again.`,
|
||||
{ cause: error, metadata: GooglePlayApiError.getErrorMetadata(error) }
|
||||
@@ -218,13 +265,14 @@ export namespace GooglePlayUtils {
|
||||
// reject its presence, even when false. True saves changes for review in Console.
|
||||
await commitAsync({ changesNotSentForReview: changesNotSentForReview || undefined });
|
||||
} catch (error) {
|
||||
if (!(error instanceof GooglePlayApiError) || error.status !== 400) {
|
||||
const lastError = lastCommitError(error);
|
||||
if (!(lastError instanceof GooglePlayApiError) || lastError.status !== 400) {
|
||||
throw error;
|
||||
}
|
||||
// Like Fastlane, retry only an explicit rejection of the review setting.
|
||||
if (
|
||||
changesNotSentForReview &&
|
||||
error.apiMessage.includes('The query parameter changesNotSentForReview must not be set')
|
||||
lastError.apiMessage.includes('The query parameter changesNotSentForReview must not be set')
|
||||
) {
|
||||
logger.warn(
|
||||
'Google does not allow changesNotSentForReview for this app right now. Retrying commit once without it. The changes will be sent for review.'
|
||||
@@ -232,7 +280,9 @@ export namespace GooglePlayUtils {
|
||||
await commitAsync({});
|
||||
} else if (
|
||||
!changesNotSentForReview &&
|
||||
error.apiMessage.includes('Please set the query parameter changesNotSentForReview to true')
|
||||
lastError.apiMessage.includes(
|
||||
'Please set the query parameter changesNotSentForReview to true'
|
||||
)
|
||||
) {
|
||||
logger.warn(
|
||||
'Google requires changesNotSentForReview=true. Retrying commit once. Send the saved changes for review from Play Console.'
|
||||
|
||||
+78
-1
@@ -84,6 +84,23 @@ it.each([408, 429, 503])('retries a transient HTTP %s commit on the same edit',
|
||||
api().post(`${editPath}:commit`).query({}).reply(200, { id: 'edit' });
|
||||
await expect(commit()).resolves.toBeUndefined();
|
||||
});
|
||||
it('shows the final rejection after a transient commit failure without claiming the outcome is known', async () => {
|
||||
mockToken();
|
||||
api().post(`${editPath}:commit`).query({}).reply(503);
|
||||
const message = 'You must let us know whether your app includes any health features.';
|
||||
api().post(`${editPath}:commit`).query({}).reply(403, { error: { message } });
|
||||
const error = await commit().catch(error => error);
|
||||
const mapped = GooglePlayUtils.mapGooglePlayError(error, packageName);
|
||||
expect(mapped).toBeInstanceOf(GooglePlayCommitOutcomeUnknownError);
|
||||
expect((mapped as Error).message).toContain(`HTTP 403 for the last commit attempt: ${message}`);
|
||||
expect((mapped as Error).message).toContain('may still have saved');
|
||||
expect((mapped as Error).message).toContain('https://play.google.com/console/');
|
||||
expect(mapped).toMatchObject({
|
||||
metadata: { googlePlayErrorResponse: JSON.stringify({ error: { message } }) },
|
||||
cause: expect.any(AggregateError),
|
||||
});
|
||||
expect(((mapped as Error).cause as AggregateError).errors).toHaveLength(2);
|
||||
});
|
||||
it('preserves cancellation before commit is sent', async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('cancelled before commit');
|
||||
@@ -92,6 +109,37 @@ it('preserves cancellation before commit is sent', async () => {
|
||||
});
|
||||
const pendingUploadMessage = 'Some of the Android App Bundle uploads are not completed yet.';
|
||||
|
||||
it.each([
|
||||
[false, pendingUploadMessage, {}],
|
||||
[true, 'The query parameter changesNotSentForReview must not be set', {}],
|
||||
[
|
||||
false,
|
||||
'Please set the query parameter changesNotSentForReview to true',
|
||||
{ changesNotSentForReview: 'true' },
|
||||
],
|
||||
] as const)(
|
||||
'recovers from a wrapped rejection: %s %s',
|
||||
async (changesNotSentForReview, message, retryQuery) => {
|
||||
mockToken();
|
||||
const query = changesNotSentForReview ? { changesNotSentForReview: 'true' } : {};
|
||||
api().post(`${editPath}:commit`).query(query).reply(503);
|
||||
api().post(`${editPath}:commit`).query(query).reply(400, { error: { message } });
|
||||
api().post(`${editPath}:commit`).query(retryQuery).reply(200, { id: 'edit' });
|
||||
await expect(commit({ changesNotSentForReview })).resolves.toBeUndefined();
|
||||
}
|
||||
);
|
||||
|
||||
it('keeps an uncertain outcome when processing recovery ends in another rejection', async () => {
|
||||
mockToken();
|
||||
api().post(`${editPath}:commit`).query({}).reply(503);
|
||||
api()
|
||||
.post(`${editPath}:commit`)
|
||||
.query({})
|
||||
.reply(400, { error: { message: pendingUploadMessage } });
|
||||
api().post(`${editPath}:commit`).query({}).reply(404);
|
||||
await expect(commit()).rejects.toBeInstanceOf(GooglePlayCommitOutcomeUnknownError);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
'waits for bundle processing without changing review settings (%s)',
|
||||
async changesNotSentForReview => {
|
||||
@@ -110,6 +158,17 @@ it.each([false, true])(
|
||||
}
|
||||
);
|
||||
|
||||
it('preserves uncertainty when wrapped processing retries are exhausted', async () => {
|
||||
mockToken();
|
||||
api().post(`${editPath}:commit`).query({}).reply(503);
|
||||
api()
|
||||
.post(`${editPath}:commit`)
|
||||
.query({})
|
||||
.times(14)
|
||||
.reply(400, { error: { message: pendingUploadMessage } });
|
||||
await expect(commit()).rejects.toBeInstanceOf(GooglePlayCommitOutcomeUnknownError);
|
||||
});
|
||||
|
||||
it('stops waiting for bundle processing after 14 attempts', async () => {
|
||||
mockToken();
|
||||
api()
|
||||
@@ -250,7 +309,9 @@ it('keeps the last Google Play error response when the commit outcome is unknown
|
||||
.query({})
|
||||
.times(6)
|
||||
.reply(503, { error: { message: 'Backend error' } });
|
||||
await expect(commit()).rejects.toMatchObject({
|
||||
const error = await commit().catch(error => error);
|
||||
expect(error.message).not.toContain('last commit attempt');
|
||||
expect(error).toMatchObject({
|
||||
metadata: {
|
||||
googlePlayRequestPath: `${editPath}:commit`,
|
||||
googlePlayErrorResponse: JSON.stringify({ error: { message: 'Backend error' } }),
|
||||
@@ -269,3 +330,19 @@ it('does not keep the upload session query or a non-JSON response', async () =>
|
||||
googlePlayErrorResponse: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an uncertain commit if processing recovery is cancelled', async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('cancelled after uncertain commit');
|
||||
const logger = createMockLogger();
|
||||
jest.spyOn(logger, 'warn').mockImplementation(() => controller.abort(reason));
|
||||
mockToken();
|
||||
api().post(`${editPath}:commit`).query({}).reply(503);
|
||||
api()
|
||||
.post(`${editPath}:commit`)
|
||||
.query({})
|
||||
.reply(400, { error: { message: pendingUploadMessage } });
|
||||
await expect(commit({ signal: controller.signal, logger })).rejects.toBeInstanceOf(
|
||||
GooglePlayCommitOutcomeUnknownError
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user