Compare commits

..
10 changed files with 114 additions and 136 deletions

No files matched your search

+10 -1
View File
@@ -10,10 +10,19 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Fix asset upload crash. ([#1205](https://github.com/expo/eas-cli/pull/1205) by [@wschurman](https://github.com/wschurman))
- Show spinner instead of silently timing out during asset upload. ([#1206](https://github.com/expo/eas-cli/pull/1206) by [@wschurman](https://github.com/wschurman))
- Fix build archive S3 URLs. ([#1207](https://github.com/expo/eas-cli/pull/1207) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Increase max number of chained "extends" in build profiles. ([#1208](https://github.com/expo/eas-cli/pull/1208) by [@wkozyra95](https://github.com/wkozyra95))
## [0.55.1](https://github.com/expo/eas-cli/releases/tag/v0.55.1) - 2022-07-12
### 🐛 Bug fixes
- Fix asset upload crash. ([#1205](https://github.com/expo/eas-cli/pull/1205) by [@wschurman](https://github.com/wschurman))
## [0.55.0](https://github.com/expo/eas-cli/releases/tag/v0.55.0) - 2022-07-11
### 🎉 New features
+56 -54
View File
@@ -22,68 +22,69 @@ yarn global add eas-cli
# Usage
```sh
eas COMMAND
# runs the command
eas (-v|--version|version)
eas COMMAND
# prints the version
eas --help COMMAND
eas (-v|--version|version)
# outputs help for specific command
eas --help COMMAND
```
# Commands
<!-- commands -->
* [`eas account:login`](#eas-accountlogin)
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
* [`eas branch:list`](#eas-branchlist)
* [`eas branch:rename`](#eas-branchrename)
* [`eas branch:view [NAME]`](#eas-branchview-name)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:submit`](#eas-buildsubmit)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
* [`eas channel:list`](#eas-channellist)
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:delete`](#eas-devicedelete)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas metadata:pull`](#eas-metadatapull)
* [`eas metadata:push`](#eas-metadatapush)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
* [`eas update:configure`](#eas-updateconfigure)
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
* [`eas update:list`](#eas-updatelist)
* [`eas update:view GROUPID`](#eas-updateview-groupid)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
- [`eas account:login`](#eas-accountlogin)
- [`eas account:logout`](#eas-accountlogout)
- [`eas account:view`](#eas-accountview)
- [`eas analytics [STATUS]`](#eas-analytics-status)
- [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
- [`eas branch:create [NAME]`](#eas-branchcreate-name)
- [`eas branch:delete [NAME]`](#eas-branchdelete-name)
- [`eas branch:list`](#eas-branchlist)
- [`eas branch:rename`](#eas-branchrename)
- [`eas branch:view [NAME]`](#eas-branchview-name)
- [`eas build`](#eas-build)
- [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
- [`eas build:configure`](#eas-buildconfigure)
- [`eas build:inspect`](#eas-buildinspect)
- [`eas build:list`](#eas-buildlist)
- [`eas build:submit`](#eas-buildsubmit)
- [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
- [`eas channel:create [NAME]`](#eas-channelcreate-name)
- [`eas channel:edit [NAME]`](#eas-channeledit-name)
- [`eas channel:list`](#eas-channellist)
- [`eas channel:view [NAME]`](#eas-channelview-name)
- [`eas config`](#eas-config)
- [`eas credentials`](#eas-credentials)
- [`eas device:create`](#eas-devicecreate)
- [`eas device:delete`](#eas-devicedelete)
- [`eas device:list`](#eas-devicelist)
- [`eas device:view [UDID]`](#eas-deviceview-udid)
- [`eas diagnostics`](#eas-diagnostics)
- [`eas help [COMMAND]`](#eas-help-command)
- [`eas init`](#eas-init)
- [`eas login`](#eas-login)
- [`eas logout`](#eas-logout)
- [`eas metadata:pull`](#eas-metadatapull)
- [`eas metadata:push`](#eas-metadatapush)
- [`eas project:info`](#eas-projectinfo)
- [`eas project:init`](#eas-projectinit)
- [`eas secret:create`](#eas-secretcreate)
- [`eas secret:delete`](#eas-secretdelete)
- [`eas secret:list`](#eas-secretlist)
- [`eas submit`](#eas-submit)
- [`eas update`](#eas-update)
- [`eas update:configure`](#eas-updateconfigure)
- [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
- [`eas update:list`](#eas-updatelist)
- [`eas update:view GROUPID`](#eas-updateview-groupid)
- [`eas webhook:create`](#eas-webhookcreate)
- [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
- [`eas webhook:list`](#eas-webhooklist)
- [`eas webhook:update`](#eas-webhookupdate)
- [`eas webhook:view ID`](#eas-webhookview-id)
- [`eas whoami`](#eas-whoami)
## `eas account:login`
@@ -1058,4 +1059,5 @@ DESCRIPTION
ALIASES
$ eas whoami
```
<!-- commandsstop -->
@@ -1,17 +1,17 @@
import { fixArchiveUrl } from '../files';
import { fixS3Url } from '../uploads';
describe(fixArchiveUrl, () => {
describe(fixS3Url, () => {
it('fixes broken links', () => {
const input =
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
const expectedOutput =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixArchiveUrl(input)).toBe(expectedOutput);
expect(fixS3Url(input)).toBe(expectedOutput);
});
it('does not change correct urls', () => {
const correctUrl =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixArchiveUrl(correctUrl)).toBe(correctUrl);
expect(fixS3Url(correctUrl)).toBe(correctUrl);
});
});
@@ -443,7 +443,11 @@ export default class UpdatePublish extends EasCommand {
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
const { uniqueUploadedAssetCount } = await uploadAssetsAsync(
assets,
spinnerText => (assetSpinner.text = `Uploading assets... ${spinnerText}`)
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading assets. Finished (${
totalAssets - missingAssets
}/${totalAssets})`;
}
);
uploadedAssetCount = uniqueUploadedAssetCount;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
@@ -9,7 +9,6 @@ import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { PublishQuery } from '../../graphql/queries/PublishQuery';
import {
MetadataJoi,
TIMEOUT_LIMIT,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
convertAssetToUpdateInfoGroupFormatAsync,
@@ -412,38 +411,6 @@ describe(uploadAssetsAsync, () => {
return { specifications: ['{}', '{}', '{}'] };
});
// beforeEach(() => {
// jest.useFakeTimers();
// });
it('throws an error if the upload exceeds TIMEOUT_LIMIT', async () => {
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
const status = AssetMetadataStatus.DoesNotExist;
mockdate.set(Date.now() + TIMEOUT_LIMIT + 1);
jest.runAllTimers();
return [
{
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
status,
__typename: 'AssetMetadataResult',
}, // userDefinedAsset
{
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
status,
__typename: 'AssetMetadataResult',
}, // android.code
{
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
status,
__typename: 'AssetMetadataResult',
}, // ios.code
];
});
mockdate.set(0);
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).rejects.toThrow(
'Asset upload timed out. Please try again.'
);
});
it('resolves if the assets are already uploaded', async () => {
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
const status = AssetMetadataStatus.Exists;
@@ -530,15 +497,15 @@ describe(uploadAssetsAsync, () => {
}, // ios.code
];
});
const updateSpinnerFn = jest.fn(_text => {});
const updateSpinnerFn = jest.fn((_totalAssets, _missingAssets) => {});
mockdate.set(0);
await uploadAssetsAsync(assetsForUpdateInfoGroup, updateSpinnerFn);
const calls = updateSpinnerFn.mock.calls;
expect(calls).toEqual([
['6 assets present'],
['3 unique assets found'],
['2 new assets uploading'],
[3, 3],
[3, 2],
[3, 0],
]);
});
});
+11 -17
View File
@@ -15,8 +15,6 @@ import { uploadWithPresignedPostAsync } from '../uploads';
import { expoCommandAsync } from '../utils/expoCli';
import uniqBy from '../utils/expodash/uniqBy';
export const TIMEOUT_LIMIT = 60_000; // 1 minute
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
type Metadata = {
version: number;
@@ -255,7 +253,7 @@ type AssetUploadResult = {
export async function uploadAssetsAsync(
assetsForUpdateInfoGroup: CollectedAssets,
updateSpinnerText?: (updatedText: string) => void
updateSpinnerText?: (totalAssets: number, missingAssets: number) => void
): Promise<AssetUploadResult> {
let assets: RawAsset[] = [];
let platform: keyof CollectedAssets;
@@ -266,7 +264,6 @@ export async function uploadAssetsAsync(
...assetsForUpdateInfoGroup[platform]!.assets,
];
}
updateSpinnerText?.(`${assets.length} ${assets.length === 1 ? 'asset' : 'assets'} present`);
const assetsWithStorageKey = await Promise.all(
assets.map(async asset => {
@@ -281,9 +278,10 @@ export async function uploadAssetsAsync(
storageKey: string;
}
>(assetsWithStorageKey, asset => asset.storageKey);
updateSpinnerText?.(
`${uniqueAssets.length} unique ${uniqueAssets.length === 1 ? 'asset' : 'assets'} found`
);
const totalAssets = uniqueAssets.length;
updateSpinnerText?.(totalAssets, totalAssets);
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(uniqueAssets);
const uniqueUploadedAssetCount = missingAssets.length;
@@ -291,6 +289,8 @@ export async function uploadAssetsAsync(
missingAssets.map(ma => ma.contentType)
);
updateSpinnerText?.(totalAssets, missingAssets.length);
const assetUploadPromiseLimit = promiseLimit(15);
await Promise.all(
@@ -302,21 +302,15 @@ export async function uploadAssetsAsync(
})
);
updateSpinnerText?.(
`${missingAssets.length} new ${missingAssets.length === 1 ? 'asset' : 'assets'} uploading`
);
// Wait up to TIMEOUT_LIMIT for assets to be uploaded and processed
const start = Date.now();
let timeout = 1;
while (missingAssets.length > 0) {
const timeoutPromise = new Promise(resolve => setTimeout(resolve, timeout * 1000)); // linear backoff
const timeoutPromise = new Promise(resolve =>
setTimeout(resolve, Math.min(timeout * 1000, 5000))
); // linear backoff
missingAssets = await filterOutAssetsThatAlreadyExistAsync(missingAssets);
await timeoutPromise; // await after filterOutAssetsThatAlreadyExistAsync for easy mocking with jest.runAllTimers
timeout += 1;
if (Date.now() - start > TIMEOUT_LIMIT) {
throw new Error('Asset upload timed out. Please try again.');
}
updateSpinnerText?.(totalAssets, missingAssets.length);
}
return {
assetCount: assets.length,
@@ -1,7 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { Headers, Response } from 'node-fetch';
import { v4 as uuidv4 } from 'uuid';
import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated';
@@ -244,8 +243,7 @@ describe(getArchiveAsync, () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
const response = new Response(undefined, { headers: new Headers([['location', ARCHIVE_URL]]) });
jest.mocked(uploadAsync).mockResolvedValueOnce({ response, bucketKey: 'wat' });
jest.mocked(uploadAsync).mockResolvedValueOnce({ url: ARCHIVE_URL, bucketKey: 'wat' });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
+2 -16
View File
@@ -1,5 +1,4 @@
import fs from 'fs-extra';
import { URL } from 'url';
import { UploadSessionType } from '../../graphql/generated';
import { uploadAsync } from '../../uploads';
@@ -16,7 +15,7 @@ export async function isExistingFileAsync(filePath: string): Promise<boolean> {
export async function uploadAppArchiveAsync(path: string): Promise<string> {
const fileSize = (await fs.stat(path)).size;
const { response } = await uploadAsync(
const { url } = await uploadAsync(
UploadSessionType.EasSubmitAppArchive,
path,
createProgressTracker({
@@ -25,18 +24,5 @@ export async function uploadAppArchiveAsync(path: string): Promise<string> {
completedMessage: 'Uploaded to EAS Submit',
})
);
const url = response.headers.get('location');
return fixArchiveUrl(String(url));
}
/**
* S3 returns broken URLs, sth like:
* https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c
* This function replaces %2F with /.
*/
export function fixArchiveUrl(archiveUrl: string): string {
const parsed = new URL(archiveUrl);
parsed.pathname = decodeURIComponent(parsed.pathname);
return parsed.toString();
return url;
}
+20 -2
View File
@@ -2,6 +2,8 @@ import assert from 'assert';
import FormData from 'form-data';
import fs from 'fs-extra';
import { Response } from 'node-fetch';
import nullthrows from 'nullthrows';
import { URL } from 'url';
import fetch from './fetch';
import { UploadSessionType } from './graphql/generated';
@@ -12,12 +14,17 @@ export async function uploadAsync(
type: UploadSessionType,
path: string,
handleProgressEvent: ProgressHandler
): Promise<{ response: Response; bucketKey: string }> {
): Promise<{ url: string; bucketKey: string }> {
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(type);
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
const response = await uploadWithPresignedPostAsync(path, presignedPost, handleProgressEvent);
return { response, bucketKey: presignedPost.fields.key };
const location = nullthrows(
response.headers.get('location'),
`location does not exist in response headers (make sure you're uploading to AWS S3)`
);
const url = fixS3Url(location);
return { url, bucketKey: presignedPost.fields.key };
}
export async function uploadWithPresignedPostAsync(
@@ -65,3 +72,14 @@ export async function uploadWithPresignedPostAsync(
return await uploadPromise;
}
}
/**
* S3 returns broken URLs, sth like:
* https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c
* This function replaces %2F with /.
*/
export function fixS3Url(archiveUrl: string): string {
const parsed = new URL(archiveUrl);
parsed.pathname = decodeURIComponent(parsed.pathname);
return parsed.toString();
}
+1 -1
View File
@@ -34,7 +34,7 @@ function resolveProfile({
profileName: string;
depth?: number;
}): EasJsonBuildProfileResolved {
if (depth >= 2) {
if (depth >= 5) {
throw new Error(
'Too long chain of profile extensions, make sure "extends" keys do not make a cycle'
);