mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7817c58f68 | ||
|
|
3f8ffac11c | ||
|
|
137bee4db0 | ||
|
|
5484dd6074 | ||
|
|
8fd3fe0a3a | ||
|
|
ce93a92dc8 | ||
|
|
7cb4981d50 | ||
|
|
e16d25f911 | ||
|
|
49d49184f2 |
No files matched your search
@@ -8,6 +8,18 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `eas account:avatar:set` to upload an account avatar from the command line. ([#4073](https://github.com/expo/eas-cli/pull/4073) by [@brentvatne](https://github.com/brentvatne))
|
||||
- [eas-cli] Handle new server-side errors thrown when observe features are blocked. ([#4042](https://github.com/expo/eas-cli/pull/4042) by [@douglowder](https://github.com/douglowder))
|
||||
- [eas-cli] Add `eas project:icon:set` to upload a project icon from the command line. ([#4068](https://github.com/expo/eas-cli/pull/4068) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [22.2.0](https://github.com/expo/eas-cli/releases/tag/v22.2.0) - 2026-08-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `--device` flag to `eas simulator` for selecting the virtual device to start. ([#4172](https://github.com/expo/eas-cli/pull/4172) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- [eas-cli] Add support for Appium EAS Simulator sessions. ([#4190](https://github.com/expo/eas-cli/pull/4190) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
@@ -17,9 +29,13 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- [eas-cli] Send the expected `Content-MD5` checksum with GCS signed URL uploads so GCS rejects corrupted uploads. ([#4208](https://github.com/expo/eas-cli/pull/4208) by [@wschurman](https://github.com/wschurman))
|
||||
- [eas-cli] Resolve the `eas go` SDK version to its repack target, so `--sdk-version 57` no longer fails with "No Expo Go repack target is published for SDK 57". ([#4215](https://github.com/expo/eas-cli/pull/4215) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Handle missing fingerprint source in fingerprint:compare command. ([#4210](https://github.com/expo/eas-cli/pull/4210) by [@wschurman](https://github.com/wschurman))
|
||||
- [build-tools] Upload tvOS (and other non-iOS) IPAs to the App Store Connect platform declared in the bundle's `DTPlatformName`, instead of always uploading to the iOS train. ([#4234](https://github.com/expo/eas-cli/pull/4234) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- [eas-cli] Fix lint and enforce max-warnings=0. ([#4219](https://github.com/expo/eas-cli/pull/4219) by [@wschurman](https://github.com/wschurman))
|
||||
- [build-tools] Rename serve-sim ngrok preview URLs to use the `web-preview` prefix. ([#4231](https://github.com/expo/eas-cli/pull/4231) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [22.0.0](https://github.com/expo/eas-cli/releases/tag/v22.0.0) - 2026-08-14
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"STAGING": "22.0.0",
|
||||
"STAGING": "22.2.0",
|
||||
"PRODUCTION": "21.5.0"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "22.1.0",
|
||||
"version": "22.2.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"actionlint": "actionlint",
|
||||
"lint": "oxlint --type-aware --import-plugin --tsconfig ./tsconfig.oxlint.json .",
|
||||
"lint": "oxlint --type-aware --import-plugin --max-warnings=0 --tsconfig ./tsconfig.oxlint.json .",
|
||||
"fmt": "oxfmt .",
|
||||
"fmt:check": "oxfmt --check .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "22.1.0",
|
||||
"version": "22.2.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -26,6 +26,7 @@ describe(readIpaInfoAsync, () => {
|
||||
bundleIdentifier: 'dev.expo.SmallestAppExample',
|
||||
bundleShortVersion: '1.0',
|
||||
bundleVersion: '1',
|
||||
dtPlatformName: 'iphoneos',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ export type IpaInfo = {
|
||||
bundleIdentifier: string;
|
||||
bundleShortVersion: string;
|
||||
bundleVersion: string;
|
||||
// `DTPlatformName` from the bundle's Info.plist (e.g. `iphoneos`, `appletvos`,
|
||||
// `macosx`, `xros`), or null when absent. Used to pick the App Store Connect
|
||||
// platform for the upload. See `AscApiUtils.ascPlatformFromDtPlatformName`.
|
||||
dtPlatformName: string | null;
|
||||
};
|
||||
|
||||
const INFO_PLIST_PATH_REGEXP = /^Payload\/[^/]+\.app\/Info\.plist$/;
|
||||
@@ -91,10 +95,14 @@ export async function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo> {
|
||||
);
|
||||
}
|
||||
|
||||
const dtPlatformName =
|
||||
typeof infoPlist.DTPlatformName === 'string' ? infoPlist.DTPlatformName : null;
|
||||
|
||||
return {
|
||||
bundleIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
dtPlatformName,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof UserError) {
|
||||
|
||||
@@ -115,16 +115,27 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
`Uploading Build to "${appResponse.data.attributes.name}" (${ascAppBundleIdentifier})...`
|
||||
);
|
||||
|
||||
// Derive the App Store Connect platform from the IPA itself so tvOS (and
|
||||
// other) binaries are uploaded to the correct version train instead of the
|
||||
// iOS one. Falls back to `IOS` if the platform cannot be read.
|
||||
const ipaInfoResult = await asyncResult(readIpaInfoAsync(ipaPath));
|
||||
const platform = ipaInfoResult.ok
|
||||
? AscApiUtils.ascPlatformFromDtPlatformName(ipaInfoResult.value.dtPlatformName)
|
||||
: 'IOS';
|
||||
stepsCtx.logger.info(`Detected App Store Connect platform: ${platform}`);
|
||||
|
||||
stepsCtx.logger.info('Creating Build Upload...');
|
||||
const buildUploadResponse = await AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
platform,
|
||||
});
|
||||
|
||||
const buildUploadId = buildUploadResponse.data.id;
|
||||
const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/ios/${buildUploadId}`;
|
||||
const platformPathSegment = AscApiUtils.testFlightPlatformPathSegment(platform);
|
||||
const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/${platformPathSegment}/${buildUploadId}`;
|
||||
outputs.build_upload_id.set(buildUploadId);
|
||||
outputs.build_upload_url.set(buildUploadUrl);
|
||||
|
||||
|
||||
@@ -221,14 +221,14 @@ describe(startNgrokTunnelAsync, () => {
|
||||
it('uses a 128-bit capability hostname and exposes explicit cleanup', async () => {
|
||||
const close = jest.fn().mockResolvedValue(undefined);
|
||||
jest.mocked(ngrok.forward).mockResolvedValue({
|
||||
url: () => 'https://serve-sim.example.test',
|
||||
url: () => 'https://web-preview.example.test',
|
||||
close,
|
||||
} as never);
|
||||
|
||||
const tunnel = await startNgrokTunnelAsync({
|
||||
port: 4321,
|
||||
subdomainPrefix: 'serve-sim',
|
||||
baseDomain: 'tunnel.example.test',
|
||||
subdomainPrefix: 'web-preview',
|
||||
baseDomain: 'eas-simulator.ngrok.dev',
|
||||
authtoken: 'token',
|
||||
logger: createLoggerMock(),
|
||||
});
|
||||
@@ -237,10 +237,10 @@ describe(startNgrokTunnelAsync, () => {
|
||||
expect.objectContaining({
|
||||
addr: 4321,
|
||||
authtoken: 'token',
|
||||
domain: expect.stringMatching(/^serve-sim-[a-f0-9]{32}\.tunnel\.example\.test$/),
|
||||
domain: expect.stringMatching(/^web-preview-[a-f0-9]{32}\.eas-simulator\.ngrok\.dev$/),
|
||||
})
|
||||
);
|
||||
expect(tunnel.url).toBe('https://serve-sim.example.test');
|
||||
expect(tunnel.url).toBe('https://web-preview.example.test');
|
||||
await tunnel.stopAsync();
|
||||
await tunnel.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -272,6 +272,10 @@ export type AscApiClientPostApi = {
|
||||
};
|
||||
};
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/platform
|
||||
export type AscPlatform =
|
||||
AscApiClientPostApi['/v1/buildUploads']['request']['data']['attributes']['platform'];
|
||||
|
||||
export type AscApiClientPatchApi = {
|
||||
[Path in keyof typeof PatchApi]: {
|
||||
request: z.input<(typeof PatchApi)[Path]['request']>;
|
||||
|
||||
@@ -5,9 +5,44 @@ import {
|
||||
AscApiClientGetApi,
|
||||
AscApiClientPostApi,
|
||||
AscApiRequestError,
|
||||
AscPlatform,
|
||||
} from './AscApiClient';
|
||||
|
||||
export namespace AscApiUtils {
|
||||
/**
|
||||
* Maps a bundle's `DTPlatformName` (from its Info.plist) to the App Store
|
||||
* Connect platform used for a build upload. Unknown or missing values fall
|
||||
* back to `IOS` to preserve the previous default.
|
||||
*/
|
||||
export function ascPlatformFromDtPlatformName(dtPlatformName: string | null): AscPlatform {
|
||||
switch (dtPlatformName) {
|
||||
case 'appletvos':
|
||||
return 'TV_OS';
|
||||
case 'macosx':
|
||||
return 'MAC_OS';
|
||||
case 'xros':
|
||||
return 'VISION_OS';
|
||||
case 'iphoneos':
|
||||
default:
|
||||
return 'IOS';
|
||||
}
|
||||
}
|
||||
|
||||
/** The App Store Connect TestFlight URL path segment for a platform. */
|
||||
export function testFlightPlatformPathSegment(platform: AscPlatform): string {
|
||||
switch (platform) {
|
||||
case 'TV_OS':
|
||||
return 'tvos';
|
||||
case 'MAC_OS':
|
||||
return 'macos';
|
||||
case 'VISION_OS':
|
||||
return 'visionos';
|
||||
case 'IOS':
|
||||
default:
|
||||
return 'ios';
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAppInfoAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
@@ -61,18 +96,20 @@ export namespace AscApiUtils {
|
||||
appleAppIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
platform,
|
||||
}: {
|
||||
client: Pick<AscApiClient, 'postAsync'>;
|
||||
appleAppIdentifier: string;
|
||||
bundleShortVersion: string;
|
||||
bundleVersion: string;
|
||||
platform: AscPlatform;
|
||||
}): Promise<AscApiClientPostApi['/v1/buildUploads']['response']> {
|
||||
try {
|
||||
return await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
platform,
|
||||
cfBundleShortVersionString: bundleShortVersion,
|
||||
cfBundleVersion: bundleVersion,
|
||||
},
|
||||
|
||||
@@ -148,6 +148,7 @@ describe('AscApiUtils', () => {
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
platform: 'IOS',
|
||||
})
|
||||
).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -176,10 +177,40 @@ describe('AscApiUtils', () => {
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
platform: 'IOS',
|
||||
})
|
||||
).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it('sends the requested platform in the build upload attributes', async () => {
|
||||
const response = {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: 'fdf9c476-aaa4-4ead-b91c-6e3cc3a47805',
|
||||
},
|
||||
} as const;
|
||||
const client = {
|
||||
postAsync: jest.fn().mockResolvedValue(response),
|
||||
};
|
||||
|
||||
await AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
platform: 'TV_OS',
|
||||
});
|
||||
|
||||
expect(client.postAsync).toHaveBeenCalledWith(
|
||||
'/v1/buildUploads',
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
attributes: expect.objectContaining({ platform: 'TV_OS' }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rethrows when error payload includes mixed error codes', async () => {
|
||||
const payload = {
|
||||
errors: [
|
||||
@@ -208,8 +239,32 @@ describe('AscApiUtils', () => {
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
platform: 'IOS',
|
||||
})
|
||||
).rejects.toBe(mixedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ascPlatformFromDtPlatformName', () => {
|
||||
it('maps known DTPlatformName values to App Store Connect platforms', () => {
|
||||
expect(AscApiUtils.ascPlatformFromDtPlatformName('iphoneos')).toBe('IOS');
|
||||
expect(AscApiUtils.ascPlatformFromDtPlatformName('appletvos')).toBe('TV_OS');
|
||||
expect(AscApiUtils.ascPlatformFromDtPlatformName('macosx')).toBe('MAC_OS');
|
||||
expect(AscApiUtils.ascPlatformFromDtPlatformName('xros')).toBe('VISION_OS');
|
||||
});
|
||||
|
||||
it('falls back to IOS for unknown or missing values', () => {
|
||||
expect(AscApiUtils.ascPlatformFromDtPlatformName(null)).toBe('IOS');
|
||||
expect(AscApiUtils.ascPlatformFromDtPlatformName('watchos')).toBe('IOS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('testFlightPlatformPathSegment', () => {
|
||||
it('returns the TestFlight URL segment for each platform', () => {
|
||||
expect(AscApiUtils.testFlightPlatformPathSegment('IOS')).toBe('ios');
|
||||
expect(AscApiUtils.testFlightPlatformPathSegment('TV_OS')).toBe('tvos');
|
||||
expect(AscApiUtils.testFlightPlatformPathSegment('MAC_OS')).toBe('macos');
|
||||
expect(AscApiUtils.testFlightPlatformPathSegment('VISION_OS')).toBe('visionos');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -722,7 +722,7 @@ export async function startServeSimWithTunnelAsync(
|
||||
await waitForServeSimReadyAsync({ serveSim, port, timeoutMs });
|
||||
const tunnel = await startNgrokTunnelAsync({
|
||||
port,
|
||||
subdomainPrefix: 'serve-sim',
|
||||
subdomainPrefix: 'web-preview',
|
||||
baseDomain,
|
||||
authtoken: getNgrokAuthtokenOrThrow(env),
|
||||
logger,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import resolveFrom from 'resolve-from';
|
||||
import spawnAsync from '@expo/turtle-spawn';
|
||||
|
||||
import { expoCommandAsync, ExpoCLIModuleNotFoundError } from '../expoCli';
|
||||
import { ExpoCLIModuleNotFoundError, expoCommandAsync } from '../expoCli';
|
||||
|
||||
jest.mock('resolve-from', () => ({
|
||||
__esModule: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "22.1.0",
|
||||
"version": "22.2.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "yarn run clean && yarn run build",
|
||||
"lint": "oxlint --type-aware --tsconfig ../../tsconfig.oxlint.json .",
|
||||
"lint": "oxlint --type-aware --max-warnings=0 --tsconfig ../../tsconfig.oxlint.json .",
|
||||
"test": "echo 'No tests yet.'",
|
||||
"watch": "yarn run build:dev -w",
|
||||
"build:dev": "ncc build ./src/index.ts -o build/",
|
||||
|
||||
+148
-124
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "22.1.0",
|
||||
"version": "22.2.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('Go command', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function makeCmd(argv: string[] = []) {
|
||||
function makeCmd(argv: string[] = []): Go {
|
||||
const ctx = {
|
||||
loggedIn: { actor: mockActor as any, graphqlClient: {} as any },
|
||||
analytics: {} as any,
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AccountUploadSessionType } from '../../../../graphql/generated';
|
||||
import { AccountQuery } from '../../../../graphql/queries/AccountQuery';
|
||||
import { selectAsync } from '../../../../prompts';
|
||||
import { uploadAccountScopedFileAtPathToGCSAsync } from '../../../../uploads';
|
||||
import { Actor } from '../../../../user/User';
|
||||
import { sleepAsync } from '../../../../utils/promise';
|
||||
import AccountAvatarSet from '../set';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../../graphql/queries/AccountQuery');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora', () => ({
|
||||
ora: jest.fn(() => {
|
||||
const spinner = {
|
||||
fail: jest.fn(),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
};
|
||||
spinner.start.mockReturnValue(spinner);
|
||||
return spinner;
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../../uploads');
|
||||
jest.mock('../../../../utils/promise');
|
||||
|
||||
const mockByIdProfileImageUrlAsync = jest.mocked(AccountQuery.byIdProfileImageUrlAsync);
|
||||
const mockSelectAsync = jest.mocked(selectAsync);
|
||||
const mockUploadAsync = jest.mocked(uploadAccountScopedFileAtPathToGCSAsync);
|
||||
const mockSleepAsync = jest.mocked(sleepAsync);
|
||||
|
||||
function getMockOclifConfig(): Config {
|
||||
const config = new Config({ root: __dirname });
|
||||
config.runHook = async () => ({
|
||||
failures: [],
|
||||
successes: [],
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
describe(AccountAvatarSet, () => {
|
||||
const graphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const personalAccount = { id: 'account-1', name: 'test-user' };
|
||||
const orgAccount = { id: 'account-2', name: 'test-org' };
|
||||
|
||||
let now: number;
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
jest.clearAllMocks();
|
||||
|
||||
now = 0;
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
||||
mockSleepAsync.mockImplementation(async ms => {
|
||||
now += ms;
|
||||
});
|
||||
|
||||
mockByIdProfileImageUrlAsync
|
||||
.mockResolvedValueOnce('https://example.com/old.png')
|
||||
.mockResolvedValueOnce('https://example.com/new.png');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createCommand(
|
||||
argv: string[],
|
||||
accounts: { id: string; name: string }[]
|
||||
): AccountAvatarSet {
|
||||
const command = new AccountAvatarSet(argv, mockConfig);
|
||||
// @ts-expect-error getContextAsync is protected
|
||||
jest.spyOn(command, 'getContextAsync').mockResolvedValue({
|
||||
loggedIn: { graphqlClient, actor: { accounts } as Actor },
|
||||
});
|
||||
return command;
|
||||
}
|
||||
|
||||
it('uploads the avatar for the account passed as an argument', async () => {
|
||||
vol.fromJSON({ '/app/avatar.png': 'fake-png-bytes' });
|
||||
|
||||
const command = createCommand(['/app/avatar.png', 'test-org'], [personalAccount, orgAccount]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockUploadAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
type: AccountUploadSessionType.ProfileImageUpload,
|
||||
accountId: orgAccount.id,
|
||||
path: '/app/avatar.png',
|
||||
});
|
||||
expect(mockSelectAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the only account without prompting', async () => {
|
||||
vol.fromJSON({ '/app/avatar.png': 'fake-png-bytes' });
|
||||
|
||||
const command = createCommand(['/app/avatar.png'], [personalAccount]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockUploadAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.objectContaining({ accountId: personalAccount.id })
|
||||
);
|
||||
expect(mockSelectAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts for an account when there are multiple', async () => {
|
||||
vol.fromJSON({ '/app/avatar.png': 'fake-png-bytes' });
|
||||
mockSelectAsync.mockResolvedValue(orgAccount);
|
||||
|
||||
const command = createCommand(['/app/avatar.png'], [personalAccount, orgAccount]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockSelectAsync).toHaveBeenCalled();
|
||||
expect(mockUploadAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.objectContaining({ accountId: orgAccount.id })
|
||||
);
|
||||
});
|
||||
|
||||
it('throws in non-interactive mode with multiple accounts and no account name', async () => {
|
||||
vol.fromJSON({ '/app/avatar.png': 'fake-png-bytes' });
|
||||
|
||||
const command = createCommand(
|
||||
['/app/avatar.png', '--non-interactive'],
|
||||
[personalAccount, orgAccount]
|
||||
);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'ACCOUNT_NAME argument must be provided when running in `--non-interactive` mode.'
|
||||
);
|
||||
expect(mockUploadAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws for an account the user does not have access to', async () => {
|
||||
vol.fromJSON({ '/app/avatar.png': 'fake-png-bytes' });
|
||||
|
||||
const command = createCommand(['/app/avatar.png', 'other-org'], [personalAccount, orgAccount]);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Account "other-org" not found or you don\'t have access. Available accounts: test-user, test-org'
|
||||
);
|
||||
expect(mockUploadAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when the file does not exist', async () => {
|
||||
const command = createCommand(['/app/missing.png', 'test-org'], [personalAccount, orgAccount]);
|
||||
await expect(command.runAsync()).rejects.toThrow('No file found at /app/missing.png');
|
||||
expect(mockUploadAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../../api';
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../../commandUtils/flags';
|
||||
import { AccountUploadSessionType } from '../../../graphql/generated';
|
||||
import { AccountQuery } from '../../../graphql/queries/AccountQuery';
|
||||
import Log, { link } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { selectAsync } from '../../../prompts';
|
||||
import { uploadAccountScopedFileAtPathToGCSAsync } from '../../../uploads';
|
||||
import {
|
||||
pollForProfileImageChangeAsync,
|
||||
validateProfileImageAsync,
|
||||
} from '../../../utils/profileImages';
|
||||
|
||||
export default class AccountAvatarSet extends EasCommand {
|
||||
static override description =
|
||||
'set the avatar for an account (for a personal account, this is your user avatar)';
|
||||
|
||||
static override args = {
|
||||
path: Args.string({
|
||||
required: true,
|
||||
description:
|
||||
'Path to the avatar image (PNG or JPEG, at most 10 MB). Non-square images are center-cropped to a square.',
|
||||
}),
|
||||
account_name: Args.string({
|
||||
required: false,
|
||||
description:
|
||||
'Name of the account to set the avatar for. Defaults to a prompt when you have access to multiple accounts.',
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { path: imagePath, account_name: accountName },
|
||||
flags,
|
||||
} = await this.parse(AccountAvatarSet);
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
const {
|
||||
loggedIn: { graphqlClient, actor },
|
||||
} = await this.getContextAsync(AccountAvatarSet, { nonInteractive });
|
||||
|
||||
await validateProfileImageAsync(imagePath);
|
||||
|
||||
let targetAccount: { id: string; name: string };
|
||||
if (accountName) {
|
||||
const found = actor.accounts.find(account => account.name === accountName);
|
||||
if (found) {
|
||||
targetAccount = found;
|
||||
} else {
|
||||
const availableAccounts = actor.accounts.map(account => account.name).join(', ');
|
||||
throw new Error(
|
||||
`Account "${accountName}" not found or you don't have access. Available accounts: ${availableAccounts}`
|
||||
);
|
||||
}
|
||||
} else if (actor.accounts.length === 1) {
|
||||
targetAccount = actor.accounts[0];
|
||||
} else if (nonInteractive) {
|
||||
throw new Error(
|
||||
'ACCOUNT_NAME argument must be provided when running in `--non-interactive` mode.'
|
||||
);
|
||||
} else {
|
||||
targetAccount = await selectAsync(
|
||||
'Select account to set the avatar for:',
|
||||
actor.accounts.map(account => ({
|
||||
title: account.name,
|
||||
value: account,
|
||||
})),
|
||||
{ initial: actor.accounts[0] }
|
||||
);
|
||||
}
|
||||
|
||||
const accountSettingsUrl = new URL(
|
||||
`/accounts/${targetAccount.name}/settings`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
const previousProfileImageUrl = await AccountQuery.byIdProfileImageUrlAsync(
|
||||
graphqlClient,
|
||||
targetAccount.id
|
||||
);
|
||||
|
||||
const spinner = ora('Uploading avatar').start();
|
||||
try {
|
||||
await uploadAccountScopedFileAtPathToGCSAsync(graphqlClient, {
|
||||
type: AccountUploadSessionType.ProfileImageUpload,
|
||||
accountId: targetAccount.id,
|
||||
path: imagePath,
|
||||
});
|
||||
|
||||
spinner.text = 'Processing avatar';
|
||||
await pollForProfileImageChangeAsync({
|
||||
fetchProfileImageUrlAsync: async () =>
|
||||
await AccountQuery.byIdProfileImageUrlAsync(graphqlClient, targetAccount.id),
|
||||
previousProfileImageUrl,
|
||||
fallbackUrl: accountSettingsUrl,
|
||||
});
|
||||
spinner.succeed(`Set avatar for ${chalk.bold(targetAccount.name)}`);
|
||||
Log.withTick(`View it in the account settings: ${link(accountSettingsUrl)}`);
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to set avatar');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ describe(IntegrationsSupabaseDashboard, () => {
|
||||
|
||||
it('fails the spinner when the browser cannot be opened', async () => {
|
||||
jest.mocked(openBrowserAsync).mockResolvedValue(false as never);
|
||||
const { ora } = jest.requireMock('../../../../ora') as { ora: jest.Mock };
|
||||
const { ora } = jest.requireMock('../../../../ora');
|
||||
const spinner = {
|
||||
start: jest.fn().mockReturnThis(),
|
||||
succeed: jest.fn().mockReturnThis(),
|
||||
@@ -116,7 +116,7 @@ describe(IntegrationsSupabaseDashboard, () => {
|
||||
|
||||
it('fails the spinner and rethrows when opening the browser throws', async () => {
|
||||
jest.mocked(openBrowserAsync).mockRejectedValue(new Error('no browser'));
|
||||
const { ora } = jest.requireMock('../../../../ora') as { ora: jest.Mock };
|
||||
const { ora } = jest.requireMock('../../../../ora');
|
||||
const spinner = {
|
||||
start: jest.fn().mockReturnThis(),
|
||||
succeed: jest.fn().mockReturnThis(),
|
||||
|
||||
@@ -137,7 +137,7 @@ describe(ProjectIconSet, () => {
|
||||
|
||||
const command = createCommand(['/app/icon.png']);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Timed out waiting for the icon to be processed'
|
||||
'Timed out waiting for the image to be processed'
|
||||
);
|
||||
expect(mockUploadAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@ import { EASNonInteractiveFlag } from '../../../commandUtils/flags';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import Log, { link } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { uploadProjectIconAsync } from '../../../project/projectIcon';
|
||||
import {
|
||||
pollForProfileImageChangeAsync,
|
||||
uploadProjectIconAsync,
|
||||
validateIconAsync,
|
||||
} from '../../../project/projectIcon';
|
||||
validateProfileImageAsync,
|
||||
} from '../../../utils/profileImages';
|
||||
|
||||
export default class ProjectIconSet extends EasCommand {
|
||||
static override description = 'set the project icon displayed on the EAS dashboard';
|
||||
@@ -45,7 +45,7 @@ export default class ProjectIconSet extends EasCommand {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
await validateIconAsync(imagePath);
|
||||
await validateProfileImageAsync(imagePath);
|
||||
|
||||
const app = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const projectDashboardUrl = getProjectDashboardUrl(app.ownerAccount.name, app.slug);
|
||||
@@ -58,13 +58,12 @@ export default class ProjectIconSet extends EasCommand {
|
||||
try {
|
||||
await uploadProjectIconAsync(graphqlClient, { projectId, imagePath });
|
||||
|
||||
// The icon is processed asynchronously (resized and assigned to the
|
||||
// project by the server), so poll until the icon URL changes.
|
||||
spinner.text = 'Processing project icon';
|
||||
await pollForProfileImageChangeAsync(graphqlClient, {
|
||||
projectId,
|
||||
await pollForProfileImageChangeAsync({
|
||||
fetchProfileImageUrlAsync: async () =>
|
||||
await AppQuery.byIdProfileImageUrlAsync(graphqlClient, projectId),
|
||||
previousProfileImageUrl,
|
||||
projectDashboardUrl,
|
||||
fallbackUrl: projectDashboardUrl,
|
||||
});
|
||||
spinner.succeed(`Set icon for ${chalk.bold(app.fullName)}`);
|
||||
Log.withTick(`View it on the project page: ${link(projectDashboardUrl)}`);
|
||||
|
||||
@@ -260,7 +260,7 @@ describe(UpdateEmbeddedList, () => {
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false, startCursor: 'c1', endCursor: 'c1' },
|
||||
});
|
||||
await createCommand(['--non-interactive']).run();
|
||||
expect(mockLogLog.mock.calls.some(c => /ago/.test(String(c[0])))).toBe(true);
|
||||
expect(mockLogLog.mock.calls.some(c => String(c[0]).includes('ago'))).toBe(true);
|
||||
});
|
||||
|
||||
it('puts "All channels" first in the prompt, then channels in order', async () => {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from '../BuildC
|
||||
import { chooseDevicesAsync } from '../DeviceUtils';
|
||||
import { SetUpAdhocProvisioningProfile, doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
|
||||
import { getAscApiKeyForAppSubmissionsAsync } from '../../api/GraphqlClient';
|
||||
import { AuthenticationMode, AppleTeamType } from '../../appstore/authenticateTypes';
|
||||
import { AppleTeamType, AuthenticationMode } from '../../appstore/authenticateTypes';
|
||||
import { hasAscEnvVars } from '../../appstore/resolveCredentials';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../../graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { promptAsync } from '../../../../prompts';
|
||||
import { AppleTeamType } from '../authenticateTypes';
|
||||
import * as Keychain from '../keychain';
|
||||
import {
|
||||
resolveAppleTeamTypeFromEnvironment,
|
||||
resolveAppleTeamAsync,
|
||||
resolveAppleTeamTypeFromEnvironment,
|
||||
resolveAscApiKeyAsync,
|
||||
resolveUserCredentialsAsync,
|
||||
} from '../resolveCredentials';
|
||||
|
||||
+7
@@ -14649,6 +14649,13 @@ export type AccountByNameQueryVariables = Exact<{
|
||||
|
||||
export type AccountByNameQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, name: string } } };
|
||||
|
||||
export type AccountByIdProfileImageUrlQueryVariables = Exact<{
|
||||
accountId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountByIdProfileImageUrlQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, profileImageUrl: string } } };
|
||||
|
||||
export type AccountFullUsageQueryVariables = Exact<{
|
||||
accountId: Scalars['String']['input'];
|
||||
currentDate: Scalars['DateTime']['input'];
|
||||
|
||||
@@ -4,6 +4,7 @@ import gql from 'graphql-tag';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AccountByIdProfileImageUrlQuery,
|
||||
AccountFullUsageQuery as AccountFullUsageQueryType,
|
||||
AccountFullUsageQueryVariables,
|
||||
AccountUsageForOverageWarningQuery,
|
||||
@@ -45,6 +46,35 @@ export const AccountQuery = {
|
||||
return data.account.byName;
|
||||
},
|
||||
|
||||
async byIdProfileImageUrlAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string
|
||||
): Promise<string | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AccountByIdProfileImageUrlQuery>(
|
||||
gql`
|
||||
query AccountByIdProfileImageUrlQuery($accountId: String!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
profileImageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountId },
|
||||
{
|
||||
requestPolicy: 'network-only',
|
||||
additionalTypenames: ['Account'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byId.profileImageUrl ?? null;
|
||||
},
|
||||
|
||||
async getFullUsageAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
|
||||
@@ -74,10 +74,10 @@ export function mergeEnvContent(rawContent: string, newVars: Record<string, stri
|
||||
continue;
|
||||
}
|
||||
// match[0] runs past the value onto a following comment line, so spans come from the groups.
|
||||
const [keyStart, keyEnd] = match.indices[1]!;
|
||||
const [keyStart, keyEnd] = match.indices[1];
|
||||
const valueRange = match.indices[2];
|
||||
const definitionEnd = valueRange
|
||||
? valueRange[1] - (/\s+$/.exec(match[2]!)?.[0].length ?? 0)
|
||||
? valueRange[1] - (/\s+$/.exec(match[2])?.[0].length ?? 0)
|
||||
: keyEnd + (/^[ \t]*(?:=[ \t]*|:[ \t]+)/.exec(rawContent.slice(keyEnd))?.[0].length ?? 0);
|
||||
|
||||
if (!rewritten.has(key)) {
|
||||
|
||||
@@ -61,7 +61,7 @@ const { AppStoreConnectApiKeyQuery } =
|
||||
|
||||
const mockApp = { id: '123', bundleId: 'com.example.app' };
|
||||
|
||||
function createMockCredentialsCtx(ensureAuthResult?: any) {
|
||||
function createMockCredentialsCtx(ensureAuthResult?: any): any {
|
||||
return {
|
||||
appStore: {
|
||||
ensureAuthenticatedAsync: jest.fn(() => ({
|
||||
@@ -73,7 +73,7 @@ function createMockCredentialsCtx(ensureAuthResult?: any) {
|
||||
} as any;
|
||||
}
|
||||
|
||||
function createBaseArgs(overrides: Record<string, any> = {}) {
|
||||
function createBaseArgs(overrides: Record<string, any> = {}): any {
|
||||
return {
|
||||
projectDir: '/app',
|
||||
profile: { bundleIdentifier: 'com.example.app' } as SubmitProfile,
|
||||
|
||||
@@ -41,7 +41,7 @@ jest.mock('../../prompts', () => ({
|
||||
|
||||
const { confirmAsync } = require('../../prompts') as jest.Mocked<typeof import('../../prompts')>;
|
||||
|
||||
function createArgs(overrides: Record<string, any> = {}) {
|
||||
function createArgs(overrides: Record<string, any> = {}): any {
|
||||
return {
|
||||
projectDir: '/app',
|
||||
profile: { bundleIdentifier: 'com.example.app' } as any,
|
||||
|
||||
@@ -37,7 +37,7 @@ const { loadConfigAsync } = require('../config/resolve') as jest.Mocked<
|
||||
>;
|
||||
const { confirmAsync } = require('../../prompts') as jest.Mocked<typeof import('../../prompts')>;
|
||||
|
||||
function createArgs(overrides: Record<string, any> = {}) {
|
||||
function createArgs(overrides: Record<string, any> = {}): any {
|
||||
return {
|
||||
projectDir: '/app',
|
||||
profile: { metadataPath: 'store.config.json', bundleIdentifier: 'com.example.app' } as any,
|
||||
|
||||
@@ -278,7 +278,7 @@ async function syncPreviewSetAsync(
|
||||
await logAsync(
|
||||
() =>
|
||||
AppPreview.uploadAsync(localization.context, {
|
||||
id: previewSet!.id,
|
||||
id: previewSet.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
|
||||
@@ -234,7 +234,7 @@ async function syncScreenshotSetAsync(
|
||||
const newScreenshot = await logAsync(
|
||||
() =>
|
||||
AppScreenshot.uploadAsync(localization.context, {
|
||||
id: screenshotSet!.id,
|
||||
id: screenshotSet.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
}),
|
||||
|
||||
@@ -10,7 +10,7 @@ export function startAndEndTime({
|
||||
daysBack?: number;
|
||||
start?: string;
|
||||
end?: string;
|
||||
}) {
|
||||
}): { startTime: string; endTime: string } {
|
||||
let startTime: string;
|
||||
let endTime: string;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Platform as ApplePlatform, AppStoreVersion, Build } from '@expo/apple-utils';
|
||||
import { App, AppStoreVersion, Platform as ApplePlatform, Build } from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getSubmissionDetailsUrl } from './utils/urls';
|
||||
|
||||
@@ -42,12 +42,12 @@ export async function uploadAccountScopedFileAtPathToGCSAsync(
|
||||
type,
|
||||
accountId,
|
||||
path,
|
||||
handleProgressEvent,
|
||||
handleProgressEvent = () => {},
|
||||
}: {
|
||||
type: AccountUploadSessionType;
|
||||
accountId: string;
|
||||
path: string;
|
||||
handleProgressEvent: ProgressHandler;
|
||||
handleProgressEvent?: ProgressHandler;
|
||||
}
|
||||
): Promise<string> {
|
||||
const signedUrl = await UploadSessionMutation.createAccountScopedUploadSessionAsync(
|
||||
|
||||
@@ -36,7 +36,9 @@ describe(ensurePNGIsNotTransparentAsync, () => {
|
||||
fs.createReadStream(transparentPngPath).pipe(response);
|
||||
});
|
||||
await new Promise<void>((res, rej) => {
|
||||
const onError = (error: Error): void => rej(error);
|
||||
const onError = (error: Error): void => {
|
||||
rej(error);
|
||||
};
|
||||
server.once('error', onError);
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', onError);
|
||||
|
||||
@@ -163,7 +163,7 @@ describe(pollForBackgroundJobReceiptAsync, () => {
|
||||
});
|
||||
|
||||
it('tolerates consecutive CombinedError fetches before NULL_RECEIPT', async () => {
|
||||
const { CombinedError } = jest.requireActual('@urql/core') as typeof import('@urql/core');
|
||||
const { CombinedError } = jest.requireActual('@urql/core');
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
const receiptId = '123';
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { sleepAsync } from './promise';
|
||||
|
||||
const ALLOWED_EXTENSIONS = ['.png', '.jpg', '.jpeg'];
|
||||
const MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024; // matches the upload session's GCS limit
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const POLL_TIMEOUT_MS = 90_000;
|
||||
|
||||
export async function validateProfileImageAsync(imagePath: string): Promise<void> {
|
||||
if (!(await fs.pathExists(imagePath))) {
|
||||
throw new Error(`No file found at ${imagePath}`);
|
||||
}
|
||||
const extension = path.extname(imagePath).toLowerCase();
|
||||
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
||||
throw new Error(
|
||||
`Unsupported image format "${extension}". The image must be a PNG or JPEG file.`
|
||||
);
|
||||
}
|
||||
const { size } = await fs.stat(imagePath);
|
||||
if (size > MAX_IMAGE_SIZE_BYTES) {
|
||||
throw new Error(
|
||||
`The image is ${(size / 1024 / 1024).toFixed(1)} MB, but the maximum allowed size is 10 MB.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile images are processed asynchronously (resized and assigned to their
|
||||
* entity by the server), so poll until the image URL changes.
|
||||
*/
|
||||
export async function pollForProfileImageChangeAsync({
|
||||
fetchProfileImageUrlAsync,
|
||||
previousProfileImageUrl,
|
||||
fallbackUrl,
|
||||
}: {
|
||||
fetchProfileImageUrlAsync: () => Promise<string | null>;
|
||||
previousProfileImageUrl: string | null;
|
||||
/** Shown in the timeout error as the page where the image may still appear. */
|
||||
fallbackUrl: string;
|
||||
}): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
||||
await sleepAsync(POLL_INTERVAL_MS);
|
||||
const profileImageUrl = await fetchProfileImageUrlAsync();
|
||||
if (profileImageUrl && profileImageUrl !== previousProfileImageUrl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Timed out waiting for the image to be processed. It may still appear shortly: ${chalk.underline(
|
||||
fallbackUrl
|
||||
)}`
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "22.1.0",
|
||||
"version": "22.2.0",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -26,7 +26,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "22.1.0",
|
||||
"@expo/build-tools": "22.2.0",
|
||||
"@expo/eas-build-job": "22.0.0",
|
||||
"@expo/logger": "22.0.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "22.1.0",
|
||||
"version": "22.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "22.1.0",
|
||||
"@expo/build-tools": "22.2.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "22.0.0",
|
||||
|
||||
@@ -2291,7 +2291,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:22.1.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:22.2.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
@@ -2959,7 +2959,7 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:22.1.0"
|
||||
"@expo/build-tools": "npm:22.2.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:22.0.0"
|
||||
@@ -10297,7 +10297,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:22.1.0"
|
||||
"@expo/build-tools": "npm:22.2.0"
|
||||
"@expo/eas-build-job": "npm:22.0.0"
|
||||
"@expo/logger": "npm:22.0.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
|
||||
Reference in new issue
Block a user