mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f2823fa18 | ||
|
|
52e6fd0e81 | ||
|
|
3957f058e9 | ||
|
|
8597524f32 | ||
|
|
7c5190a4ab | ||
|
|
9247f3cf78 | ||
|
|
bde331da63 | ||
|
|
fd055f08b3 | ||
|
|
785d690e65 | ||
|
|
47335f02f0 | ||
|
|
75d9be813f | ||
|
|
cdaa011ce3 | ||
|
|
ee6e197587 | ||
|
|
63baff4daf | ||
|
|
85a4054b17 | ||
|
|
15cd0c5ae2 | ||
|
|
efc78f43d0 | ||
|
|
a905a50f65 | ||
|
|
b36b5b36d7 | ||
|
|
f867b7b9f2 |
No files matched your search
@@ -12,9 +12,9 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
# Pinned to a commit SHA (not the v1.4.0 tag) because this runs under
|
||||
# pull_request_target with a write-scoped token — a re-pointed tag from
|
||||
# a compromised upstream account would give the attacker repo write access.
|
||||
- uses: tobyhs/codemention@bb6bfb2c3ff1e6fee7ee37006bbee6d114057225 # v1.4.0
|
||||
# Pinned to a commit SHA (not a tag or branch) because this runs under
|
||||
# pull_request_target with a write-scoped token — a re-pointed tag or
|
||||
# moving branch would allow unreviewed upstream code to run here.
|
||||
- uses: tobyhs/codemention@ebac5877393ab8693fd73b42708c8552043e9a3b # post-v1.5.2
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
+19
-2
@@ -8,14 +8,31 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Select correct tvOS build target for non-interactive Apple builds when EXPO_TV env variable is set. ([#3907](https://github.com/expo/eas-cli/pull/3907) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- [eas-cli] Bump `@expo/apple-utils` to `2.1.22`. ([#3913](https://github.com/expo/eas-cli/pull/3913) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [20.5.0](https://github.com/expo/eas-cli/releases/tag/v20.5.0) - 2026-06-29
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] `eas integrations:posthog:connect` now supports existing PostHog user connection. ([#3895](https://github.com/expo/eas-cli/pull/3895) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
## [20.4.0](https://github.com/expo/eas-cli/releases/tag/v20.4.0) - 2026-06-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] New command `observe:session` for inspecting events by session ID. ([#3868](https://github.com/expo/eas-cli/pull/3868) by [@douglowder](https://github.com/douglowder))
|
||||
- [eas-cli] Add tiered build-credit usage warnings (approaching/at/over) before each build, lowering the warning threshold to 80% to match email notifications. ([#3882](https://github.com/expo/eas-cli/pull/3882) by [@sarahlane8](https://github.com/sarahlane8))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Fix `eas update --channel` failing with "Channel has no branches associated with it" when the channel was auto-created by a build. A branch with the channel's name is now created and linked, matching the behavior for a channel that doesn't exist yet. ([#3891](https://github.com/expo/eas-cli/pull/3891) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [20.3.0](https://github.com/expo/eas-cli/releases/tag/v20.3.0) - 2026-06-18
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "20.3.0",
|
||||
"version": "20.5.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "20.3.0",
|
||||
"version": "20.4.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -40,7 +40,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
env: {
|
||||
EAS_VERBOSE: '1',
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
NPM_CONFIG_REGISTRY: npmCacheUrl,
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
},
|
||||
logger,
|
||||
cwd: '/tmp/build',
|
||||
@@ -56,6 +56,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
env: {
|
||||
EAS_VERBOSE: '1',
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
NPM_CONFIG_REGISTRY: npmCacheUrl,
|
||||
},
|
||||
});
|
||||
@@ -66,6 +67,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
env: {
|
||||
EAS_VERBOSE: '1',
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
},
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
@@ -110,7 +112,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
packageManager: PackageManager.NPM,
|
||||
env: {
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
NPM_CONFIG_REGISTRY: npmCacheUrl,
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
},
|
||||
logger,
|
||||
cwd: '/tmp/build',
|
||||
@@ -158,7 +160,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
packageManager: PackageManager.NPM,
|
||||
env: {
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
NPM_CONFIG_REGISTRY: 'http://npm.staging.caches.eas-build.internal',
|
||||
EAS_BUILD_NPM_CACHE_URL: 'http://npm.staging.caches.eas-build.internal',
|
||||
},
|
||||
logger,
|
||||
cwd: '/tmp/build',
|
||||
@@ -170,7 +172,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
expect(Sentry.capture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not retry when EAS_USE_NPM_CACHE is not enabled', async () => {
|
||||
it('does not retry when the npm cache is not enabled', async () => {
|
||||
const logger = createMockLogger();
|
||||
const npmCacheUrl = 'http://npm.staging.caches.eas-build.internal';
|
||||
const error = Object.assign(
|
||||
@@ -188,9 +190,7 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
await expect(
|
||||
installDependenciesWithNpmCacheFallbackAsync({
|
||||
packageManager: PackageManager.NPM,
|
||||
env: {
|
||||
NPM_CONFIG_REGISTRY: npmCacheUrl,
|
||||
},
|
||||
env: {},
|
||||
logger,
|
||||
cwd: '/tmp/build',
|
||||
useFrozenLockfile: false,
|
||||
@@ -200,6 +200,40 @@ describe(installDependenciesWithNpmCacheFallbackAsync, () => {
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(Sentry.capture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('installs through the npm cache registry without retrying when the cache install succeeds', async () => {
|
||||
const logger = createMockLogger();
|
||||
const npmCacheUrl = 'http://npm.staging.caches.eas-build.internal';
|
||||
|
||||
jest
|
||||
.mocked(spawn)
|
||||
.mockReturnValueOnce(createSpawnPromise(Promise.resolve(createSpawnResult())));
|
||||
|
||||
await installDependenciesWithNpmCacheFallbackAsync({
|
||||
packageManager: PackageManager.NPM,
|
||||
env: {
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
},
|
||||
logger,
|
||||
cwd: '/tmp/build',
|
||||
useFrozenLockfile: false,
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(spawn).toHaveBeenCalledWith('npm', ['install', '--include=dev'], {
|
||||
cwd: '/tmp/build',
|
||||
logger,
|
||||
infoCallbackFn: undefined,
|
||||
lineTransformer: expect.any(Function),
|
||||
env: {
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
NPM_CONFIG_REGISTRY: npmCacheUrl,
|
||||
},
|
||||
});
|
||||
expect(Sentry.capture).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function createSpawnPromise(result: Promise<SpawnResult>): SpawnPromise<SpawnResult> {
|
||||
|
||||
@@ -93,7 +93,24 @@ export async function installDependenciesWithNpmCacheFallbackAsync({
|
||||
infoCallbackFn?: SpawnOptions['infoCallbackFn'];
|
||||
useFrozenLockfile: boolean;
|
||||
}): Promise<void> {
|
||||
const npmCacheUrl = env.NPM_CONFIG_REGISTRY;
|
||||
const npmCacheUrl = env.EAS_BUILD_NPM_CACHE_URL;
|
||||
|
||||
if (!npmCacheUrl) {
|
||||
await (
|
||||
await installDependenciesAsync({
|
||||
packageManager,
|
||||
env,
|
||||
logger,
|
||||
infoCallbackFn,
|
||||
cwd,
|
||||
useFrozenLockfile,
|
||||
})
|
||||
).spawnPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Installing dependencies using the npm cache registry (${npmCacheUrl}).`);
|
||||
|
||||
let firstErrorLine: string | undefined;
|
||||
let errorLineCount = 0;
|
||||
|
||||
@@ -101,11 +118,11 @@ export async function installDependenciesWithNpmCacheFallbackAsync({
|
||||
await (
|
||||
await installDependenciesAsync({
|
||||
packageManager,
|
||||
env,
|
||||
env: { ...env, NPM_CONFIG_REGISTRY: npmCacheUrl },
|
||||
logger,
|
||||
infoCallbackFn,
|
||||
lineTransformer: (line: string) => {
|
||||
if (isNpmCacheRegistryErrorLine(line, { env, npmCacheUrl })) {
|
||||
if (isNpmCacheRegistryErrorLine(line, npmCacheUrl)) {
|
||||
firstErrorLine ??= line;
|
||||
errorLineCount += 1;
|
||||
}
|
||||
@@ -124,7 +141,7 @@ export async function installDependenciesWithNpmCacheFallbackAsync({
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!isNpmCacheInstallFailure(err, { env, npmCacheUrl })) {
|
||||
if (!isNpmCacheInstallFailure(err, npmCacheUrl)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -144,11 +161,10 @@ export async function installDependenciesWithNpmCacheFallbackAsync({
|
||||
},
|
||||
});
|
||||
|
||||
const { NPM_CONFIG_REGISTRY: _NPM_CONFIG_REGISTRY, ...fallbackEnv } = env;
|
||||
await (
|
||||
await installDependenciesAsync({
|
||||
packageManager,
|
||||
env: fallbackEnv,
|
||||
env,
|
||||
logger,
|
||||
infoCallbackFn,
|
||||
cwd,
|
||||
@@ -172,35 +188,17 @@ export function resolvePackagerDir(ctx: BuildContext<Job>): string {
|
||||
return packagerRunDir;
|
||||
}
|
||||
|
||||
function isNpmCacheInstallFailure(
|
||||
err: unknown,
|
||||
{ env, npmCacheUrl }: { env: Record<string, string | undefined>; npmCacheUrl: string | undefined }
|
||||
): boolean {
|
||||
if (!isNpmCacheRegistryEnabled(env, npmCacheUrl)) {
|
||||
return false;
|
||||
}
|
||||
const errorOutput = getErrorOutput(err);
|
||||
return errorOutput.includes(npmCacheUrl);
|
||||
function isNpmCacheInstallFailure(err: unknown, npmCacheUrl: string): boolean {
|
||||
return getErrorOutput(err).includes(npmCacheUrl);
|
||||
}
|
||||
|
||||
function isNpmCacheRegistryErrorLine(
|
||||
line: string,
|
||||
{ env, npmCacheUrl }: { env: Record<string, string | undefined>; npmCacheUrl: string | undefined }
|
||||
): boolean {
|
||||
function isNpmCacheRegistryErrorLine(line: string, npmCacheUrl: string): boolean {
|
||||
return (
|
||||
isNpmCacheRegistryEnabled(env, npmCacheUrl) &&
|
||||
line.includes(npmCacheUrl) &&
|
||||
/(?:error|failed|ENOTFOUND|ECONN|ETIMEDOUT|EAI_AGAIN|FetchError)/i.test(line)
|
||||
);
|
||||
}
|
||||
|
||||
function isNpmCacheRegistryEnabled(
|
||||
env: Record<string, string | undefined>,
|
||||
npmCacheUrl: string | undefined
|
||||
): npmCacheUrl is string {
|
||||
return env.EAS_USE_NPM_CACHE === '1' && !!npmCacheUrl;
|
||||
}
|
||||
|
||||
function getErrorOutput(err: unknown): string {
|
||||
if (!(err instanceof Error)) {
|
||||
return '';
|
||||
|
||||
@@ -233,7 +233,7 @@ async function uploadProjectMetadataAsync(
|
||||
}
|
||||
`),
|
||||
{
|
||||
buildId: ctx.env.EAS_BUILD_ID,
|
||||
buildId: nullthrows(ctx.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set'),
|
||||
projectMetadataFile: {
|
||||
type: 'GCS',
|
||||
bucketKey: uploadSession.bucketKey,
|
||||
|
||||
@@ -63,7 +63,7 @@ export interface BuildContextOptions {
|
||||
reportError?: (
|
||||
msg: string,
|
||||
err?: Error,
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string | undefined> }
|
||||
) => void;
|
||||
skipNativeBuild?: boolean;
|
||||
metadata?: Metadata;
|
||||
@@ -80,7 +80,7 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
public readonly reportError?: (
|
||||
msg: string,
|
||||
err?: Error,
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string | undefined> }
|
||||
) => void;
|
||||
public readonly skipNativeBuild?: boolean;
|
||||
public readonly expoApiV2BaseUrl?: string;
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function runFastlane(
|
||||
cwd,
|
||||
}: {
|
||||
logger?: bunyan;
|
||||
env?: Record<string, string>;
|
||||
env?: Env;
|
||||
cwd?: string;
|
||||
} = {}
|
||||
): Promise<SpawnResult> {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { BuildStepContext, BuildStepEnv } from '@expo/steps';
|
||||
import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
||||
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import {
|
||||
PackageManager,
|
||||
findPackagerRootDir,
|
||||
resolvePackageManager,
|
||||
shouldUseFrozenLockfile,
|
||||
} from '../../../utils/packageManager';
|
||||
import { readPackageJson } from '../../../utils/project';
|
||||
import { installNodeModules } from '../installNodeModules';
|
||||
|
||||
jest.mock('@expo/turtle-spawn', () => jest.fn());
|
||||
jest.mock('../../../sentry', () => ({
|
||||
Sentry: {
|
||||
capture: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../utils/packageManager', () => ({
|
||||
...jest.requireActual('../../../utils/packageManager'),
|
||||
resolvePackageManager: jest.fn(),
|
||||
findPackagerRootDir: jest.fn(),
|
||||
shouldUseFrozenLockfile: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../utils/project', () => ({
|
||||
...jest.requireActual('../../../utils/project'),
|
||||
readPackageJson: jest.fn(),
|
||||
}));
|
||||
|
||||
describe(installNodeModules, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(resolvePackageManager).mockReturnValue(PackageManager.NPM);
|
||||
jest.mocked(findPackagerRootDir).mockReturnValue('/tmp/build');
|
||||
jest.mocked(shouldUseFrozenLockfile).mockReturnValue(false);
|
||||
jest.mocked(readPackageJson).mockReturnValue({});
|
||||
});
|
||||
|
||||
function createStepCtx(logger: ReturnType<typeof createMockLogger>): BuildStepContext {
|
||||
return {
|
||||
logger,
|
||||
workingDirectory: '/tmp/build',
|
||||
global: {
|
||||
projectTargetDirectory: '/tmp/build',
|
||||
staticContext: { metadata: {} },
|
||||
},
|
||||
} as unknown as BuildStepContext;
|
||||
}
|
||||
|
||||
it('retries the install without the npm cache registry when the cache install fails', async () => {
|
||||
const logger = createMockLogger();
|
||||
const npmCacheUrl = 'http://npm.caches.eas-build.internal';
|
||||
const env: BuildStepEnv = {
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
};
|
||||
const error = Object.assign(new Error('npm install exited with non-zero code: 1'), {
|
||||
status: 1,
|
||||
signal: null,
|
||||
stdout: '',
|
||||
stderr: `npm error request to ${npmCacheUrl}/left-pad failed, reason: getaddrinfo ENOTFOUND`,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(spawn)
|
||||
.mockReturnValueOnce(createSpawnPromise(Promise.reject(error)))
|
||||
.mockReturnValueOnce(createSpawnPromise(Promise.resolve(createSpawnResult())));
|
||||
|
||||
await installNodeModules(createStepCtx(logger), env);
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(2);
|
||||
expect(spawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'npm',
|
||||
['install', '--include=dev'],
|
||||
expect.objectContaining({
|
||||
env: {
|
||||
EAS_USE_NPM_CACHE: '1',
|
||||
EAS_BUILD_NPM_CACHE_URL: npmCacheUrl,
|
||||
NPM_CONFIG_REGISTRY: npmCacheUrl,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(spawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'npm',
|
||||
['install', '--include=dev'],
|
||||
expect.objectContaining({
|
||||
env: { EAS_USE_NPM_CACHE: '1', EAS_BUILD_NPM_CACHE_URL: npmCacheUrl },
|
||||
})
|
||||
);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Failed to install dependencies using the npm cache registry (${npmCacheUrl}). Retrying without the npm cache registry.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not retry when the npm cache is not enabled', async () => {
|
||||
const logger = createMockLogger();
|
||||
const error = Object.assign(new Error('npm install exited with non-zero code: 1'), {
|
||||
status: 1,
|
||||
signal: null,
|
||||
stdout: '',
|
||||
stderr: 'npm error some unrelated failure',
|
||||
});
|
||||
|
||||
jest.mocked(spawn).mockReturnValueOnce(createSpawnPromise(Promise.reject(error)));
|
||||
|
||||
await expect(installNodeModules(createStepCtx(logger), {})).rejects.toThrow(error);
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function createSpawnPromise(result: Promise<SpawnResult>): SpawnPromise<SpawnResult> {
|
||||
return Object.assign(result, {
|
||||
child: {
|
||||
pid: 123,
|
||||
},
|
||||
}) as SpawnPromise<SpawnResult>;
|
||||
}
|
||||
|
||||
function createSpawnResult(): SpawnResult {
|
||||
return {
|
||||
pid: 123,
|
||||
output: ['', ''],
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
status: 0,
|
||||
signal: null,
|
||||
};
|
||||
}
|
||||
@@ -27,7 +27,7 @@ describe(createRepackBuildFunction, () => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
it('should set the output path for successful repack', async () => {
|
||||
it('should set generated output path for successful repack', async () => {
|
||||
const repack = createRepackBuildFunction();
|
||||
const repackStep = repack.createBuildStepFromFunctionCall(
|
||||
createGlobalContextMock({
|
||||
@@ -38,14 +38,33 @@ describe(createRepackBuildFunction, () => {
|
||||
{
|
||||
callInputs: {
|
||||
platform: 'ios',
|
||||
source_app_path: '/path/to/source_app',
|
||||
output_path: '/path/to/output_app',
|
||||
source_app_path: '/path/to/source_app.ipa',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await repackStep.executeAsync();
|
||||
expect(repackStep.outputById['output_path'].value).toBe('/path/to/output_app');
|
||||
expect(repackStep.outputById['output_path'].value).toMatch(/repacked-.*\.ipa$/);
|
||||
});
|
||||
|
||||
it('should rename generated aab output path to apk', async () => {
|
||||
const repack = createRepackBuildFunction();
|
||||
const repackStep = repack.createBuildStepFromFunctionCall(
|
||||
createGlobalContextMock({
|
||||
staticContextContent: {
|
||||
job: createTestAndroidJob(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
callInputs: {
|
||||
platform: 'android',
|
||||
source_app_path: '/path/to/source_app.aab',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await repackStep.executeAsync();
|
||||
expect(repackStep.outputById['output_path'].value).toMatch(/repacked-.*\.apk$/);
|
||||
});
|
||||
|
||||
it('should throw for unsupported platforms', async () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ jest.mock('../../../utils/IosSimulatorUtils', () => ({
|
||||
getAvailableDevicesAsync: jest.fn(),
|
||||
getDeviceAsync: jest.fn(),
|
||||
cloneAsync: jest.fn(),
|
||||
enableAccessibilitySettingsAsync: jest.fn(),
|
||||
startAsync: jest.fn(),
|
||||
waitForReadyAsync: jest.fn(),
|
||||
disableApsdAsync: jest.fn(),
|
||||
@@ -34,15 +35,59 @@ function createStep(callInputs?: Record<string, unknown>) {
|
||||
|
||||
describe(createStartIosSimulatorBuildFunction, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
|
||||
mockedUtils.getAvailableDevicesAsync.mockResolvedValue([]);
|
||||
mockedUtils.getDeviceAsync.mockResolvedValue(null);
|
||||
mockedUtils.cloneAsync.mockResolvedValue(undefined);
|
||||
mockedUtils.enableAccessibilitySettingsAsync.mockResolvedValue(undefined);
|
||||
mockedUtils.startAsync.mockResolvedValue({ udid: 'test-udid' as any });
|
||||
mockedUtils.waitForReadyAsync.mockResolvedValue(undefined);
|
||||
mockedUtils.disableApsdAsync.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('does not enable accessibility settings by default', async () => {
|
||||
await createStep({ device_identifier: 'iPhone 15' }).executeAsync();
|
||||
|
||||
expect(mockedUtils.enableAccessibilitySettingsAsync).not.toHaveBeenCalled();
|
||||
expect(mockedUtils.startAsync).toHaveBeenCalledWith({
|
||||
deviceIdentifier: 'iPhone 15',
|
||||
env: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('enables accessibility settings before starting the main device and every clone when requested', async () => {
|
||||
mockedUtils.startAsync
|
||||
.mockResolvedValueOnce({ udid: 'base' as any })
|
||||
.mockResolvedValueOnce({ udid: 'clone-1' as any })
|
||||
.mockResolvedValueOnce({ udid: 'clone-2' as any });
|
||||
|
||||
await createStep({
|
||||
device_identifier: 'iPhone 15',
|
||||
count: 2,
|
||||
enable_accessibility_settings: true,
|
||||
}).executeAsync();
|
||||
|
||||
expect(mockedUtils.enableAccessibilitySettingsAsync).toHaveBeenCalledWith({
|
||||
deviceIdentifier: 'iPhone 15',
|
||||
env: expect.any(Object),
|
||||
});
|
||||
expect(mockedUtils.enableAccessibilitySettingsAsync).toHaveBeenCalledWith({
|
||||
deviceIdentifier: 'eas-simulator-1',
|
||||
env: expect.any(Object),
|
||||
});
|
||||
expect(mockedUtils.enableAccessibilitySettingsAsync).toHaveBeenCalledWith({
|
||||
deviceIdentifier: 'eas-simulator-2',
|
||||
env: expect.any(Object),
|
||||
});
|
||||
for (const [callIndex] of mockedUtils.startAsync.mock.calls.entries()) {
|
||||
const enableCallOrder =
|
||||
mockedUtils.enableAccessibilitySettingsAsync.mock.invocationCallOrder[callIndex];
|
||||
const startCallOrder = mockedUtils.startAsync.mock.invocationCallOrder[callIndex];
|
||||
expect(enableCallOrder).toBeLessThan(startCallOrder);
|
||||
}
|
||||
});
|
||||
|
||||
it('disables apsd on the main device and every clone', async () => {
|
||||
mockedUtils.startAsync
|
||||
.mockResolvedValueOnce({ udid: 'base' as any })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asyncResult } from '@expo/results';
|
||||
import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps';
|
||||
|
||||
import { Sentry } from '../../sentry';
|
||||
import { retryOnDNSFailure } from '../../utils/retryOnDNSFailure';
|
||||
|
||||
export function createSubmissionEntityFunction(): BuildFunction {
|
||||
@@ -54,18 +55,42 @@ export function createSubmissionEntityFunction(): BuildFunction {
|
||||
const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
|
||||
if (!robotAccessToken) {
|
||||
stepsCtx.logger.error('Failed to create submission entity: no robot access token found');
|
||||
Sentry.capture('Failed to create submission entity: missing robot access token');
|
||||
return;
|
||||
}
|
||||
|
||||
const buildId = inputs.build_id.value;
|
||||
if (!buildId) {
|
||||
stepsCtx.logger.error('Failed to create submission entity: no build ID provided');
|
||||
Sentry.capture('Failed to create submission entity: missing build ID', {
|
||||
extras: {
|
||||
buildId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowJobId = stepsCtx.global.env.__WORKFLOW_JOB_ID;
|
||||
if (!workflowJobId) {
|
||||
stepsCtx.logger.error('Failed to create submission entity: no workflow job ID found');
|
||||
Sentry.capture('Failed to create submission entity: missing workflow job ID', {
|
||||
extras: {
|
||||
buildId,
|
||||
workflowJobId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const expoApiServerURL = stepsCtx.global.staticContext.expoApiServerURL;
|
||||
if (!expoApiServerURL) {
|
||||
stepsCtx.logger.error('Failed to create submission entity: no Expo API server URL found');
|
||||
Sentry.capture('Failed to create submission entity: missing Expo API server URL', {
|
||||
extras: {
|
||||
buildId,
|
||||
workflowJobId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,7 +108,7 @@ export function createSubmissionEntityFunction(): BuildFunction {
|
||||
|
||||
try {
|
||||
const response = await retryOnDNSFailure(fetch)(
|
||||
new URL('/v2/app-store-submissions/', stepsCtx.global.staticContext.expoApiServerURL),
|
||||
new URL('/v2/app-store-submissions/', expoApiServerURL),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserError } from '@expo/eas-build-job';
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
@@ -56,18 +56,23 @@ export function createDownloadArtifactFunction(): BuildFunction {
|
||||
const interpolationContext = stepsCtx.global.getInterpolationContext();
|
||||
|
||||
if (!('workflow' in interpolationContext)) {
|
||||
throw new UserError(
|
||||
'EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW',
|
||||
'No workflow found in the interpolation context.'
|
||||
);
|
||||
throw new SystemError('No workflow found in the interpolation context.', {
|
||||
trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW',
|
||||
});
|
||||
}
|
||||
|
||||
const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
|
||||
if (!robotAccessToken) {
|
||||
throw new UserError(
|
||||
'EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN',
|
||||
'No robot access token found in the job secrets.'
|
||||
);
|
||||
throw new SystemError('No robot access token found in the job secrets.', {
|
||||
trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN',
|
||||
});
|
||||
}
|
||||
|
||||
const expoApiServerURL = stepsCtx.global.staticContext.expoApiServerURL;
|
||||
if (!expoApiServerURL) {
|
||||
throw new SystemError('Missing Expo API server URL.', {
|
||||
trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_EXPO_API_SERVER_URL',
|
||||
});
|
||||
}
|
||||
|
||||
const workflowRunId = interpolationContext.workflow.id;
|
||||
@@ -82,7 +87,7 @@ export function createDownloadArtifactFunction(): BuildFunction {
|
||||
const { artifactPath } = await downloadArtifactAsync({
|
||||
logger,
|
||||
workflowRunId,
|
||||
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
|
||||
expoApiServerURL,
|
||||
robotAccessToken,
|
||||
params,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BuildFunction, BuildStepContext, BuildStepEnv } from '@expo/steps';
|
||||
import path from 'path';
|
||||
|
||||
import { installDependenciesAsync } from '../../common/installDependencies';
|
||||
import { installDependenciesWithNpmCacheFallbackAsync } from '../../common/installDependencies';
|
||||
import {
|
||||
findPackagerRootDir,
|
||||
getPackageVersionFromPackageJson,
|
||||
@@ -63,7 +63,7 @@ export async function installNodeModules(
|
||||
packageName: 'react-native',
|
||||
});
|
||||
|
||||
const { spawnPromise } = await installDependenciesAsync({
|
||||
await installDependenciesWithNpmCacheFallbackAsync({
|
||||
packageManager,
|
||||
env,
|
||||
logger: stepCtx.logger,
|
||||
@@ -74,5 +74,4 @@ export async function installNodeModules(
|
||||
reactNativeVersion,
|
||||
}),
|
||||
});
|
||||
await spawnPromise;
|
||||
}
|
||||
@@ -42,11 +42,6 @@ export function createRepackBuildFunction(): BuildFunction {
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'output_path',
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'embed_bundle_assets',
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
|
||||
@@ -109,9 +104,10 @@ export function createRepackBuildFunction(): BuildFunction {
|
||||
stepsCtx.logger.info(`Created temporary working directory: ${workingDirectory}`);
|
||||
|
||||
const sourceAppPath = inputs.source_app_path.value as string;
|
||||
const outputPath =
|
||||
(inputs.output_path.value as string) ??
|
||||
path.join(tmpDir, `repacked-${randomUUID()}${path.extname(sourceAppPath)}`);
|
||||
const outputPath = createOutputPath({
|
||||
sourceAppPath,
|
||||
tmpDir,
|
||||
});
|
||||
const exportEmbedOptions = inputs.embed_bundle_assets.value
|
||||
? {
|
||||
sourcemapOutput: undefined,
|
||||
@@ -202,6 +198,21 @@ export function createRepackBuildFunction(): BuildFunction {
|
||||
});
|
||||
}
|
||||
|
||||
function createOutputPath({
|
||||
sourceAppPath,
|
||||
tmpDir,
|
||||
}: {
|
||||
sourceAppPath: string;
|
||||
tmpDir: string;
|
||||
}): string {
|
||||
const outputPath = path.join(tmpDir, `repacked-${randomUUID()}${path.extname(sourceAppPath)}`);
|
||||
const extension = path.extname(outputPath);
|
||||
if (extension.toLowerCase() !== '.aab') {
|
||||
return outputPath;
|
||||
}
|
||||
return `${outputPath.slice(0, -extension.length)}.apk`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install `@expo/repack-app` in a sandbox directory and import it.
|
||||
*/
|
||||
|
||||
@@ -69,6 +69,10 @@ export function createRestoreCacheFunction(): BuildFunction {
|
||||
.filter(key => key !== '');
|
||||
|
||||
const jobId = nullthrows(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
||||
const expoApiServerURL = nullthrows(
|
||||
stepsCtx.global.staticContext.expoApiServerURL,
|
||||
'expoApiServerURL is not set'
|
||||
);
|
||||
const robotAccessToken = nullthrows(
|
||||
stepsCtx.global.staticContext.job.secrets?.robotAccessToken,
|
||||
'robotAccessToken is not set'
|
||||
@@ -77,7 +81,7 @@ export function createRestoreCacheFunction(): BuildFunction {
|
||||
const { archivePath, matchedKey } = await downloadCacheAsync({
|
||||
logger,
|
||||
jobId,
|
||||
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
|
||||
expoApiServerURL,
|
||||
robotAccessToken,
|
||||
paths,
|
||||
key,
|
||||
|
||||
@@ -43,6 +43,10 @@ export function createSaveCacheFunction(): BuildFunction {
|
||||
.filter(path => path.length > 0);
|
||||
const key = z.string().parse(inputs.key.value);
|
||||
const jobId = nullthrows(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
||||
const expoApiServerURL = nullthrows(
|
||||
stepsCtx.global.staticContext.expoApiServerURL,
|
||||
'expoApiServerURL is not set'
|
||||
);
|
||||
const robotAccessToken = nullthrows(
|
||||
stepsCtx.global.staticContext.job.secrets?.robotAccessToken,
|
||||
'robotAccessToken is not set'
|
||||
@@ -61,7 +65,7 @@ export function createSaveCacheFunction(): BuildFunction {
|
||||
await uploadPublicCacheAsync({
|
||||
logger,
|
||||
jobId,
|
||||
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
|
||||
expoApiServerURL,
|
||||
robotAccessToken,
|
||||
archivePath,
|
||||
key,
|
||||
@@ -73,7 +77,7 @@ export function createSaveCacheFunction(): BuildFunction {
|
||||
await uploadCacheAsync({
|
||||
logger,
|
||||
jobId,
|
||||
expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
|
||||
expoApiServerURL,
|
||||
robotAccessToken,
|
||||
archivePath,
|
||||
key,
|
||||
|
||||
@@ -31,6 +31,12 @@ export function createStartIosSimulatorBuildFunction(): BuildFunction {
|
||||
defaultValue: 1,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.NUMBER,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'enable_accessibility_settings',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
|
||||
}),
|
||||
],
|
||||
fn: async ({ logger }, { inputs, env }) => {
|
||||
try {
|
||||
@@ -55,11 +61,18 @@ export function createStartIosSimulatorBuildFunction(): BuildFunction {
|
||||
| undefined;
|
||||
const originalDeviceIdentifier =
|
||||
deviceIdentifierInput ?? (await findMostGenericIphoneUuidAsync({ env }));
|
||||
const enableAccessibilitySettings = Boolean(inputs.enable_accessibility_settings.value);
|
||||
|
||||
if (!originalDeviceIdentifier) {
|
||||
throw new Error('Could not find an iPhone among available simulator devices.');
|
||||
}
|
||||
|
||||
if (enableAccessibilitySettings) {
|
||||
await IosSimulatorUtils.enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier: originalDeviceIdentifier,
|
||||
env,
|
||||
});
|
||||
}
|
||||
const { udid } = await IosSimulatorUtils.startAsync({
|
||||
deviceIdentifier: originalDeviceIdentifier,
|
||||
env,
|
||||
@@ -97,6 +110,12 @@ export function createStartIosSimulatorBuildFunction(): BuildFunction {
|
||||
env,
|
||||
});
|
||||
|
||||
if (enableAccessibilitySettings) {
|
||||
await IosSimulatorUtils.enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier: cloneDeviceName,
|
||||
env,
|
||||
});
|
||||
}
|
||||
const { udid: cloneUdid } = await IosSimulatorUtils.startAsync({
|
||||
deviceIdentifier: cloneDeviceName,
|
||||
env,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExpoError, SystemError, UserError } from '@expo/eas-build-job';
|
||||
import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
@@ -92,6 +93,67 @@ export namespace IosSimulatorUtils {
|
||||
});
|
||||
}
|
||||
|
||||
export async function enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier,
|
||||
env,
|
||||
}: {
|
||||
deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const devices = await getAvailableDevicesAsync({ env, filter: 'available' });
|
||||
const device = devices.find(
|
||||
device =>
|
||||
device.isAvailable &&
|
||||
(device.udid === deviceIdentifier || device.name === deviceIdentifier)
|
||||
);
|
||||
if (!device) {
|
||||
throw new UserError(
|
||||
'EAS_IOS_SIMULATOR_NOT_FOUND',
|
||||
`Failed to find available iOS Simulator "${deviceIdentifier}" to update accessibility settings.`
|
||||
);
|
||||
}
|
||||
if (device.state !== 'Shutdown') {
|
||||
throw new UserError(
|
||||
'EAS_IOS_SIMULATOR_NOT_SHUTDOWN',
|
||||
`Expected iOS Simulator "${deviceIdentifier}" to be shutdown before updating accessibility settings, but it is ${device.state}.`
|
||||
);
|
||||
}
|
||||
|
||||
const plistPath = path.join(
|
||||
device.dataPath,
|
||||
'Library',
|
||||
'Preferences',
|
||||
'com.apple.Accessibility.plist'
|
||||
);
|
||||
await fs.promises.mkdir(path.dirname(plistPath), { recursive: true });
|
||||
|
||||
const plistExists = await fs.promises
|
||||
.access(plistPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (!plistExists) {
|
||||
await spawn('plutil', ['-create', 'binary1', plistPath], { env });
|
||||
}
|
||||
|
||||
for (const key of [
|
||||
'AutomationEnabled',
|
||||
'IgnoreAXServerEntitlements',
|
||||
'AccessibilityEnabled',
|
||||
'ApplicationAccessibilityEnabled',
|
||||
]) {
|
||||
await spawn('plutil', ['-replace', key, '-bool', 'true', plistPath], { env });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ExpoError) {
|
||||
throw err;
|
||||
}
|
||||
throw new SystemError('Failed to update iOS Simulator accessibility settings.', {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function startAsync({
|
||||
deviceIdentifier,
|
||||
env,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SystemError, UserError } from '@expo/eas-build-job';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -18,9 +19,143 @@ const mockedSpawn = jest.mocked(spawn);
|
||||
|
||||
describe('IosSimulatorUtils', () => {
|
||||
beforeEach(() => {
|
||||
mockedSpawn.mockReset();
|
||||
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
|
||||
});
|
||||
|
||||
describe(IosSimulatorUtils.startAsync, () => {
|
||||
it('waits for boot completion without writing accessibility prefs', async () => {
|
||||
mockedSpawn.mockImplementation((async (command: string, args: string[]) => {
|
||||
if (command === 'xcrun' && args.join(' ') === 'simctl bootstatus test-udid -b') {
|
||||
return {
|
||||
stdout: 'Monitoring boot status for iPhone 15 (test-udid).\n',
|
||||
stderr: '',
|
||||
} as any;
|
||||
}
|
||||
return { stdout: '', stderr: '' } as any;
|
||||
}) as any);
|
||||
|
||||
await IosSimulatorUtils.startAsync({
|
||||
deviceIdentifier: 'test-udid' as any,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(mockedSpawn).not.toHaveBeenCalledWith(
|
||||
'xcrun',
|
||||
['simctl', 'list', 'devices', '--json', '--no-escape-slashes', 'test-udid'],
|
||||
expect.anything()
|
||||
);
|
||||
expect(mockedSpawn).not.toHaveBeenCalledWith('plutil', expect.anything(), expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe(IosSimulatorUtils.enableAccessibilitySettingsAsync, () => {
|
||||
function device(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
dataPath: '/tmp/eas-test-simulator-data',
|
||||
dataPathSize: 1,
|
||||
logPath: '/tmp/eas-test-simulator-logs',
|
||||
udid: 'test-udid',
|
||||
isAvailable: true,
|
||||
deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15',
|
||||
state: 'Shutdown',
|
||||
name: 'iPhone 15',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function mockAvailableDevices(devices: Record<string, unknown>[]) {
|
||||
mockedSpawn.mockImplementation((async (command: string, args: string[]) => {
|
||||
if (
|
||||
command === 'xcrun' &&
|
||||
args.join(' ') === 'simctl list devices --json --no-escape-slashes available'
|
||||
) {
|
||||
return {
|
||||
stdout: JSON.stringify({
|
||||
devices: {
|
||||
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': devices,
|
||||
},
|
||||
}),
|
||||
stderr: '',
|
||||
} as any;
|
||||
}
|
||||
return { stdout: '', stderr: '' } as any;
|
||||
}) as any);
|
||||
}
|
||||
|
||||
it('writes accessibility prefs for an exact shutdown simulator match', async () => {
|
||||
mockAvailableDevices([
|
||||
device({
|
||||
udid: 'test-udid-pro',
|
||||
name: 'iPhone 17 Pro',
|
||||
dataPath: '/tmp/eas-test-simulator-data-pro',
|
||||
}),
|
||||
device({ name: 'iPhone 17' }),
|
||||
]);
|
||||
|
||||
await expect(
|
||||
IosSimulatorUtils.enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier: 'iPhone 17' as any,
|
||||
env: process.env,
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
for (const key of [
|
||||
'AutomationEnabled',
|
||||
'IgnoreAXServerEntitlements',
|
||||
'AccessibilityEnabled',
|
||||
'ApplicationAccessibilityEnabled',
|
||||
]) {
|
||||
expect(mockedSpawn).toHaveBeenCalledWith(
|
||||
'plutil',
|
||||
[
|
||||
'-replace',
|
||||
key,
|
||||
'-bool',
|
||||
'true',
|
||||
'/tmp/eas-test-simulator-data/Library/Preferences/com.apple.Accessibility.plist',
|
||||
],
|
||||
{ env: process.env }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws UserError when no available simulator exactly matches the identifier', async () => {
|
||||
mockAvailableDevices([device({ name: 'iPhone 17 Pro' })]);
|
||||
|
||||
await expect(
|
||||
IosSimulatorUtils.enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier: 'iPhone 17' as any,
|
||||
env: process.env,
|
||||
})
|
||||
).rejects.toThrow(UserError);
|
||||
|
||||
expect(mockedSpawn).not.toHaveBeenCalledWith('plutil', expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
it('throws UserError when the matched simulator is already booted', async () => {
|
||||
mockAvailableDevices([device({ state: 'Booted' })]);
|
||||
|
||||
await expect(
|
||||
IosSimulatorUtils.enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier: 'test-udid' as any,
|
||||
env: process.env,
|
||||
})
|
||||
).rejects.toThrow(UserError);
|
||||
});
|
||||
|
||||
it('throws SystemError when pre-boot accessibility setup fails', async () => {
|
||||
mockedSpawn.mockRejectedValue(new Error('Failed to list simulators'));
|
||||
|
||||
await expect(
|
||||
IosSimulatorUtils.enableAccessibilitySettingsAsync({
|
||||
deviceIdentifier: 'test-udid' as any,
|
||||
env: process.env,
|
||||
})
|
||||
).rejects.toThrow(SystemError);
|
||||
});
|
||||
});
|
||||
|
||||
describe(IosSimulatorUtils.waitForReadyAsync, () => {
|
||||
it('takes the readiness screenshot into a writable temp file, not /dev/null', async () => {
|
||||
await IosSimulatorUtils.waitForReadyAsync({
|
||||
|
||||
@@ -104,4 +104,33 @@ describe(expoUpdates.configureExpoUpdatesIfInstalledAsync, () => {
|
||||
expect(iosSetChannelNativelyAsync).toBeCalledTimes(1);
|
||||
expect(getExpoUpdatesPackageVersionIfInstalledAsync).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not fail on missing EAS_BUILD_ID when logging fingerprint diffs', async () => {
|
||||
jest.mocked(getExpoUpdatesPackageVersionIfInstalledAsync).mockResolvedValue('0.18.0');
|
||||
|
||||
const warn = jest.fn();
|
||||
const managedCtx: BuildContext<BuildJob> = {
|
||||
appConfig: {},
|
||||
env: {},
|
||||
job: {
|
||||
platform: Platform.IOS,
|
||||
},
|
||||
logger: { warn },
|
||||
metadata: {
|
||||
runtimeVersion: 'runtime-version-from-local-machine',
|
||||
},
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx, {
|
||||
resolvedRuntimeVersion: 'runtime-version-from-eas-build',
|
||||
resolvedFingerprintSources: [],
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Runtime version calculated on local machine not equal to runtime version calculated during build.'
|
||||
);
|
||||
|
||||
expect(warn).toHaveBeenCalledWith('Skipping fingerprint diff because EAS_BUILD_ID is not set');
|
||||
});
|
||||
});
|
||||
@@ -243,6 +243,12 @@ async function logDiffFingerprints({
|
||||
ctx: BuildContext<BuildJob>;
|
||||
}): Promise<void> {
|
||||
const { resolvedRuntimeVersion, resolvedFingerprintSources } = resolvedRuntime;
|
||||
const buildId = ctx.env.EAS_BUILD_ID;
|
||||
|
||||
if (!buildId) {
|
||||
ctx.logger.warn('Skipping fingerprint diff because EAS_BUILD_ID is not set');
|
||||
return;
|
||||
}
|
||||
|
||||
const fingerprintInfo = await ctx.graphqlClient
|
||||
.query(
|
||||
@@ -257,7 +263,7 @@ async function logDiffFingerprints({
|
||||
}
|
||||
}
|
||||
`),
|
||||
{ id: ctx.env.EAS_BUILD_ID }
|
||||
{ id: buildId }
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { StaticWorkflowInterpolationContextZ } from '../common';
|
||||
import { EnvSchema, StaticWorkflowInterpolationContextZ } from '../common';
|
||||
|
||||
describe('EnvSchema', () => {
|
||||
it('accepts explicit undefined values', () => {
|
||||
const env = {
|
||||
DEFINED_ENV: 'value',
|
||||
UNDEFINED_ENV: undefined,
|
||||
};
|
||||
|
||||
const { value, error } = EnvSchema.validate(env);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(value).toEqual(env);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StaticWorkflowInterpolationContextZ', () => {
|
||||
it('accepts app and account context', () => {
|
||||
|
||||
@@ -115,8 +115,8 @@ export const ArchiveSourceSchemaZ = z.discriminatedUnion('type', [
|
||||
}),
|
||||
]);
|
||||
|
||||
export type Env = Record<string, string>;
|
||||
export const EnvSchema = Joi.object().pattern(Joi.string(), Joi.string());
|
||||
export type Env = Record<string, string | undefined>;
|
||||
export const EnvSchema = Joi.object().pattern(Joi.string(), Joi.string().optional());
|
||||
|
||||
export type EnvironmentSecret = {
|
||||
name: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ type StaticJobOnlyInterpolationContext = {
|
||||
outputs: Record<string, string | undefined>;
|
||||
}
|
||||
>;
|
||||
expoApiServerURL: string;
|
||||
expoApiServerURL: string | undefined;
|
||||
};
|
||||
|
||||
export type StaticJobInterpolationContext =
|
||||
|
||||
+136
-113
@@ -141,6 +141,7 @@ eas --help COMMAND
|
||||
* [`eas observe:metrics [METRIC]`](#eas-observemetrics-metric)
|
||||
* [`eas observe:metrics-summary`](#eas-observemetrics-summary)
|
||||
* [`eas observe:routes`](#eas-observeroutes)
|
||||
* [`eas observe:session SESSIONID`](#eas-observesession-sessionid)
|
||||
* [`eas observe:versions`](#eas-observeversions)
|
||||
* [`eas onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-onboarding-target_project_directory)
|
||||
* [`eas open`](#eas-open)
|
||||
@@ -206,7 +207,7 @@ DESCRIPTION
|
||||
view the audit logs for an account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/audit.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/account/audit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/audit.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/account/audit.ts)_
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
@@ -227,7 +228,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -244,7 +245,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -266,7 +267,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -283,7 +284,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -297,7 +298,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -349,7 +350,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -370,7 +371,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -390,7 +391,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -410,7 +411,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -433,7 +434,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -474,7 +475,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -494,7 +495,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -512,7 +513,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -532,7 +533,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -557,7 +558,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -584,7 +585,7 @@ DESCRIPTION
|
||||
download a simulator/emulator build by build ID or fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -622,7 +623,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -662,7 +663,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -692,7 +693,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -720,7 +721,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -772,7 +773,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -790,7 +791,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -808,7 +809,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -825,7 +826,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -846,7 +847,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -867,7 +868,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -889,7 +890,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:insights`
|
||||
|
||||
@@ -913,7 +914,7 @@ DESCRIPTION
|
||||
display adoption, crash, and unique-user insights for a channel + runtime version
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -933,7 +934,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -955,7 +956,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -977,7 +978,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -1012,7 +1013,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -1035,7 +1036,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -1056,7 +1057,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -1073,7 +1074,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1091,7 +1092,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1120,7 +1121,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1145,7 +1146,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1166,7 +1167,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1187,7 +1188,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1224,7 +1225,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1244,7 +1245,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1265,7 +1266,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1286,7 +1287,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1300,7 +1301,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1314,7 +1315,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1346,7 +1347,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1372,7 +1373,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1394,7 +1395,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1422,7 +1423,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1450,7 +1451,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1473,7 +1474,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1495,7 +1496,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1529,7 +1530,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1573,7 +1574,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1604,7 +1605,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1683,7 +1684,7 @@ DESCRIPTION
|
||||
connect a project to an App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
|
||||
## `eas integrations:asc:disconnect`
|
||||
|
||||
@@ -1702,7 +1703,7 @@ DESCRIPTION
|
||||
disconnect the current project from its App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
|
||||
## `eas integrations:asc:status`
|
||||
|
||||
@@ -1720,7 +1721,7 @@ DESCRIPTION
|
||||
show the App Store Connect app link status for the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
|
||||
## `eas integrations:convex:connect`
|
||||
|
||||
@@ -1742,7 +1743,7 @@ DESCRIPTION
|
||||
connect Convex to your Expo project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
|
||||
|
||||
## `eas integrations:convex:dashboard`
|
||||
|
||||
@@ -1756,7 +1757,7 @@ DESCRIPTION
|
||||
open the Convex dashboard for the linked Convex project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
|
||||
|
||||
## `eas integrations:convex:project`
|
||||
|
||||
@@ -1770,7 +1771,7 @@ DESCRIPTION
|
||||
display the Convex project linked to the current Expo app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/project.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/project.ts)_
|
||||
|
||||
## `eas integrations:convex:project:delete`
|
||||
|
||||
@@ -1788,7 +1789,7 @@ DESCRIPTION
|
||||
remove the Convex project link for the current Expo app from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
|
||||
|
||||
## `eas integrations:convex:team`
|
||||
|
||||
@@ -1802,7 +1803,7 @@ DESCRIPTION
|
||||
display Convex teams linked to the current Expo app's owner account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/team.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/team.ts)_
|
||||
|
||||
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
|
||||
|
||||
@@ -1823,7 +1824,7 @@ DESCRIPTION
|
||||
remove a Convex team link from the current Expo app owner account's EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
|
||||
|
||||
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
|
||||
|
||||
@@ -1843,7 +1844,7 @@ DESCRIPTION
|
||||
send a Convex team invitation to your verified email address
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
|
||||
|
||||
## `eas integrations:posthog:connect`
|
||||
|
||||
@@ -1870,7 +1871,7 @@ DESCRIPTION
|
||||
connect PostHog to your Expo project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/connect.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/posthog/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/connect.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/posthog/connect.ts)_
|
||||
|
||||
## `eas integrations:posthog:dashboard`
|
||||
|
||||
@@ -1888,7 +1889,7 @@ DESCRIPTION
|
||||
open the PostHog dashboard for the linked PostHog project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/dashboard.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/posthog/dashboard.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/dashboard.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/posthog/dashboard.ts)_
|
||||
|
||||
## `eas integrations:posthog:disconnect`
|
||||
|
||||
@@ -1907,7 +1908,7 @@ DESCRIPTION
|
||||
remove the PostHog project link for the current Expo app from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/disconnect.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/integrations/posthog/disconnect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/disconnect.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/integrations/posthog/disconnect.ts)_
|
||||
|
||||
## `eas login`
|
||||
|
||||
@@ -1959,7 +1960,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1977,7 +1978,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1995,7 +1996,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -2054,7 +2055,7 @@ DESCRIPTION
|
||||
arguments, a list of the available event names and associated event counts is returned.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/events.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/observe/events.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/events.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/observe/events.ts)_
|
||||
|
||||
## `eas observe:metrics [METRIC]`
|
||||
|
||||
@@ -2089,7 +2090,7 @@ DESCRIPTION
|
||||
display individual performance metric samples ordered by value
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/observe/metrics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/observe/metrics.ts)_
|
||||
|
||||
## `eas observe:metrics-summary`
|
||||
|
||||
@@ -2120,7 +2121,7 @@ DESCRIPTION
|
||||
display aggregated performance metric statistics grouped by app version
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics-summary.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/observe/metrics-summary.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics-summary.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/observe/metrics-summary.ts)_
|
||||
|
||||
## `eas observe:routes`
|
||||
|
||||
@@ -2157,7 +2158,29 @@ DESCRIPTION
|
||||
display app navigation route metrics (Cold TTR, Warm TTR, TTI) grouped by route name
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/routes.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/observe/routes.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/routes.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/observe/routes.ts)_
|
||||
|
||||
## `eas observe:session SESSIONID`
|
||||
|
||||
display the timeline of metric and log events for a specific session
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas observe:session SESSIONID [--project-id <value>] [--json] [--non-interactive]
|
||||
|
||||
ARGUMENTS
|
||||
SESSIONID Session ID to inspect
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--project-id=<value> EAS project ID (defaults to the project ID of the current directory)
|
||||
|
||||
DESCRIPTION
|
||||
display the timeline of metric and log events for a specific session
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/session.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/observe/session.ts)_
|
||||
|
||||
## `eas observe:versions`
|
||||
|
||||
@@ -2182,7 +2205,7 @@ DESCRIPTION
|
||||
display app versions with build and update details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/observe/versions.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/observe/versions.ts)_
|
||||
|
||||
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -2212,7 +2235,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -2226,7 +2249,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -2249,7 +2272,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -2273,7 +2296,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -2291,7 +2314,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -2324,7 +2347,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -2367,7 +2390,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -2388,7 +2411,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -2409,7 +2432,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -2434,7 +2457,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:embedded:delete ID`
|
||||
|
||||
@@ -2455,7 +2478,7 @@ DESCRIPTION
|
||||
delete an embedded update registered with EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/embedded/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/embedded/delete.ts)_
|
||||
|
||||
## `eas update:embedded:list`
|
||||
|
||||
@@ -2481,7 +2504,7 @@ DESCRIPTION
|
||||
list embedded updates registered with EAS Update for this project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/embedded/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/embedded/list.ts)_
|
||||
|
||||
## `eas update:embedded:upload`
|
||||
|
||||
@@ -2511,7 +2534,7 @@ EXAMPLES
|
||||
$ eas update:embedded:upload --platform android --bundle android/app/src/main/assets/index.android.bundle --manifest android/app/src/main/assets/app.manifest --channel production --build-id <BUILD-ID>
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/upload.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/embedded/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/upload.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/embedded/upload.ts)_
|
||||
|
||||
## `eas update:embedded:view ID`
|
||||
|
||||
@@ -2531,7 +2554,7 @@ DESCRIPTION
|
||||
view details of an embedded update registered with EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/embedded/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/embedded/view.ts)_
|
||||
|
||||
## `eas update:insights GROUPID`
|
||||
|
||||
@@ -2558,7 +2581,7 @@ DESCRIPTION
|
||||
display launch, crash, unique-user, and size insights for an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -2585,7 +2608,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -2623,7 +2646,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2651,7 +2674,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2681,7 +2704,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback [GROUPID]`
|
||||
|
||||
@@ -2713,7 +2736,7 @@ DESCRIPTION
|
||||
roll back to an embedded update or an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2737,7 +2760,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2758,7 +2781,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2780,7 +2803,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2800,7 +2823,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2819,7 +2842,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2842,7 +2865,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2859,7 +2882,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2980,7 +3003,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -3000,7 +3023,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -3024,7 +3047,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -3064,7 +3087,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -3086,7 +3109,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -3114,7 +3137,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -3134,7 +3157,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -3156,5 +3179,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v20.3.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v20.5.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+2413
-1
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "20.3.0",
|
||||
"version": "20.5.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -44,7 +44,7 @@
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.19",
|
||||
"@expo/apple-utils": "2.1.22",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
import { LoggerLevel } from '@expo/logger';
|
||||
import { NodePackageManager } from '@expo/package-manager';
|
||||
@@ -65,5 +65,5 @@ export interface BuildContext<T extends Platform> {
|
||||
loggerLevel?: LoggerLevel;
|
||||
isVerboseLoggingEnabled: boolean;
|
||||
whatToTest?: string;
|
||||
env: Record<string, string>;
|
||||
env: Env;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Env, Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import { LoggerLevel } from '@expo/logger';
|
||||
@@ -69,7 +69,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
refreshAdHocProvisioningProfile?: boolean;
|
||||
isVerboseLoggingEnabled: boolean;
|
||||
whatToTest?: string;
|
||||
env: Record<string, string>;
|
||||
env: Env;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
|
||||
env,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Job, Metadata, version } from '@expo/eas-build-job';
|
||||
import { Env, Job, Metadata, version } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
@@ -42,7 +42,7 @@ export async function runLocalBuildAsync(
|
||||
job: Job,
|
||||
metadata: Metadata,
|
||||
options: LocalBuildOptions,
|
||||
env: Record<string, string>
|
||||
env: Env
|
||||
): Promise<void> {
|
||||
const { command, args } = await getCommandAndArgsAsync(job, metadata);
|
||||
let spinner;
|
||||
|
||||
@@ -64,6 +64,19 @@ export function getWorkflowRunUrl(
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getDeviceRunSessionUrl(
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
deviceRunSessionId: string
|
||||
): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/simulator-sessions/${encodeURIComponent(deviceRunSessionId)}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Links to the raw job-run page; prefer a higher-level URL (e.g. the workflow run
|
||||
* or the feature-specific dashboard) that gives users more context. Use this only for internal
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import openBrowserAsync from 'better-opn';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
@@ -17,6 +18,7 @@ import { PostHogQuery } from '../../../../graphql/queries/PostHogQuery';
|
||||
import {
|
||||
PostHogOrganizationConnectionData,
|
||||
PostHogProjectData,
|
||||
StartPostHogConnectionResult,
|
||||
} from '../../../../graphql/types/PostHogConnection';
|
||||
import Log from '../../../../log';
|
||||
import { createOrModifyExpoConfigAsync } from '../../../../project/expoConfig';
|
||||
@@ -27,6 +29,7 @@ import { printJsonOnlyOutput } from '../../../../utils/json';
|
||||
import IntegrationsPostHogConnect from '../connect';
|
||||
|
||||
jest.mock('@expo/spawn-async');
|
||||
jest.mock('better-opn');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../../project/expoConfig');
|
||||
jest.mock('../../../../graphql/queries/PostHogQuery');
|
||||
@@ -42,6 +45,7 @@ jest.mock('../../../../ora', () => ({
|
||||
start: jest.fn().mockReturnThis(),
|
||||
succeed: jest.fn().mockReturnThis(),
|
||||
fail: jest.fn().mockReturnThis(),
|
||||
stop: jest.fn().mockReturnThis(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -79,6 +83,16 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const mockConnectionResult: StartPostHogConnectionResult = {
|
||||
__typename: 'PostHogOrganizationConnection',
|
||||
...mockConnection,
|
||||
};
|
||||
|
||||
const mockPendingResult: StartPostHogConnectionResult = {
|
||||
__typename: 'PostHogPendingConnection',
|
||||
url: 'https://us.posthog.com/login?next=/oauth/authorize',
|
||||
};
|
||||
|
||||
const mockProject: PostHogProjectData = {
|
||||
id: 'project-1',
|
||||
posthogProjectIdentifier: '467657',
|
||||
@@ -137,8 +151,11 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValue(mockConnection);
|
||||
jest.mocked(PostHogQuery.getPostHogProjectByAppIdAsync).mockResolvedValue(mockProject);
|
||||
jest.mocked(PostHogMutation.createPostHogAccountRequestAsync).mockResolvedValue(mockConnection);
|
||||
jest
|
||||
.mocked(PostHogMutation.startPostHogConnectionAsync)
|
||||
.mockResolvedValue(mockConnectionResult);
|
||||
jest.mocked(PostHogMutation.setupPostHogProjectAsync).mockResolvedValue(mockProject);
|
||||
jest.mocked(openBrowserAsync).mockResolvedValue(true as never);
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([]);
|
||||
jest.mocked(EnvironmentVariableMutation.createForAppAsync).mockResolvedValue({
|
||||
id: 'env-var-1',
|
||||
@@ -187,7 +204,7 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
jest.mocked(promptAsync).mockImplementation((async (opts: any) => {
|
||||
if (opts.name === 'features') {
|
||||
provisionedBeforePrompt =
|
||||
jest.mocked(PostHogMutation.createPostHogAccountRequestAsync).mock.calls.length > 0 &&
|
||||
jest.mocked(PostHogMutation.startPostHogConnectionAsync).mock.calls.length > 0 &&
|
||||
jest.mocked(PostHogMutation.setupPostHogProjectAsync).mock.calls.length > 0;
|
||||
return { features: ['analytics'] };
|
||||
}
|
||||
@@ -261,7 +278,7 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
createCommand(['--non-interactive', '--error-tracking']).runAsync()
|
||||
).rejects.toThrow(/personal API key in non-interactive/);
|
||||
|
||||
expect(PostHogMutation.createPostHogAccountRequestAsync).not.toHaveBeenCalled();
|
||||
expect(PostHogMutation.startPostHogConnectionAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('non-interactive --error-tracking with --posthog-cli-api-key sets up CLI vars without prompting', async () => {
|
||||
@@ -321,31 +338,121 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
it('reuses an existing connection + project without provisioning', async () => {
|
||||
await createCommand([]).runAsync();
|
||||
|
||||
expect(PostHogMutation.createPostHogAccountRequestAsync).not.toHaveBeenCalled();
|
||||
expect(PostHogMutation.startPostHogConnectionAsync).not.toHaveBeenCalled();
|
||||
expect(PostHogMutation.setupPostHogProjectAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces the existing-account dead-end without provisioning a project', async () => {
|
||||
it('completes an existing-account connection via the browser, then provisions the project', async () => {
|
||||
// No connection at the first check; the start mutation hands off to the browser, and
|
||||
// polling then finds the connection the website callback created.
|
||||
jest
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValue(mockConnection);
|
||||
jest.mocked(PostHogQuery.getPostHogProjectByAppIdAsync).mockResolvedValue(null);
|
||||
jest.mocked(PostHogMutation.startPostHogConnectionAsync).mockResolvedValue(mockPendingResult);
|
||||
mockFeatureSelection(['analytics']);
|
||||
|
||||
await createCommand(['--region', 'US']).runAsync();
|
||||
|
||||
expect(openBrowserAsync).toHaveBeenCalledWith(mockPendingResult.url);
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Opened'));
|
||||
expect(PostHogMutation.setupPostHogProjectAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
posthogOrganizationConnectionId: mockConnection.id,
|
||||
});
|
||||
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still prints the approval URL when a browser cannot be opened', async () => {
|
||||
jest
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValue(mockConnection);
|
||||
jest.mocked(PostHogQuery.getPostHogProjectByAppIdAsync).mockResolvedValue(null);
|
||||
jest.mocked(PostHogMutation.startPostHogConnectionAsync).mockResolvedValue(mockPendingResult);
|
||||
jest.mocked(openBrowserAsync).mockResolvedValue(false as never);
|
||||
mockFeatureSelection(['analytics']);
|
||||
|
||||
await createCommand(['--region', 'US']).runAsync();
|
||||
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Open this URL to approve the connection')
|
||||
);
|
||||
expect(PostHogMutation.setupPostHogProjectAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries polling after a transient failure, then completes', async () => {
|
||||
jest.useFakeTimers();
|
||||
const debugSpy = jest.spyOn(Log, 'debug').mockImplementation(() => {});
|
||||
try {
|
||||
// No connection initially; the first poll throws (transient), the next finds it.
|
||||
jest
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockRejectedValueOnce(new Error('network blip'))
|
||||
.mockResolvedValue(mockConnection);
|
||||
jest.mocked(PostHogQuery.getPostHogProjectByAppIdAsync).mockResolvedValue(null);
|
||||
jest.mocked(PostHogMutation.startPostHogConnectionAsync).mockResolvedValue(mockPendingResult);
|
||||
mockFeatureSelection(['analytics']);
|
||||
|
||||
const promise = createCommand(['--region', 'US']).runAsync();
|
||||
// Flush the poll interval so the retry runs after the transient failure.
|
||||
await jest.advanceTimersByTimeAsync(3_000);
|
||||
await promise;
|
||||
|
||||
expect(debugSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Polling for the PostHog connection failed')
|
||||
);
|
||||
expect(PostHogMutation.setupPostHogProjectAsync).toHaveBeenCalled();
|
||||
} finally {
|
||||
debugSpy.mockRestore();
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses the browser handoff non-interactively and provisions nothing', async () => {
|
||||
jest
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValue(null);
|
||||
jest.mocked(PostHogQuery.getPostHogProjectByAppIdAsync).mockResolvedValue(null);
|
||||
jest.mocked(PostHogMutation.createPostHogAccountRequestAsync).mockRejectedValue(
|
||||
Object.assign(new Error('You already have a PostHog account.'), {
|
||||
graphQLErrors: [{ extensions: { errorCode: 'POSTHOG_EXISTING_USER_NOT_SUPPORTED_ERROR' } }],
|
||||
})
|
||||
jest.mocked(PostHogMutation.startPostHogConnectionAsync).mockResolvedValue(mockPendingResult);
|
||||
|
||||
await expect(createCommand(['--non-interactive', '--region', 'US']).runAsync()).rejects.toThrow(
|
||||
/already has a PostHog account.*interactively/s
|
||||
);
|
||||
|
||||
await expect(createCommand(['--region', 'US']).runAsync()).rejects.toThrow(
|
||||
'You already have a PostHog account.'
|
||||
);
|
||||
|
||||
// Fails (non-zero exit) without provisioning a project or writing any config.
|
||||
expect(openBrowserAsync).not.toHaveBeenCalled();
|
||||
expect(PostHogMutation.setupPostHogProjectAsync).not.toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.createForAppAsync).not.toHaveBeenCalled();
|
||||
expect(fs.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('times out (and provisions nothing) if the connection is never approved', async () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
// Connection never appears: null at the first check and on every poll.
|
||||
jest
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValue(null);
|
||||
jest.mocked(PostHogQuery.getPostHogProjectByAppIdAsync).mockResolvedValue(null);
|
||||
jest.mocked(PostHogMutation.startPostHogConnectionAsync).mockResolvedValue(mockPendingResult);
|
||||
mockFeatureSelection(['analytics']);
|
||||
|
||||
const promise = createCommand(['--region', 'US']).runAsync();
|
||||
const assertion = expect(promise).rejects.toThrow(/Timed out waiting for the PostHog/);
|
||||
// Advance past the 15-minute poll window, flushing each poll iteration's awaits.
|
||||
await jest.advanceTimersByTimeAsync(16 * 60 * 1_000);
|
||||
await assertion;
|
||||
|
||||
expect(PostHogMutation.setupPostHogProjectAsync).not.toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.createForAppAsync).not.toHaveBeenCalled();
|
||||
expect(fs.writeFile).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('emits JSON output with --json', async () => {
|
||||
await createCommand(['--json', '--posthog-cli-api-key', 'phx_x']).runAsync();
|
||||
|
||||
@@ -390,7 +497,7 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
|
||||
await createCommand(['--region', 'US']).runAsync();
|
||||
|
||||
expect(PostHogMutation.createPostHogAccountRequestAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
expect(PostHogMutation.startPostHogConnectionAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
accountId: testAccountId,
|
||||
region: PostHogRegion.Us,
|
||||
});
|
||||
@@ -405,7 +512,7 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
.mocked(PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync)
|
||||
.mockResolvedValue(null);
|
||||
jest
|
||||
.mocked(PostHogMutation.createPostHogAccountRequestAsync)
|
||||
.mocked(PostHogMutation.startPostHogConnectionAsync)
|
||||
.mockRejectedValue(new Error('network down'));
|
||||
|
||||
await expect(createCommand(['--region', 'US']).runAsync()).rejects.toThrow('network down');
|
||||
@@ -532,7 +639,7 @@ describe(IntegrationsPostHogConnect, () => {
|
||||
await createCommand([]).runAsync();
|
||||
|
||||
expect(selectAsync).toHaveBeenCalledWith('Select a PostHog region', expect.any(Array));
|
||||
expect(PostHogMutation.createPostHogAccountRequestAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
expect(PostHogMutation.startPostHogConnectionAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
accountId: testAccountId,
|
||||
region: PostHogRegion.Us,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Flags } from '@oclif/core';
|
||||
import openBrowserAsync from 'better-opn';
|
||||
import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import * as fs from 'fs-extra';
|
||||
@@ -24,7 +25,11 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
|
||||
import { PostHogMutation } from '../../../graphql/mutations/PostHogMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import { PostHogQuery } from '../../../graphql/queries/PostHogQuery';
|
||||
import { PostHogProjectData } from '../../../graphql/types/PostHogConnection';
|
||||
import {
|
||||
PostHogOrganizationConnectionData,
|
||||
PostHogProjectData,
|
||||
StartPostHogConnectionResult,
|
||||
} from '../../../graphql/types/PostHogConnection';
|
||||
import Log, { link } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { createOrModifyExpoConfigAsync } from '../../../project/expoConfig';
|
||||
@@ -59,6 +64,11 @@ const POSTHOG_CLI_API_KEY_ENV_VAR_NAME = 'POSTHOG_CLI_API_KEY';
|
||||
const POSTHOG_CLI_PROJECT_ID_ENV_VAR_NAME = 'POSTHOG_CLI_PROJECT_ID';
|
||||
const POSTHOG_CLI_HOST_ENV_VAR_NAME = 'POSTHOG_CLI_HOST';
|
||||
|
||||
// Match the server's 15-minute pending-row TTL — timing out sooner would strand an approval
|
||||
// the user completes within the window.
|
||||
const CONNECTION_POLL_INTERVAL_MS = 2_000;
|
||||
const CONNECTION_POLL_TIMEOUT_MS = 15 * 60 * 1_000;
|
||||
|
||||
const PERSONAL_API_KEY_SETTINGS_PATH = '/settings/user-api-keys';
|
||||
const ERROR_TRACKING_NEEDS_KEY_MESSAGE = `Error tracking needs a PostHog personal API key in non-interactive mode. Pass --posthog-cli-api-key (create one in PostHog under Settings → Personal API keys (${PERSONAL_API_KEY_SETTINGS_PATH}) with the "Source map upload" preset), or drop --error-tracking.`;
|
||||
|
||||
@@ -141,19 +151,7 @@ export default class IntegrationsPostHogConnect extends EasCommand {
|
||||
);
|
||||
} else {
|
||||
const region = await this.resolveRegionAsync(regionFlag, nonInteractive);
|
||||
const spinner = ora('Creating PostHog organization').start();
|
||||
try {
|
||||
connection = await PostHogMutation.createPostHogAccountRequestAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
region,
|
||||
});
|
||||
spinner.succeed(
|
||||
`Created PostHog organization ${chalk.bold(connection.posthogOrganizationName)}`
|
||||
);
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to create PostHog organization');
|
||||
throw error;
|
||||
}
|
||||
connection = await this.startConnectionAsync(graphqlClient, account, region, nonInteractive);
|
||||
}
|
||||
|
||||
let project = await PostHogQuery.getPostHogProjectByAppIdAsync(graphqlClient, projectId);
|
||||
@@ -276,6 +274,97 @@ export default class IntegrationsPostHogConnect extends EasCommand {
|
||||
this.printNextSteps(project, features, manualSteps);
|
||||
}
|
||||
|
||||
private async startConnectionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
account: { id: string; name: string },
|
||||
region: PostHogRegion,
|
||||
nonInteractive: boolean
|
||||
): Promise<PostHogOrganizationConnectionData> {
|
||||
const spinner = ora('Creating PostHog organization').start();
|
||||
let result: StartPostHogConnectionResult;
|
||||
try {
|
||||
result = await PostHogMutation.startPostHogConnectionAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
region,
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to create PostHog organization');
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result.__typename === 'PostHogOrganizationConnection') {
|
||||
spinner.succeed(`Created PostHog organization ${chalk.bold(result.posthogOrganizationName)}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`The email on the ${chalk.bold(account.name)} account already has a PostHog account, which must be connected by approving it in a browser. Re-run \`eas integrations:posthog:connect\` interactively to finish connecting.`
|
||||
);
|
||||
}
|
||||
return await this.completePendingConnectionViaBrowserAsync(graphqlClient, account, result);
|
||||
}
|
||||
|
||||
private async completePendingConnectionViaBrowserAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
account: { id: string; name: string },
|
||||
pending: { url: string }
|
||||
): Promise<PostHogOrganizationConnectionData> {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`The email on the ${chalk.bold(account.name)} account already has a PostHog account. Approve connecting it to Expo in your browser.`
|
||||
);
|
||||
const opened = await openBrowserAsync(pending.url).catch(() => false);
|
||||
Log.log(
|
||||
opened
|
||||
? `Opened ${link(pending.url)}`
|
||||
: `Open this URL to approve the connection: ${link(pending.url)}`
|
||||
);
|
||||
|
||||
const spinner = ora(
|
||||
'Waiting for you to approve the connection in PostHog (up to 15 minutes; press Ctrl-C to cancel)'
|
||||
).start();
|
||||
try {
|
||||
const connection = await this.pollForConnectionAsync(graphqlClient, account.id);
|
||||
spinner.succeed(
|
||||
`Connected PostHog organization ${chalk.bold(connection.posthogOrganizationName)}`
|
||||
);
|
||||
return connection;
|
||||
} catch (error) {
|
||||
spinner.fail("Couldn't confirm the PostHog connection");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async pollForConnectionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string
|
||||
): Promise<PostHogOrganizationConnectionData> {
|
||||
const deadline = Date.now() + CONNECTION_POLL_TIMEOUT_MS;
|
||||
while (true) {
|
||||
let connection: PostHogOrganizationConnectionData | null = null;
|
||||
try {
|
||||
connection = await PostHogQuery.getPostHogOrganizationConnectionByAccountIdAsync(
|
||||
graphqlClient,
|
||||
accountId,
|
||||
{ useCache: false }
|
||||
);
|
||||
} catch (error) {
|
||||
Log.debug(`Polling for the PostHog connection failed, will retry: ${error}`);
|
||||
}
|
||||
if (connection) {
|
||||
return connection;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
throw new Error(
|
||||
'Timed out waiting for the PostHog connection. If you approved it in your browser, re-run `eas integrations:posthog:connect` — it will pick up the connection. If not, approve it and try again.'
|
||||
);
|
||||
}
|
||||
await new Promise<void>(resolve => setTimeout(resolve, CONNECTION_POLL_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveFeaturesAsync(
|
||||
flags: { 'session-replay'?: boolean; 'error-tracking'?: boolean },
|
||||
cliApiKey: string | undefined,
|
||||
|
||||
@@ -118,7 +118,8 @@ describe(SimulatorGet, () => {
|
||||
id: 'session-123',
|
||||
type: 'agent-device',
|
||||
status: DeviceRunSessionStatus.InProgress,
|
||||
jobRunUrl: 'https://expo.dev/accounts/testuser/projects/testapp/job-runs/job-123',
|
||||
deviceRunSessionUrl:
|
||||
'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123',
|
||||
remoteConfig: session.remoteConfig,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +135,8 @@ describe(SimulatorList, () => {
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
startedAt: '2025-01-01T00:00:05.000Z',
|
||||
finishedAt: undefined,
|
||||
jobRunUrl: 'https://expo.dev/accounts/testuser/projects/testapp/job-runs/job-123',
|
||||
deviceRunSessionUrl:
|
||||
'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
|
||||
@@ -61,7 +61,8 @@ type DeviceRunSessionById = DeviceRunSessionByIdQuery['deviceRunSessions']['byId
|
||||
const graphqlClient = {} as ExpoGraphqlClient;
|
||||
const projectDir = '/test/project';
|
||||
const simulatorDotenvPath = `${projectDir}/.env.eas-simulator`;
|
||||
const jobRunUrl = 'https://expo.dev/accounts/testuser/projects/testapp/job-runs/job-123';
|
||||
const deviceRunSessionUrl =
|
||||
'https://expo.dev/accounts/testuser/projects/testapp/simulator-sessions/session-123';
|
||||
|
||||
const mockCreateDeviceRunSessionAsync = jest.mocked(
|
||||
DeviceRunSessionMutation.createDeviceRunSessionAsync
|
||||
@@ -185,7 +186,7 @@ describe(SimulatorStart, () => {
|
||||
});
|
||||
expect(fs.writeFile).not.toHaveBeenCalled();
|
||||
expect(mockOra.mock.results[0]?.value.succeed).toHaveBeenCalledWith(
|
||||
`Device run session created (id: session-123) ${jobRunUrl}`
|
||||
`Simulator session created (id: session-123) ${deviceRunSessionUrl}`
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("export AGENT_DEVICE_DAEMON_BASE_URL='https://agent.example.com'")
|
||||
@@ -214,7 +215,7 @@ describe(SimulatorStart, () => {
|
||||
mockByIdAsync.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(mockOra.mock.results[0]?.value.succeed).toHaveBeenCalledWith(
|
||||
`Device run session created (id: session-123, saved to ${SIMULATOR_DOTENV_FILE_NAME}) ${jobRunUrl}`
|
||||
`Simulator session created (id: session-123, saved to ${SIMULATOR_DOTENV_FILE_NAME}) ${deviceRunSessionUrl}`
|
||||
);
|
||||
expect(Log.withTick).not.toHaveBeenCalled();
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
@@ -298,7 +299,7 @@ describe(SimulatorStart, () => {
|
||||
|
||||
const { command } = createCommand(['--platform', 'ios', '--non-interactive', '--no-force']);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Existing simulator session in environment. Use --force to create a new device session.'
|
||||
'Existing simulator session in environment. Use --force to create a new simulator session.'
|
||||
);
|
||||
|
||||
expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { getBareJobRunUrl } from '../../build/utils/url';
|
||||
import { getDeviceRunSessionUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
@@ -24,11 +24,11 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
export default class SimulatorGet extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] get info about a remote simulator session on EAS by its device run session ID';
|
||||
'[EXPERIMENTAL] get info about a remote simulator session on EAS by its simulator session ID';
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
description: `Device run session ID. Defaults to ${SIMULATOR_DOTENV_FILE_NAME}.`,
|
||||
description: `Simulator session ID. Defaults to ${SIMULATOR_DOTENV_FILE_NAME}.`,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -61,26 +61,28 @@ export default class SimulatorGet extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
const fetchSpinner = ora(`Fetching device run session ${flagId}`).start();
|
||||
const fetchSpinner = ora(`Fetching simulator session ${flagId}`).start();
|
||||
let session;
|
||||
try {
|
||||
session = await DeviceRunSessionQuery.byIdAsync(graphqlClient, flagId);
|
||||
fetchSpinner.succeed(`Fetched device run session ${session.id}`);
|
||||
fetchSpinner.succeed(`Fetched simulator session ${session.id}`);
|
||||
} catch (err) {
|
||||
fetchSpinner.fail(`Failed to fetch device run session ${flagId}`);
|
||||
fetchSpinner.fail(`Failed to fetch simulator session ${flagId}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const jobRunUrl = session.turtleJobRun
|
||||
? getBareJobRunUrl(session.app.ownerAccount.name, session.app.slug, session.turtleJobRun.id)
|
||||
: '';
|
||||
const deviceRunSessionUrl = getDeviceRunSessionUrl(
|
||||
session.app.ownerAccount.name,
|
||||
session.app.slug,
|
||||
session.id
|
||||
);
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput({
|
||||
id: session.id,
|
||||
type: deviceRunSessionTypeToFlagValue(session.type),
|
||||
status: session.status,
|
||||
jobRunUrl: jobRunUrl || undefined,
|
||||
deviceRunSessionUrl,
|
||||
remoteConfig: session.remoteConfig,
|
||||
});
|
||||
return;
|
||||
@@ -90,7 +92,7 @@ export default class SimulatorGet extends EasCommand {
|
||||
Log.log(`ID: ${session.id}`);
|
||||
Log.log(`Type: ${session.type}`);
|
||||
Log.log(`Status: ${session.status}`);
|
||||
Log.log(`URL: ${jobRunUrl ? link(jobRunUrl) : ''}`);
|
||||
Log.log(`URL: ${link(deviceRunSessionUrl)}`);
|
||||
|
||||
if (session.status === DeviceRunSessionStatus.InProgress) {
|
||||
Log.newLine();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getBareJobRunUrl } from '../../build/utils/url';
|
||||
import { getDeviceRunSessionUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
@@ -110,7 +110,7 @@ export default class SimulatorList extends EasCommand {
|
||||
|
||||
const limit = flags.limit ?? DEFAULT_LIMIT;
|
||||
|
||||
const fetchSpinner = jsonFlag ? null : ora('Fetching device run sessions').start();
|
||||
const fetchSpinner = jsonFlag ? null : ora('Fetching simulator sessions').start();
|
||||
let connection;
|
||||
try {
|
||||
connection = await DeviceRunSessionQuery.listByAppIdAsync(graphqlClient, {
|
||||
@@ -119,9 +119,9 @@ export default class SimulatorList extends EasCommand {
|
||||
after: flags.after,
|
||||
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
||||
});
|
||||
fetchSpinner?.succeed(`Fetched ${connection.edges.length} device run session(s)`);
|
||||
fetchSpinner?.succeed(`Fetched ${connection.edges.length} simulator session(s)`);
|
||||
} catch (err) {
|
||||
fetchSpinner?.fail('Failed to fetch device run sessions');
|
||||
fetchSpinner?.fail('Failed to fetch simulator sessions');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -137,13 +137,11 @@ export default class SimulatorList extends EasCommand {
|
||||
createdAt: session.createdAt,
|
||||
startedAt: session.startedAt ?? undefined,
|
||||
finishedAt: session.finishedAt ?? undefined,
|
||||
jobRunUrl: session.turtleJobRun
|
||||
? getBareJobRunUrl(
|
||||
session.app.ownerAccount.name,
|
||||
session.app.slug,
|
||||
session.turtleJobRun.id
|
||||
)
|
||||
: undefined,
|
||||
deviceRunSessionUrl: getDeviceRunSessionUrl(
|
||||
session.app.ownerAccount.name,
|
||||
session.app.slug,
|
||||
session.id
|
||||
),
|
||||
})),
|
||||
pageInfo: connection.pageInfo,
|
||||
});
|
||||
@@ -152,25 +150,25 @@ export default class SimulatorList extends EasCommand {
|
||||
|
||||
if (sessions.length === 0) {
|
||||
Log.newLine();
|
||||
Log.log('No device run sessions found.');
|
||||
Log.log('No simulator sessions found.');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
const formattedEntries = sessions.map(session => {
|
||||
const jobRunUrl = session.turtleJobRun
|
||||
? getBareJobRunUrl(session.app.ownerAccount.name, session.app.slug, session.turtleJobRun.id)
|
||||
: null;
|
||||
const deviceRunSessionUrl = getDeviceRunSessionUrl(
|
||||
session.app.ownerAccount.name,
|
||||
session.app.slug,
|
||||
session.id
|
||||
);
|
||||
const lines = [
|
||||
`ID: ${session.id}`,
|
||||
`Type: ${session.type}`,
|
||||
`Status: ${session.status}`,
|
||||
`Platform: ${session.platform}`,
|
||||
`Created: ${fromNow(new Date(session.createdAt))} ago`,
|
||||
`URL: ${link(deviceRunSessionUrl)}`,
|
||||
];
|
||||
if (jobRunUrl) {
|
||||
lines.push(`URL: ${link(jobRunUrl)}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
});
|
||||
Log.log(formattedEntries.join(`\n\n${chalk.dim('———')}\n\n`));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getBareJobRunUrl } from '../../build/utils/url';
|
||||
import { getDeviceRunSessionUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -54,22 +54,22 @@ export default class SimulatorStart extends EasCommand {
|
||||
required: true,
|
||||
})(),
|
||||
type: Flags.option({
|
||||
description: 'Type of device run session to create',
|
||||
description: 'Type of simulator session to create',
|
||||
options: Object.values(DEVICE_RUN_SESSION_TYPE_FLAG_VALUES),
|
||||
default: DEVICE_RUN_SESSION_TYPE_FLAG_VALUES[DeviceRunSessionType.AgentDevice],
|
||||
})(),
|
||||
'package-version': Flags.string({
|
||||
description:
|
||||
'Version of the package backing the device run session (e.g. "0.1.3-alpha.3"). Defaults to "latest" when omitted.',
|
||||
'Version of the package backing the simulator session (e.g. "0.1.3-alpha.3"). Defaults to "latest" when omitted.',
|
||||
}),
|
||||
'max-duration-minutes': Flags.integer({
|
||||
description:
|
||||
'Maximum duration of the device run session in minutes before it is automatically stopped. Only customizable on paid plans. Defaults to a value derived from the job run priority when omitted.',
|
||||
'Maximum duration of the simulator session in minutes before it is automatically stopped. Only customizable on paid plans. Defaults to a value derived from the job run priority when omitted.',
|
||||
min: 0,
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description:
|
||||
'[default: true] Create a new device session even when an existing simulator session is present in the environment.',
|
||||
'[default: true] Create a new simulator session even when an existing simulator session is present in the environment.',
|
||||
default: true,
|
||||
allowNo: true,
|
||||
}),
|
||||
@@ -107,7 +107,7 @@ export default class SimulatorStart extends EasCommand {
|
||||
const existingDeviceRunSessionId = process.env[EAS_SIMULATOR_SESSION_ID];
|
||||
if (existingDeviceRunSessionId && !flags.force) {
|
||||
throw new Error(
|
||||
`Existing simulator session in environment. Use --force to create a new device session.`
|
||||
`Existing simulator session in environment. Use --force to create a new simulator session.`
|
||||
);
|
||||
}
|
||||
if (existingDeviceRunSessionId) {
|
||||
@@ -121,9 +121,9 @@ export default class SimulatorStart extends EasCommand {
|
||||
|
||||
const platform = flags.platform === 'android' ? AppPlatform.Android : AppPlatform.Ios;
|
||||
|
||||
const createSpinner = ora('🚀 Creating device run session').start();
|
||||
const createSpinner = ora('🚀 Creating simulator session').start();
|
||||
let deviceRunSessionId: string;
|
||||
let jobRunUrl: string;
|
||||
let deviceRunSessionUrl: string;
|
||||
try {
|
||||
const session = await DeviceRunSessionMutation.createDeviceRunSessionAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
@@ -133,8 +133,12 @@ export default class SimulatorStart extends EasCommand {
|
||||
maxRunTimeMinutes: flags['max-duration-minutes'],
|
||||
});
|
||||
deviceRunSessionId = session.id;
|
||||
const jobRunId = nullthrows(session.turtleJobRun?.id, 'Expected device run session to start');
|
||||
jobRunUrl = getBareJobRunUrl(session.app.ownerAccount.name, session.app.slug, jobRunId);
|
||||
nullthrows(session.turtleJobRun?.id, 'Expected simulator session to start');
|
||||
deviceRunSessionUrl = getDeviceRunSessionUrl(
|
||||
session.app.ownerAccount.name,
|
||||
session.app.slug,
|
||||
deviceRunSessionId
|
||||
);
|
||||
const simulatorEnvWritten =
|
||||
!jsonFlag && flags['out-config-type'] === OUT_CONFIG_TYPE_VALUES.Dotenv
|
||||
? await writeSimulatorEnvSafelyAsync(projectDir, {
|
||||
@@ -142,12 +146,12 @@ export default class SimulatorStart extends EasCommand {
|
||||
})
|
||||
: false;
|
||||
createSpinner.succeed(
|
||||
`Device run session created (id: ${deviceRunSessionId}${
|
||||
`Simulator session created (id: ${deviceRunSessionId}${
|
||||
simulatorEnvWritten ? `, saved to ${SIMULATOR_DOTENV_FILE_NAME}` : ''
|
||||
}) ${link(jobRunUrl)}`
|
||||
}) ${link(deviceRunSessionUrl)}`
|
||||
);
|
||||
} catch (err) {
|
||||
createSpinner.fail('Failed to create device run session');
|
||||
createSpinner.fail('Failed to create simulator session');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -164,7 +168,7 @@ export default class SimulatorStart extends EasCommand {
|
||||
session.status === DeviceRunSessionStatus.Stopped
|
||||
) {
|
||||
throw new Error(
|
||||
`Device run session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} session was ready. ${link(jobRunUrl)}`
|
||||
`Simulator session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} session was ready. ${link(deviceRunSessionUrl)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,7 +179,7 @@ export default class SimulatorStart extends EasCommand {
|
||||
jobRunStatus === JobRunStatus.Finished
|
||||
) {
|
||||
throw new Error(
|
||||
`Turtle job run for device run session ${deviceRunSessionId} ${jobRunStatus.toLowerCase()} before the ${flags.type} session was ready. ${link(jobRunUrl)}`
|
||||
`Turtle job run for simulator session ${deviceRunSessionId} ${jobRunStatus.toLowerCase()} before the ${flags.type} session was ready. ${link(deviceRunSessionUrl)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,7 +201,7 @@ export default class SimulatorStart extends EasCommand {
|
||||
pollSpinner.fail(`Timed out waiting for ${flags.type} session to be ready`);
|
||||
await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
|
||||
throw new Error(
|
||||
`Timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s waiting for ${flags.type} session to be ready. ${link(jobRunUrl)}`
|
||||
`Timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s waiting for ${flags.type} session to be ready. ${link(deviceRunSessionUrl)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,7 +216,7 @@ export default class SimulatorStart extends EasCommand {
|
||||
printJsonOnlyOutput({
|
||||
id: deviceRunSessionId,
|
||||
type: flags.type,
|
||||
jobRunUrl,
|
||||
deviceRunSessionUrl,
|
||||
remoteConfig,
|
||||
});
|
||||
return;
|
||||
@@ -232,7 +236,7 @@ export default class SimulatorStart extends EasCommand {
|
||||
await waitForSessionEndOrInterruptAsync({
|
||||
graphqlClient,
|
||||
deviceRunSessionId,
|
||||
jobRunUrl,
|
||||
deviceRunSessionUrl,
|
||||
projectDir,
|
||||
});
|
||||
}
|
||||
@@ -258,16 +262,16 @@ async function writeSimulatorEnvSafelyAsync(
|
||||
async function waitForSessionEndOrInterruptAsync({
|
||||
graphqlClient,
|
||||
deviceRunSessionId,
|
||||
jobRunUrl,
|
||||
deviceRunSessionUrl,
|
||||
projectDir,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
deviceRunSessionId: string;
|
||||
jobRunUrl: string;
|
||||
deviceRunSessionUrl: string;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
const spinner = ora(
|
||||
`Device run session active — press Ctrl+C to stop, or run \`eas simulator:stop --id ${deviceRunSessionId}\` from another shell`
|
||||
`Simulator session active — press Ctrl+C to stop, or run \`eas simulator:stop --id ${deviceRunSessionId}\` from another shell`
|
||||
).start();
|
||||
|
||||
const abortController = new AbortController();
|
||||
@@ -286,7 +290,7 @@ async function waitForSessionEndOrInterruptAsync({
|
||||
// Force exit on a second Ctrl+C in case cleanup is hanging. The session may still be
|
||||
// running on EAS, so tell the user how to make sure it gets terminated.
|
||||
spinner.fail(
|
||||
`Aborted before the device run session could be stopped. Run \`eas simulator:stop --id ${deviceRunSessionId}\` to terminate it and avoid unexpected charges.`
|
||||
`Aborted before the simulator session could be stopped. Run \`eas simulator:stop --id ${deviceRunSessionId}\` to terminate it and avoid unexpected charges.`
|
||||
);
|
||||
process.exit(130);
|
||||
}
|
||||
@@ -301,7 +305,7 @@ async function waitForSessionEndOrInterruptAsync({
|
||||
session = await DeviceRunSessionQuery.byIdAsync(graphqlClient, deviceRunSessionId);
|
||||
} catch (err) {
|
||||
Log.debug(
|
||||
`Failed to poll device run session: ${err instanceof Error ? err.message : String(err)}`
|
||||
`Failed to poll simulator session: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
await Promise.race([sleepAsync(POLL_INTERVAL_MS), abortPromise]);
|
||||
continue;
|
||||
@@ -312,15 +316,15 @@ async function waitForSessionEndOrInterruptAsync({
|
||||
session.status === DeviceRunSessionStatus.Errored ||
|
||||
jobRunStatus === JobRunStatus.Errored
|
||||
) {
|
||||
spinner.fail(`Device run session errored. ${link(jobRunUrl)}`);
|
||||
throw new Error(`Device run session ${deviceRunSessionId} errored.`);
|
||||
spinner.fail(`Simulator session errored. ${link(deviceRunSessionUrl)}`);
|
||||
throw new Error(`Simulator session ${deviceRunSessionId} errored.`);
|
||||
}
|
||||
if (
|
||||
session.status === DeviceRunSessionStatus.Stopped ||
|
||||
jobRunStatus === JobRunStatus.Canceled ||
|
||||
jobRunStatus === JobRunStatus.Finished
|
||||
) {
|
||||
spinner.succeed(`Device run session ended. ${link(jobRunUrl)}`);
|
||||
spinner.succeed(`Simulator session ended. ${link(deviceRunSessionUrl)}`);
|
||||
await resetSimulatorEnvVerboseAsync(projectDir);
|
||||
return;
|
||||
}
|
||||
@@ -328,17 +332,17 @@ async function waitForSessionEndOrInterruptAsync({
|
||||
await Promise.race([sleepAsync(POLL_INTERVAL_MS), abortPromise]);
|
||||
}
|
||||
|
||||
spinner.text = 'Stopping device run session...';
|
||||
spinner.text = 'Stopping simulator session...';
|
||||
const stopped = await ensureDeviceRunSessionStoppedSafelyAsync(
|
||||
graphqlClient,
|
||||
deviceRunSessionId
|
||||
);
|
||||
if (stopped) {
|
||||
spinner.succeed('Device run session stopped');
|
||||
spinner.succeed('Simulator session stopped');
|
||||
await resetSimulatorEnvVerboseAsync(projectDir);
|
||||
} else {
|
||||
spinner.fail(
|
||||
`Could not confirm the device run session was stopped. Run \`eas simulator:stop --id ${deviceRunSessionId}\` to terminate it and avoid unexpected charges.`
|
||||
`Could not confirm the simulator session was stopped. Run \`eas simulator:stop --id ${deviceRunSessionId}\` to terminate it and avoid unexpected charges.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -368,7 +372,7 @@ async function ensureDeviceRunSessionStoppedSafelyAsync(
|
||||
} catch (err) {
|
||||
// Cleanup is best-effort; surface the failure but don't mask the original error.
|
||||
Log.warn(
|
||||
`Failed to stop device run session ${deviceRunSessionId}: ${
|
||||
`Failed to stop simulator session ${deviceRunSessionId}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -17,11 +17,11 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
export default class SimulatorStop extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] stop a remote simulator session on EAS by its device run session ID';
|
||||
'[EXPERIMENTAL] stop a remote simulator session on EAS by its simulator session ID';
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
description: `Device run session ID. Defaults to ${SIMULATOR_DOTENV_FILE_NAME}.`,
|
||||
description: `Simulator session ID. Defaults to ${SIMULATOR_DOTENV_FILE_NAME}.`,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -54,16 +54,16 @@ export default class SimulatorStop extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
const stopSpinner = ora(`🛑 Stopping device run session ${flagId}`).start();
|
||||
const stopSpinner = ora(`🛑 Stopping simulator session ${flagId}`).start();
|
||||
let session;
|
||||
try {
|
||||
session = await DeviceRunSessionMutation.ensureDeviceRunSessionStoppedAsync(
|
||||
graphqlClient,
|
||||
flagId
|
||||
);
|
||||
stopSpinner.succeed(`🎉 Device run session ${session.id} is ${session.status.toLowerCase()}`);
|
||||
stopSpinner.succeed(`🎉 Simulator session ${session.id} is ${session.status.toLowerCase()}`);
|
||||
} catch (err) {
|
||||
stopSpinner.fail(`Failed to stop device run session ${flagId}`);
|
||||
stopSpinner.fail(`Failed to stop simulator session ${flagId}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import { createFingerprintAsync } from './cli';
|
||||
import { Fingerprint } from './types';
|
||||
@@ -16,7 +16,7 @@ export async function getFingerprintInfoFromLocalProjectForPlatformsAsync(
|
||||
projectId: string,
|
||||
vcsClient: Client,
|
||||
platforms: AppPlatform[],
|
||||
{ env }: { env?: Record<string, string> } = {}
|
||||
{ env }: { env?: Env } = {}
|
||||
): Promise<Fingerprint> {
|
||||
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
|
||||
const optionsFromWorkflow = getFingerprintOptionsFromWorkflow(platforms, workflows);
|
||||
|
||||
+334
-3
@@ -1436,6 +1436,8 @@ export type App = Project & {
|
||||
/** Android app credentials for the project */
|
||||
androidAppCredentials: Array<AndroidAppCredentials>;
|
||||
appStoreConnectApp?: Maybe<AppStoreConnectApp>;
|
||||
/** Connection status for this project's App Store Connect-triggered workflows. */
|
||||
appStoreConnectWorkflowConnectionStatus: AppStoreConnectWorkflowConnectionStatus;
|
||||
/**
|
||||
* ios.appStoreUrl field from most recent classic update manifest
|
||||
* @deprecated Classic updates have been deprecated.
|
||||
@@ -1591,6 +1593,7 @@ export type App = Project & {
|
||||
/** EAS updates owned by an app */
|
||||
updates: Array<Update>;
|
||||
updatesPaginated: AppUpdatesConnection;
|
||||
updatesTimeline: UpdatesTimelineConnection;
|
||||
/** Project query object for querying EAS usage metrics */
|
||||
usageMetrics: AppUsageMetrics;
|
||||
/** @deprecated Use ownerAccount.name instead */
|
||||
@@ -1874,6 +1877,16 @@ export type AppUpdatesPaginatedArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppUpdatesTimelineArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<UpdatesTimelineFilter>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppWebhooksArgs = {
|
||||
filter?: InputMaybe<WebhookFilter>;
|
||||
@@ -2209,6 +2222,14 @@ export type AppObserve = {
|
||||
customEventList: AppObserveCustomEventListConnection;
|
||||
customEventNames: AppObserveCustomEventNames;
|
||||
environments: Array<Scalars['String']['output']>;
|
||||
/** Breaks a single error group down by app version, OS, device, or country (detail page bars). */
|
||||
errorGroupBreakdown: AppObserveErrorGroupBreakdown;
|
||||
/** Distinct unhandled-JS-error groups (the issues list), grouped by fingerprint. */
|
||||
errorGroups: AppObserveErrorGroups;
|
||||
/** Headline error stats for the overview: crash-free rates and error/affected counts. */
|
||||
errorStats: AppObserveErrorStats;
|
||||
/** Time-bucketed exception counts split into fatal vs non-fatal, for the stacked-bar chart. */
|
||||
errorTimeSeries: AppObserveErrorTimeSeries;
|
||||
events: AppObserveEventsConnection;
|
||||
navigationRoutes: AppObserveNavigationRoutesConnection;
|
||||
timeSeries: AppObserveTimeSeries;
|
||||
@@ -2266,6 +2287,26 @@ export type AppObserveEnvironmentsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveErrorGroupBreakdownArgs = {
|
||||
input: AppObserveErrorGroupBreakdownInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveErrorGroupsArgs = {
|
||||
input: AppObserveErrorGroupsInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveErrorStatsArgs = {
|
||||
input: AppObserveErrorStatsInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveErrorTimeSeriesArgs = {
|
||||
input: AppObserveErrorTimeSeriesInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveEventsArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -2406,10 +2447,17 @@ export type AppObserveCustomEvent = {
|
||||
deviceOsVersion: Scalars['String']['output'];
|
||||
easClientId: Scalars['String']['output'];
|
||||
environment?: Maybe<Scalars['String']['output']>;
|
||||
errorFingerprint?: Maybe<Scalars['String']['output']>;
|
||||
errorSource?: Maybe<Scalars['String']['output']>;
|
||||
eventName: Scalars['String']['output'];
|
||||
exceptionMessage?: Maybe<Scalars['String']['output']>;
|
||||
exceptionStacktrace?: Maybe<Scalars['String']['output']>;
|
||||
/** Error fields below are populated only for exception events (eventName: exception). */
|
||||
exceptionType?: Maybe<Scalars['String']['output']>;
|
||||
expoSdkVersion?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
ingestedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
isFatal?: Maybe<Scalars['Boolean']['output']>;
|
||||
properties: Array<AppObserveEventProperty>;
|
||||
reactNativeVersion?: Maybe<Scalars['String']['output']>;
|
||||
sessionId?: Maybe<Scalars['String']['output']>;
|
||||
@@ -2464,6 +2512,8 @@ export type AppObserveCustomEventListFilter = {
|
||||
easClientId?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Restrict to one error group. Combine with the exception eventName to list a group's occurrences. */
|
||||
errorFingerprint?: InputMaybe<Scalars['String']['input']>;
|
||||
eventName?: InputMaybe<Scalars['String']['input']>;
|
||||
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
@@ -2504,6 +2554,167 @@ export type AppObserveCustomEventPropertyFilter = {
|
||||
value: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveErrorBreakdownBucket = {
|
||||
__typename?: 'AppObserveErrorBreakdownBucket';
|
||||
count: Scalars['Int']['output'];
|
||||
/** Dimension value (app version, OS, device model, or country code). Empty string when unknown. */
|
||||
key: Scalars['String']['output'];
|
||||
uniqueUserCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export enum AppObserveErrorBreakdownDimension {
|
||||
AppVersion = 'APP_VERSION',
|
||||
Country = 'COUNTRY',
|
||||
Device = 'DEVICE',
|
||||
Os = 'OS'
|
||||
}
|
||||
|
||||
export type AppObserveErrorGroup = {
|
||||
__typename?: 'AppObserveErrorGroup';
|
||||
affectedSessionCount: Scalars['Int']['output'];
|
||||
/** How the error was captured, from the most recent occurrence (e.g. global). */
|
||||
errorSource?: Maybe<Scalars['String']['output']>;
|
||||
eventCount: Scalars['Int']['output'];
|
||||
/** Error message, from the most recent occurrence. */
|
||||
exceptionMessage?: Maybe<Scalars['String']['output']>;
|
||||
/** Error class/type, from the most recent occurrence (e.g. TypeError). */
|
||||
exceptionType?: Maybe<Scalars['String']['output']>;
|
||||
/** Stable grouping key (sha256 over error type + entropy-normalized message). */
|
||||
fingerprint: Scalars['String']['output'];
|
||||
firstSeenAt: Scalars['DateTime']['output'];
|
||||
isFatal: Scalars['Boolean']['output'];
|
||||
lastSeenAt: Scalars['DateTime']['output'];
|
||||
/** Distinct device OS values seen for this group (e.g. Android, iOS). */
|
||||
platforms: Array<Scalars['String']['output']>;
|
||||
/** FATAL if any occurrence in the group was fatal, otherwise ERROR. */
|
||||
severity: AppObserveErrorSeverity;
|
||||
uniqueUserCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveErrorGroupBreakdown = {
|
||||
__typename?: 'AppObserveErrorGroupBreakdown';
|
||||
/** Buckets ordered by count descending. */
|
||||
buckets: Array<AppObserveErrorBreakdownBucket>;
|
||||
/** True when more buckets exist than were returned (top-N truncation). */
|
||||
isTruncated: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
/** Filters for the per-group breakdown. Same release/time/platform filters used across Observe. */
|
||||
export type AppObserveErrorGroupBreakdownInput = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
dimension: AppObserveErrorBreakdownDimension;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
fingerprint: Scalars['String']['input'];
|
||||
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveErrorGroups = {
|
||||
__typename?: 'AppObserveErrorGroups';
|
||||
groups: Array<AppObserveErrorGroup>;
|
||||
/** True when more groups exist than were returned (top-N truncation). */
|
||||
isTruncated: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveErrorGroupsInput = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Restrict to a single group, e.g. to fetch one group's header on the detail page. */
|
||||
fingerprint?: InputMaybe<Scalars['String']['input']>;
|
||||
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
orderBy?: InputMaybe<AppObserveErrorGroupsOrderBy>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
/** Restrict to fatal-only or non-fatal-only errors. */
|
||||
severity?: InputMaybe<AppObserveErrorSeverity>;
|
||||
/** Restrict to a capture source (e.g. global). */
|
||||
source?: InputMaybe<Scalars['String']['input']>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export enum AppObserveErrorGroupsOrderBy {
|
||||
FirstSeen = 'FIRST_SEEN',
|
||||
LastSeen = 'LAST_SEEN',
|
||||
MostFrequent = 'MOST_FREQUENT',
|
||||
MostUsers = 'MOST_USERS'
|
||||
}
|
||||
|
||||
export enum AppObserveErrorSeverity {
|
||||
Error = 'ERROR',
|
||||
Fatal = 'FATAL'
|
||||
}
|
||||
|
||||
export type AppObserveErrorStats = {
|
||||
__typename?: 'AppObserveErrorStats';
|
||||
/** Distinct sessions that hit any exception. */
|
||||
affectedSessions: Scalars['Int']['output'];
|
||||
/** Distinct users that hit any exception. */
|
||||
affectedUsers: Scalars['Int']['output'];
|
||||
/** Fraction (0..1) of active sessions without a fatal error. Denominator spans app_metrics and app_events. */
|
||||
crashFreeSessions: Scalars['Float']['output'];
|
||||
/** Fraction (0..1) of active users without a fatal error. */
|
||||
crashFreeUsers: Scalars['Float']['output'];
|
||||
fatalCount: Scalars['Int']['output'];
|
||||
nonFatalCount: Scalars['Int']['output'];
|
||||
/** Total exception events in range. */
|
||||
totalErrors: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
/** Filters for the error-stats header. Same release/time/platform filters used across Observe. */
|
||||
export type AppObserveErrorStatsInput = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveErrorTimeSeries = {
|
||||
__typename?: 'AppObserveErrorTimeSeries';
|
||||
buckets: Array<AppObserveErrorTimeSeriesBucket>;
|
||||
/** Sum of fatalCount across all buckets. */
|
||||
totalFatalCount: Scalars['Int']['output'];
|
||||
/** Sum of nonFatalCount across all buckets. */
|
||||
totalNonFatalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveErrorTimeSeriesBucket = {
|
||||
__typename?: 'AppObserveErrorTimeSeriesBucket';
|
||||
/** Start of the bucket interval. */
|
||||
bucket: Scalars['DateTime']['output'];
|
||||
fatalCount: Scalars['Int']['output'];
|
||||
nonFatalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
/** Filters for the error time series. Same release/time/platform filters used across Observe, plus the bucket size. */
|
||||
export type AppObserveErrorTimeSeriesInput = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Bucket width in minutes. Defaults to 60. */
|
||||
bucketIntervalMinutes?: InputMaybe<Scalars['Int']['input']>;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Restrict to one error group for the detail page's occurrences-over-time chart. Omit for the overview chart. */
|
||||
fingerprint?: InputMaybe<Scalars['String']['input']>;
|
||||
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveEvent = {
|
||||
__typename?: 'AppObserveEvent';
|
||||
appBuildNumber: Scalars['String']['output'];
|
||||
@@ -2561,8 +2772,11 @@ export type AppObserveEventsConnection = {
|
||||
export type AppObserveEventsFilter = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Filter by the update the device was *running* at event time (the app_update_id column). */
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Filter by the update that was *downloaded* (the expo.update_id tag), as surfaced by the EAS Update Recent updates download drill-down. Distinct from appUpdateId (running update); the two are not interchangeable. */
|
||||
downloadedUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
easClientId?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -3024,6 +3238,16 @@ export enum AppStoreConnectUserRole {
|
||||
Unknown = 'UNKNOWN'
|
||||
}
|
||||
|
||||
/** Whether a project has an App Store Connect connection for its ASC-triggered workflows. */
|
||||
export enum AppStoreConnectWorkflowConnectionStatus {
|
||||
/** An App Store Connect app connection is linked to this project. */
|
||||
HasWorkflowsIsConnected = 'HAS_WORKFLOWS_IS_CONNECTED',
|
||||
/** The project has App Store Connect-triggered workflows but no connection. */
|
||||
HasWorkflowsMissingConnection = 'HAS_WORKFLOWS_MISSING_CONNECTION',
|
||||
/** The project has no workflows with an on.app_store_connect trigger. */
|
||||
NoAppStoreConnectWorkflows = 'NO_APP_STORE_CONNECT_WORKFLOWS'
|
||||
}
|
||||
|
||||
export type AppSubmissionEdge = {
|
||||
__typename?: 'AppSubmissionEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
@@ -3941,6 +4165,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
deployment?: Maybe<Deployment>;
|
||||
developmentClient?: Maybe<Scalars['Boolean']['output']>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
embeddedUpdate?: Maybe<EmbeddedUpdate>;
|
||||
enqueuedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
error?: Maybe<BuildError>;
|
||||
estimatedWaitTimeLeftSeconds?: Maybe<Scalars['Int']['output']>;
|
||||
@@ -4563,6 +4788,11 @@ export type CodeSigningInfoInput = {
|
||||
sig: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CompletePostHogConnectionInput = {
|
||||
code: Scalars['String']['input'];
|
||||
state: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type Concurrencies = {
|
||||
__typename?: 'Concurrencies';
|
||||
android: Scalars['Int']['output'];
|
||||
@@ -5362,6 +5592,7 @@ export type DeploymentsMutationDeleteWorkerDeploymentByIdentifierArgs = {
|
||||
export type DeviceRunSession = {
|
||||
__typename?: 'DeviceRunSession';
|
||||
app: App;
|
||||
artifacts: Array<DeviceRunSessionArtifact>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
finishedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
@@ -5380,6 +5611,29 @@ export type DeviceRunSession = {
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionArtifact = {
|
||||
__typename?: 'DeviceRunSessionArtifact';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
deviceRunSession: DeviceRunSession;
|
||||
downloadUrl: Scalars['String']['output'];
|
||||
fileSizeBytes?: Maybe<Scalars['Float']['output']>;
|
||||
filename: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
metadata?: Maybe<Scalars['JSONObject']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionArtifactQuery = {
|
||||
__typename?: 'DeviceRunSessionArtifactQuery';
|
||||
byId: DeviceRunSessionArtifact;
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionArtifactQueryByIdArgs = {
|
||||
deviceRunSessionArtifactId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionArtifactUploadSession = {
|
||||
__typename?: 'DeviceRunSessionArtifactUploadSession';
|
||||
headers: Scalars['JSONObject']['output'];
|
||||
@@ -6213,13 +6467,20 @@ export type EditUpdateBranchInput = {
|
||||
|
||||
export type EmbeddedUpdate = {
|
||||
__typename?: 'EmbeddedUpdate';
|
||||
build?: Maybe<Build>;
|
||||
channel: Scalars['String']['output'];
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
/** The manifest UUID baked into the binary by expo-updates at build time. */
|
||||
id: Scalars['ID']['output'];
|
||||
launchAsset: EmbeddedUpdateAsset;
|
||||
platform: AppPlatform;
|
||||
runtime?: Maybe<Runtime>;
|
||||
runtimeVersion: Scalars['String']['output'];
|
||||
/**
|
||||
* A short-lived signed URL for downloading this bundle's launch asset. Minted on access,
|
||||
* so only select it when the user is actually downloading.
|
||||
*/
|
||||
signedAssetUrl: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type EmbeddedUpdateAsset = {
|
||||
@@ -8300,9 +8561,24 @@ export type PostHogOrganizationConnection = {
|
||||
|
||||
export type PostHogOrganizationConnectionMutation = {
|
||||
__typename?: 'PostHogOrganizationConnectionMutation';
|
||||
/**
|
||||
* Completes an existing-user connection from the browser callback: exchanges the
|
||||
* authorization code with the stored PKCE verifier and persists the connection.
|
||||
*/
|
||||
completePostHogConnection: PostHogOrganizationConnection;
|
||||
createPostHogAccountRequest: PostHogOrganizationConnection;
|
||||
/** Removes the Expo-side connection only; the PostHog organization is preserved. */
|
||||
deletePostHogOrganizationConnection: Scalars['ID']['output'];
|
||||
/**
|
||||
* Starts a PostHog connection. Returns the connection directly for a new PostHog
|
||||
* user, or a pending browser-auth handoff for an existing PostHog user.
|
||||
*/
|
||||
startPostHogConnection: StartPostHogConnectionResult;
|
||||
};
|
||||
|
||||
|
||||
export type PostHogOrganizationConnectionMutationCompletePostHogConnectionArgs = {
|
||||
input: CompletePostHogConnectionInput;
|
||||
};
|
||||
|
||||
|
||||
@@ -8315,6 +8591,22 @@ export type PostHogOrganizationConnectionMutationDeletePostHogOrganizationConnec
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type PostHogOrganizationConnectionMutationStartPostHogConnectionArgs = {
|
||||
input: CreatePostHogAccountRequestInput;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returned when the account's email already belongs to a PostHog account. The user
|
||||
* must approve the connection in their browser at `url`; the CLI then polls for
|
||||
* completion with `state`.
|
||||
*/
|
||||
export type PostHogPendingConnection = {
|
||||
__typename?: 'PostHogPendingConnection';
|
||||
state: Scalars['ID']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type PostHogProject = {
|
||||
__typename?: 'PostHogProject';
|
||||
app: App;
|
||||
@@ -8818,6 +9110,7 @@ export type RootQuery = {
|
||||
convexIntegration: ConvexIntegrationQuery;
|
||||
/** Top-level query object for querying Deployments. */
|
||||
deployments: DeploymentQuery;
|
||||
deviceRunSessionArtifacts: DeviceRunSessionArtifactQuery;
|
||||
deviceRunSessions: DeviceRunSessionQuery;
|
||||
/** Top-level query object for querying Echo chats. */
|
||||
echoChat: EchoChatQuery;
|
||||
@@ -9349,6 +9642,8 @@ export enum StandardOffer {
|
||||
YearlySub = 'YEARLY_SUB'
|
||||
}
|
||||
|
||||
export type StartPostHogConnectionResult = PostHogOrganizationConnection | PostHogPendingConnection;
|
||||
|
||||
/** Incident for a given component from Expo status page API. */
|
||||
export type StatuspageIncident = {
|
||||
__typename?: 'StatuspageIncident';
|
||||
@@ -10119,6 +10414,14 @@ export type UpdateGitHubRepositorySettingsInput = {
|
||||
baseDirectory: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type UpdateGroup = {
|
||||
__typename?: 'UpdateGroup';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
/** The shared update_group UUID of the member updates. */
|
||||
id: Scalars['ID']['output'];
|
||||
updates: Array<Update>;
|
||||
};
|
||||
|
||||
export type UpdateGroupEdge = {
|
||||
__typename?: 'UpdateGroupEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
@@ -10247,6 +10550,32 @@ export type UpdatesMetricsData = {
|
||||
labels: Array<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type UpdatesTimelineConnection = {
|
||||
__typename?: 'UpdatesTimelineConnection';
|
||||
edges: Array<UpdatesTimelineEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type UpdatesTimelineEdge = {
|
||||
__typename?: 'UpdatesTimelineEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
node: UpdatesTimelineItem;
|
||||
};
|
||||
|
||||
export type UpdatesTimelineFilter = {
|
||||
channel?: InputMaybe<Scalars['String']['input']>;
|
||||
platform?: InputMaybe<AppPlatform>;
|
||||
runtimeVersions?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
types?: InputMaybe<Array<UpdatesTimelineItemType>>;
|
||||
};
|
||||
|
||||
export type UpdatesTimelineItem = EmbeddedUpdate | UpdateGroup;
|
||||
|
||||
export enum UpdatesTimelineItemType {
|
||||
EmbeddedUpdate = 'EMBEDDED_UPDATE',
|
||||
UpdateGroup = 'UPDATE_GROUP'
|
||||
}
|
||||
|
||||
export type UploadEmbeddedUpdateInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
channel: Scalars['String']['input'];
|
||||
@@ -12375,6 +12704,8 @@ export enum WorkflowRunTriggerEventType {
|
||||
AppStoreConnectExternalBetaStateChanged = 'APP_STORE_CONNECT_EXTERNAL_BETA_STATE_CHANGED',
|
||||
EasSubmit = 'EAS_SUBMIT',
|
||||
ExpoLaunch = 'EXPO_LAUNCH',
|
||||
GithubPullRequestBaseRefChanged = 'GITHUB_PULL_REQUEST_BASE_REF_CHANGED',
|
||||
GithubPullRequestEdited = 'GITHUB_PULL_REQUEST_EDITED',
|
||||
GithubPullRequestLabeled = 'GITHUB_PULL_REQUEST_LABELED',
|
||||
GithubPullRequestOpened = 'GITHUB_PULL_REQUEST_OPENED',
|
||||
GithubPullRequestReadyForReview = 'GITHUB_PULL_REQUEST_READY_FOR_REVIEW',
|
||||
@@ -13233,12 +13564,12 @@ export type CreateLocalBuildMutationVariables = Exact<{
|
||||
|
||||
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'PartnerActor', id: string, displayName: string } | { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
|
||||
|
||||
export type CreatePostHogAccountRequestMutationVariables = Exact<{
|
||||
export type StartPostHogConnectionMutationVariables = Exact<{
|
||||
input: CreatePostHogAccountRequestInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreatePostHogAccountRequestMutation = { __typename?: 'RootMutation', posthogOrganizationConnection: { __typename?: 'PostHogOrganizationConnectionMutation', createPostHogAccountRequest: { __typename?: 'PostHogOrganizationConnection', id: string, posthogOrganizationIdentifier: string, posthogOrganizationName: string, posthogRegion: PostHogRegion, createdAt: any, updatedAt: any } } };
|
||||
export type StartPostHogConnectionMutation = { __typename?: 'RootMutation', posthogOrganizationConnection: { __typename?: 'PostHogOrganizationConnectionMutation', startPostHogConnection: { __typename: 'PostHogOrganizationConnection', id: string, posthogOrganizationIdentifier: string, posthogOrganizationName: string, posthogRegion: PostHogRegion, createdAt: any, updatedAt: any } | { __typename: 'PostHogPendingConnection', url: string } } };
|
||||
|
||||
export type SetupPostHogProjectMutationVariables = Exact<{
|
||||
input: SetupPostHogProjectInput;
|
||||
@@ -13424,7 +13755,7 @@ export type AccountUsageForOverageWarningQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountUsageForOverageWarningQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null } | null, usageMetrics: { __typename?: 'AccountUsageMetrics', EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, serviceMetric: EasServiceMetric, value: number, limit: number }> } } } } };
|
||||
export type AccountUsageForOverageWarningQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null } | null, usageMetrics: { __typename?: 'AccountUsageMetrics', EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, totalCost: number, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, serviceMetric: EasServiceMetric, value: number, limit: number }>, overageMetrics: Array<{ __typename?: 'EstimatedOverageAndCost', id: string, value: number }> } } } } };
|
||||
|
||||
export type AccountBillingPeriodQueryVariables = Exact<{
|
||||
accountId: Scalars['String']['input'];
|
||||
|
||||
@@ -5,19 +5,19 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import { CreatePostHogAccountRequestInput, SetupPostHogProjectInput } from '../generated';
|
||||
import {
|
||||
PostHogOrganizationConnectionData,
|
||||
PostHogOrganizationConnectionFragmentNode,
|
||||
PostHogProjectData,
|
||||
PostHogProjectFragmentNode,
|
||||
StartPostHogConnectionResult,
|
||||
} from '../types/PostHogConnection';
|
||||
|
||||
type CreatePostHogAccountRequestMutation = {
|
||||
type StartPostHogConnectionMutation = {
|
||||
posthogOrganizationConnection: {
|
||||
createPostHogAccountRequest: PostHogOrganizationConnectionData;
|
||||
startPostHogConnection: StartPostHogConnectionResult;
|
||||
};
|
||||
};
|
||||
|
||||
type CreatePostHogAccountRequestMutationVariables = {
|
||||
type StartPostHogConnectionMutationVariables = {
|
||||
input: CreatePostHogAccountRequestInput;
|
||||
};
|
||||
|
||||
@@ -42,32 +42,36 @@ type DeletePostHogProjectMutationVariables = {
|
||||
};
|
||||
|
||||
export const PostHogMutation = {
|
||||
async createPostHogAccountRequestAsync(
|
||||
async startPostHogConnectionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: CreatePostHogAccountRequestInput
|
||||
): Promise<PostHogOrganizationConnectionData> {
|
||||
): Promise<StartPostHogConnectionResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
CreatePostHogAccountRequestMutation,
|
||||
CreatePostHogAccountRequestMutationVariables
|
||||
>(
|
||||
.mutation<StartPostHogConnectionMutation, StartPostHogConnectionMutationVariables>(
|
||||
gql`
|
||||
mutation CreatePostHogAccountRequest($input: CreatePostHogAccountRequestInput!) {
|
||||
mutation StartPostHogConnection($input: CreatePostHogAccountRequestInput!) {
|
||||
posthogOrganizationConnection {
|
||||
createPostHogAccountRequest(input: $input) {
|
||||
id
|
||||
...PostHogOrganizationConnectionFragment
|
||||
startPostHogConnection(input: $input) {
|
||||
__typename
|
||||
... on PostHogOrganizationConnection {
|
||||
id
|
||||
...PostHogOrganizationConnectionFragment
|
||||
}
|
||||
... on PostHogPendingConnection {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(PostHogOrganizationConnectionFragmentNode)}
|
||||
`,
|
||||
{ input }
|
||||
{ input },
|
||||
{ additionalTypenames: ['PostHogOrganizationConnection'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.posthogOrganizationConnection.createPostHogAccountRequest;
|
||||
return data.posthogOrganizationConnection.startPostHogConnection;
|
||||
},
|
||||
|
||||
async setupPostHogProjectAsync(
|
||||
|
||||
@@ -198,6 +198,11 @@ export const AccountQuery = {
|
||||
value
|
||||
limit
|
||||
}
|
||||
overageMetrics {
|
||||
id
|
||||
value
|
||||
}
|
||||
totalCost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ type PostHogProjectByAppIdQueryVariables = {
|
||||
export const PostHogQuery = {
|
||||
async getPostHogOrganizationConnectionByAccountIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string
|
||||
accountId: string,
|
||||
{ useCache = true }: { useCache?: boolean } = {}
|
||||
): Promise<PostHogOrganizationConnectionData | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -62,7 +63,10 @@ export const PostHogQuery = {
|
||||
${print(PostHogOrganizationConnectionFragmentNode)}
|
||||
`,
|
||||
{ accountId },
|
||||
{ additionalTypenames: ['PostHogOrganizationConnection'] }
|
||||
{
|
||||
additionalTypenames: ['PostHogOrganizationConnection'],
|
||||
requestPolicy: useCache ? 'cache-first' : 'network-only',
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
@@ -12,6 +12,15 @@ export type PostHogOrganizationConnectionData = Pick<
|
||||
| 'updatedAt'
|
||||
>;
|
||||
|
||||
// `state` is the website callback's correlator; the CLI only needs `url`, so it omits it.
|
||||
export type PostHogPendingConnectionData = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type StartPostHogConnectionResult =
|
||||
| ({ __typename: 'PostHogOrganizationConnection' } & PostHogOrganizationConnectionData)
|
||||
| ({ __typename: 'PostHogPendingConnection' } & PostHogPendingConnectionData);
|
||||
|
||||
export type PostHogProjectData = Pick<
|
||||
PostHogProject,
|
||||
| 'id'
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { testTarget } from '../../../credentials/__tests__/fixtures-ios';
|
||||
import { ApplePlatform } from '../../../credentials/ios/appstore/constants';
|
||||
import { Target } from '../../../credentials/ios/types';
|
||||
import { getApplePlatformFromDeviceFamily, getApplePlatformFromSdkRoot } from '../target';
|
||||
import {
|
||||
getApplePlatformFromDeviceFamily,
|
||||
getApplePlatformFromSdkRoot,
|
||||
getApplePlatformFromTarget,
|
||||
getManagedTvBuildSettings,
|
||||
} from '../target';
|
||||
|
||||
function getApplePlatformWithSdkRoot(sdkRoot: string): ApplePlatform | null {
|
||||
const target = {
|
||||
@@ -56,3 +61,64 @@ describe(getApplePlatformFromDeviceFamily, () => {
|
||||
expect(getApplePlatformWithDeviceFamily('ilovecats')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getManagedTvBuildSettings, () => {
|
||||
const originalExpoTv = process.env.EXPO_TV;
|
||||
afterEach(() => {
|
||||
if (originalExpoTv === undefined) {
|
||||
delete process.env.EXPO_TV;
|
||||
} else {
|
||||
process.env.EXPO_TV = originalExpoTv;
|
||||
}
|
||||
});
|
||||
|
||||
test('returns undefined when EXPO_TV is not set in env or process.env', () => {
|
||||
delete process.env.EXPO_TV;
|
||||
expect(getManagedTvBuildSettings(undefined)).toBeUndefined();
|
||||
expect(getManagedTvBuildSettings({})).toBeUndefined();
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: '' })).toBeUndefined();
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: '0' })).toBeUndefined();
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: 'false' })).toBeUndefined();
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: 'no' })).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns TARGETED_DEVICE_FAMILY=3 when EXPO_TV is truthy in the env parameter (eas.json case)', () => {
|
||||
delete process.env.EXPO_TV;
|
||||
for (const value of ['1', 'true', 'yes', 'TRUE', 'Yes']) {
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: value })).toEqual({
|
||||
TARGETED_DEVICE_FAMILY: '3',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('falls back to process.env when env is missing EXPO_TV (shell case)', () => {
|
||||
process.env.EXPO_TV = '1';
|
||||
expect(getManagedTvBuildSettings(undefined)).toEqual({ TARGETED_DEVICE_FAMILY: '3' });
|
||||
expect(getManagedTvBuildSettings({})).toEqual({ TARGETED_DEVICE_FAMILY: '3' });
|
||||
});
|
||||
|
||||
test('env-parameter EXPO_TV takes precedence over process.env (eas.json overrides shell)', () => {
|
||||
process.env.EXPO_TV = '0';
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: '1' })).toEqual({
|
||||
TARGETED_DEVICE_FAMILY: '3',
|
||||
});
|
||||
|
||||
process.env.EXPO_TV = '1';
|
||||
expect(getManagedTvBuildSettings({ EXPO_TV: '0' })).toBeUndefined();
|
||||
});
|
||||
|
||||
test('feeds into getApplePlatformFromTarget so managed tvOS targets resolve to TV_OS', () => {
|
||||
delete process.env.EXPO_TV;
|
||||
const target: Target = {
|
||||
...testTarget,
|
||||
buildSettings: getManagedTvBuildSettings({ EXPO_TV: '1' }),
|
||||
};
|
||||
expect(getApplePlatformFromTarget(target)).toBe(ApplePlatform.TV_OS);
|
||||
|
||||
const iosTarget: Target = {
|
||||
...testTarget,
|
||||
buildSettings: getManagedTvBuildSettings({}),
|
||||
};
|
||||
expect(getApplePlatformFromTarget(iosTarget)).toBe(ApplePlatform.IOS);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ExportedConfig, IOSConfig, compileModsAsync } from '@expo/config-plugins';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
|
||||
|
||||
@@ -17,7 +18,7 @@ interface Target {
|
||||
|
||||
export async function getManagedApplicationTargetEntitlementsAsync(
|
||||
projectDir: string,
|
||||
env: Record<string, string>,
|
||||
env: Env,
|
||||
vcsClient: Client
|
||||
): Promise<JSONObject> {
|
||||
let expoConfigError: any;
|
||||
@@ -95,7 +96,7 @@ export async function getManagedApplicationTargetEntitlementsAsync(
|
||||
|
||||
async function resolveManagedApplicationTargetEntitlementsWithBundledConfigAsync(
|
||||
projectDir: string,
|
||||
env: Record<string, string>,
|
||||
env: Env,
|
||||
vcsClient: Client
|
||||
): Promise<JSONObject> {
|
||||
const originalProcessEnv = process.env;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig, XcodeProject } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import Joi from 'joi';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
@@ -26,7 +26,7 @@ interface UserDefinedTarget {
|
||||
interface ResolveTargetOptions {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
env?: Record<string, string>;
|
||||
env?: Env;
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
vcsClient: Client;
|
||||
}
|
||||
@@ -40,6 +40,28 @@ const AppExtensionsConfigSchema = Joi.array().items(
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* For managed projects we don't have an Xcode project to read build settings
|
||||
* from, but the @react-native-tvos/config-tv plugin signals a tvOS build via
|
||||
* the EXPO_TV environment variable. That variable can come from the build
|
||||
* profile's `env` in eas.json (the typical case, which flows in via `env`),
|
||||
* or from the shell process when the user runs `EXPO_TV=1 eas build ...`
|
||||
* (which is NOT included in the merged `env` upstream, so we read
|
||||
* `process.env` directly as a fallback). When set, synthesize
|
||||
* TARGETED_DEVICE_FAMILY=3 so getApplePlatformFromTarget (and every
|
||||
* Apple-portal flow downstream) picks TV_OS for these targets instead of
|
||||
* defaulting to IOS.
|
||||
*/
|
||||
export function getManagedTvBuildSettings(
|
||||
env: Env | undefined
|
||||
): Target['buildSettings'] | undefined {
|
||||
const expoTv = (env?.EXPO_TV ?? process.env.EXPO_TV ?? '').toString().toLowerCase();
|
||||
if (expoTv === '1' || expoTv === 'true' || expoTv === 'yes') {
|
||||
return { TARGETED_DEVICE_FAMILY: '3' };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function resolveManagedProjectTargetsAsync({
|
||||
exp,
|
||||
projectDir,
|
||||
@@ -76,12 +98,14 @@ export async function resolveManagedProjectTargetsAsync({
|
||||
);
|
||||
}
|
||||
|
||||
const managedBuildSettings = getManagedTvBuildSettings(env);
|
||||
const extensionsTargets: Target[] = appExtensions.map(extension => ({
|
||||
targetName: extension.targetName,
|
||||
buildConfiguration,
|
||||
bundleIdentifier: extension.bundleIdentifier,
|
||||
parentBundleIdentifier: extension.parentBundleIdentifier ?? applicationTargetBundleIdentifier,
|
||||
entitlements: extension.entitlements ?? {},
|
||||
...(managedBuildSettings && { buildSettings: managedBuildSettings }),
|
||||
}));
|
||||
return [
|
||||
{
|
||||
@@ -89,6 +113,7 @@ export async function resolveManagedProjectTargetsAsync({
|
||||
bundleIdentifier: applicationTargetBundleIdentifier,
|
||||
buildConfiguration,
|
||||
entitlements: applicationTargetEntitlements,
|
||||
...(managedBuildSettings && { buildSettings: managedBuildSettings }),
|
||||
},
|
||||
...extensionsTargets,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Env, Platform } from '@expo/eas-build-job';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface SubmitArchiveFlags {
|
||||
export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
credentialsCtx?: CredentialsContext;
|
||||
env?: Record<string, string>;
|
||||
env?: Env;
|
||||
nonInteractive: boolean;
|
||||
isVerboseFastlaneEnabled: boolean;
|
||||
groups: string[] | undefined;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { AccountQuery } from '../../../graphql/queries/AccountQuery';
|
||||
import Log, { link } from '../../../log';
|
||||
import {
|
||||
classifyUsageTier,
|
||||
createProgressBar,
|
||||
displayOverageWarning,
|
||||
maybeWarnAboutUsageOveragesAsync,
|
||||
@@ -44,11 +45,15 @@ function createMockAccountUsage({
|
||||
name = 'test-account',
|
||||
subscriptionName = 'Free',
|
||||
buildPlanMetrics = [],
|
||||
overageMetrics = [],
|
||||
totalCost = 0,
|
||||
}: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
subscriptionName?: string;
|
||||
buildPlanMetrics?: EstimatedUsage[];
|
||||
overageMetrics?: { __typename: 'EstimatedOverageAndCost'; id: string; value: number }[];
|
||||
totalCost?: number;
|
||||
} = {}): AccountUsageForOverageWarningQuery['account']['byId'] {
|
||||
return {
|
||||
__typename: 'Account',
|
||||
@@ -64,6 +69,8 @@ function createMockAccountUsage({
|
||||
__typename: 'UsageMetricTotal',
|
||||
id: 'metric-id',
|
||||
planMetrics: buildPlanMetrics,
|
||||
overageMetrics,
|
||||
totalCost,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -83,7 +90,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
mockDebug.mockClear();
|
||||
});
|
||||
|
||||
it('displays a warning for a Free plan with high build usage', async () => {
|
||||
it('displays an approaching-tier warning for a Free plan at 80%+ usage', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 85 })],
|
||||
@@ -102,14 +109,14 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
);
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 85% of your included build credits for this month.")
|
||||
expect.stringContaining("You've used 85% of your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a warning for a Starter plan with high build usage', async () => {
|
||||
it('displays an approaching-tier warning for a paid plan at 80%+ usage', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Starter',
|
||||
@@ -123,7 +130,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 90% of your included build credits for this month.")
|
||||
expect.stringContaining("You've used 90% of your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -132,6 +139,94 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an at-tier warning for a Free plan that has hit the limit', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 100 })],
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've reached your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('New builds are blocked until your billing period resets.')
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an at-tier warning for a paid plan that has hit the limit with no overage', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Starter',
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 100 })],
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've reached your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Additional builds will be charged at pay-as-you-go rates.')
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the at-tier message for a Free plan even if overage is reported (defensive fallback)', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 105 })],
|
||||
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 5 }],
|
||||
totalCost: 0,
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've reached your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('New builds are blocked until your billing period resets.')
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an over-tier warning for a paid plan with overage and cost', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Starter',
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 110 })],
|
||||
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 10 }],
|
||||
totalCost: 1500,
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"You've used 10 builds beyond your included credits this billing period ($15.00 in overages so far)."
|
||||
)
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Additional builds continue at pay-as-you-go rates.')
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display a warning when usage is below threshold', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
@@ -171,6 +266,30 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyUsageTier', () => {
|
||||
it('returns null when usage is below threshold', () => {
|
||||
expect(classifyUsageTier({ planValue: 50, limit: 100, overageCount: 0 })).toBeNull();
|
||||
expect(classifyUsageTier({ planValue: 79, limit: 100, overageCount: 0 })).toBeNull();
|
||||
});
|
||||
|
||||
it('returns "approaching" at >= 80% but below limit', () => {
|
||||
expect(classifyUsageTier({ planValue: 80, limit: 100, overageCount: 0 })).toBe('approaching');
|
||||
expect(classifyUsageTier({ planValue: 99, limit: 100, overageCount: 0 })).toBe('approaching');
|
||||
});
|
||||
|
||||
it('returns "at" when planValue >= limit and no overage', () => {
|
||||
expect(classifyUsageTier({ planValue: 100, limit: 100, overageCount: 0 })).toBe('at');
|
||||
});
|
||||
|
||||
it('returns "over" when any overage has been counted', () => {
|
||||
expect(classifyUsageTier({ planValue: 105, limit: 100, overageCount: 5 })).toBe('over');
|
||||
});
|
||||
|
||||
it('prefers "over" if overage exists, regardless of planValue', () => {
|
||||
expect(classifyUsageTier({ planValue: 50, limit: 100, overageCount: 1 })).toBe('over');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProgressBar', () => {
|
||||
it('creates a progress bar with correct fill for a given percentage', () => {
|
||||
const bar = createProgressBar(85, 20);
|
||||
@@ -202,16 +321,20 @@ describe('displayOverageWarning', () => {
|
||||
mockLink.mockClear();
|
||||
});
|
||||
|
||||
it('displays a warning for a Free plan', () => {
|
||||
it('approaching tier: displays a warning for a Free plan with progress bar', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: true,
|
||||
tier: 'approaching',
|
||||
name: 'test-account',
|
||||
hasFreePlan: true,
|
||||
planValue: 85,
|
||||
limit: 100,
|
||||
overageCount: 0,
|
||||
overageCostCents: 0,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledTimes(2);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 85% of your included build credits for this month.")
|
||||
expect.stringContaining("You've used 85% of your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
|
||||
@@ -219,18 +342,23 @@ describe('displayOverageWarning', () => {
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Upgrade your plan to continue service.')
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(expect.stringMatching(/█+░+/));
|
||||
});
|
||||
|
||||
it('displays a warning for a paid plan', () => {
|
||||
it('approaching tier: displays a warning for a paid plan with progress bar', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: false,
|
||||
tier: 'approaching',
|
||||
name: 'test-account',
|
||||
hasFreePlan: false,
|
||||
planValue: 85,
|
||||
limit: 100,
|
||||
overageCount: 0,
|
||||
overageCostCents: 0,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledTimes(2);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 85% of your included build credits for this month.")
|
||||
expect.stringContaining("You've used 85% of your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -238,16 +366,113 @@ describe('displayOverageWarning', () => {
|
||||
)
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('See usage in billing.'));
|
||||
});
|
||||
|
||||
it('at tier: displays a Free-plan limit-reached message with present-tense block wording', () => {
|
||||
displayOverageWarning({
|
||||
tier: 'at',
|
||||
name: 'test-account',
|
||||
hasFreePlan: true,
|
||||
planValue: 100,
|
||||
limit: 100,
|
||||
overageCount: 0,
|
||||
overageCostCents: 0,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('██████████████████████████░░░░')
|
||||
expect.stringContaining("You've reached your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('New builds are blocked until your billing period resets.')
|
||||
);
|
||||
});
|
||||
|
||||
it('at tier: displays a paid-plan limit-reached message with pay-as-you-go warning', () => {
|
||||
displayOverageWarning({
|
||||
tier: 'at',
|
||||
name: 'test-account',
|
||||
hasFreePlan: false,
|
||||
planValue: 100,
|
||||
limit: 100,
|
||||
overageCount: 0,
|
||||
overageCostCents: 0,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've reached your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Additional builds will be charged at pay-as-you-go rates.')
|
||||
);
|
||||
});
|
||||
|
||||
it('over tier on Free plan: defensively falls back to the at-tier blocked message', () => {
|
||||
displayOverageWarning({
|
||||
tier: 'over',
|
||||
name: 'test-account',
|
||||
hasFreePlan: true,
|
||||
planValue: 105,
|
||||
limit: 100,
|
||||
overageCount: 5,
|
||||
overageCostCents: 0,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've reached your included build credits this billing period.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('New builds are blocked until your billing period resets.')
|
||||
);
|
||||
});
|
||||
|
||||
it('over tier: displays a paid-plan overage count and accrued cost', () => {
|
||||
displayOverageWarning({
|
||||
tier: 'over',
|
||||
name: 'test-account',
|
||||
hasFreePlan: false,
|
||||
planValue: 110,
|
||||
limit: 100,
|
||||
overageCount: 10,
|
||||
overageCostCents: 1500,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"You've used 10 builds beyond your included credits this billing period ($15.00 in overages so far)."
|
||||
)
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Additional builds continue at pay-as-you-go rates.')
|
||||
);
|
||||
});
|
||||
|
||||
it('over tier: paid plan with a single overage build uses singular wording', () => {
|
||||
displayOverageWarning({
|
||||
tier: 'over',
|
||||
name: 'test-account',
|
||||
hasFreePlan: false,
|
||||
planValue: 101,
|
||||
limit: 100,
|
||||
overageCount: 1,
|
||||
overageCostCents: 150,
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"You've used 1 build beyond your included credits this billing period ($1.50 in overages so far)."
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('includes correct account name in billing URL', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: true,
|
||||
tier: 'approaching',
|
||||
name: 'my-custom-account',
|
||||
hasFreePlan: true,
|
||||
planValue: 85,
|
||||
limit: 100,
|
||||
overageCount: 0,
|
||||
overageCostCents: 0,
|
||||
});
|
||||
|
||||
expect(mockLink).toHaveBeenCalledWith(
|
||||
@@ -255,27 +480,4 @@ describe('displayOverageWarning', () => {
|
||||
expect.objectContaining({ text: 'Upgrade your plan to continue service.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('shows progress bar when usage is under 100%', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: true,
|
||||
name: 'test-account',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(expect.stringMatching(/█+░+/));
|
||||
});
|
||||
|
||||
it('does not show progress bar when usage is at 100%', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 100,
|
||||
hasFreePlan: true,
|
||||
name: 'test-account',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 100% of your included build credits for this month.")
|
||||
);
|
||||
expect(mockWarn).not.toHaveBeenCalledWith(expect.stringMatching(/[█░]/));
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,9 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
|
||||
import { AccountQuery } from '../../graphql/queries/AccountQuery';
|
||||
import Log, { link } from '../../log';
|
||||
|
||||
const THRESHOLD_PERCENT = 85;
|
||||
const APPROACHING_THRESHOLD_PERCENT = 80;
|
||||
|
||||
export type UsageTier = 'approaching' | 'at' | 'over';
|
||||
|
||||
export async function maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient,
|
||||
@@ -15,28 +17,71 @@ export async function maybeWarnAboutUsageOveragesAsync({
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const currentDate = new Date();
|
||||
const {
|
||||
name,
|
||||
subscription,
|
||||
usageMetrics: { EAS_BUILD },
|
||||
} = await AccountQuery.getUsageForOverageWarningAsync(graphqlClient, accountId, currentDate);
|
||||
const account = await AccountQuery.getUsageForOverageWarningAsync(
|
||||
graphqlClient,
|
||||
accountId,
|
||||
currentDate
|
||||
);
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const planMetric = EAS_BUILD?.planMetrics?.[0];
|
||||
const { name, subscription, usageMetrics } = account;
|
||||
const buildMetrics = usageMetrics.EAS_BUILD;
|
||||
const planMetric = buildMetrics?.planMetrics?.[0];
|
||||
if (!planMetric || !subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const percentUsed = calculatePercentUsed(planMetric.value, planMetric.limit);
|
||||
if (percentUsed >= THRESHOLD_PERCENT) {
|
||||
const hasFreePlan = subscription.name === 'Free';
|
||||
displayOverageWarning({ percentUsed, hasFreePlan, name });
|
||||
const overageCount = buildMetrics.overageMetrics.reduce((sum, o) => sum + o.value, 0);
|
||||
const overageCostCents = buildMetrics.totalCost;
|
||||
const tier = classifyUsageTier({
|
||||
planValue: planMetric.value,
|
||||
limit: planMetric.limit,
|
||||
overageCount,
|
||||
});
|
||||
if (!tier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasFreePlan = subscription.name === 'Free';
|
||||
displayOverageWarning({
|
||||
tier,
|
||||
name,
|
||||
hasFreePlan,
|
||||
planValue: planMetric.value,
|
||||
limit: planMetric.limit,
|
||||
overageCount,
|
||||
overageCostCents,
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail if we can't fetch usage data - we don't want to block the user's workflow
|
||||
Log.debug(`Failed to fetch usage data: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function classifyUsageTier({
|
||||
planValue,
|
||||
limit,
|
||||
overageCount,
|
||||
}: {
|
||||
planValue: number;
|
||||
limit: number;
|
||||
overageCount: number;
|
||||
}): UsageTier | null {
|
||||
if (overageCount > 0) {
|
||||
return 'over';
|
||||
}
|
||||
if (limit > 0 && planValue >= limit) {
|
||||
return 'at';
|
||||
}
|
||||
const percentUsed = calculatePercentUsed(planValue, limit);
|
||||
if (percentUsed >= APPROACHING_THRESHOLD_PERCENT) {
|
||||
return 'approaching';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function calculatePercentUsed(value: number, limit: number): number {
|
||||
if (limit === 0) {
|
||||
return 0;
|
||||
@@ -52,31 +97,70 @@ export function createProgressBar(percentUsed: number, width: number = 30): stri
|
||||
return `${filled}${empty}`;
|
||||
}
|
||||
|
||||
function formatCents(cents: number): string {
|
||||
return `$${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
export function displayOverageWarning({
|
||||
percentUsed,
|
||||
tier,
|
||||
name,
|
||||
hasFreePlan,
|
||||
name,
|
||||
planValue,
|
||||
limit,
|
||||
overageCount,
|
||||
overageCostCents,
|
||||
}: {
|
||||
percentUsed: number;
|
||||
tier: UsageTier;
|
||||
name: string;
|
||||
hasFreePlan: boolean;
|
||||
name: string;
|
||||
planValue: number;
|
||||
limit: number;
|
||||
overageCount: number;
|
||||
overageCostCents: number;
|
||||
}): void {
|
||||
const message = chalk.bold(
|
||||
`You've used ${percentUsed}% of your included build credits for this month.`
|
||||
);
|
||||
// Don't show progress bar at 100% - it's redundant when the limit is reached
|
||||
const progressBar = percentUsed < 100 ? ' ' + createProgressBar(percentUsed) : '';
|
||||
Log.warn(message + progressBar);
|
||||
const billingUrl = `https://expo.dev/accounts/${name}/settings/billing`;
|
||||
|
||||
if (tier === 'approaching') {
|
||||
const percentUsed = calculatePercentUsed(planValue, limit);
|
||||
Log.warn(
|
||||
chalk.bold(
|
||||
`You've used ${percentUsed}% of your included build credits this billing period.`
|
||||
) +
|
||||
' ' +
|
||||
createProgressBar(percentUsed)
|
||||
);
|
||||
Log.warn(
|
||||
hasFreePlan
|
||||
? "You won't be able to start new builds once you reach the limit. " +
|
||||
link(billingUrl, { text: 'Upgrade your plan to continue service.', dim: false })
|
||||
: 'Additional usage beyond your limit will be charged at pay-as-you-go rates. ' +
|
||||
link(billingUrl, { text: 'See usage in billing.', dim: false })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const billingUrl = `https://expo.dev/accounts/${name}/settings/billing`;
|
||||
const warning = hasFreePlan
|
||||
? "You won't be able to start new builds once you reach the limit. " +
|
||||
link(billingUrl, { text: 'Upgrade your plan to continue service.', dim: false })
|
||||
: 'Additional usage beyond your limit will be charged at pay-as-you-go rates. ' +
|
||||
link(billingUrl, {
|
||||
text: 'See usage in billing.',
|
||||
dim: false,
|
||||
});
|
||||
// Free users are blocked at the limit, so they can't reach an "over" state.
|
||||
// If we ever see Free + over (data inconsistency, mid-flight plan change), treat it as "at".
|
||||
if (tier === 'at' || (tier === 'over' && hasFreePlan)) {
|
||||
Log.warn(chalk.bold("You've reached your included build credits this billing period."));
|
||||
Log.warn(
|
||||
hasFreePlan
|
||||
? 'New builds are blocked until your billing period resets. ' +
|
||||
link(billingUrl, { text: 'Upgrade your plan to continue building.', dim: false })
|
||||
: 'Additional builds will be charged at pay-as-you-go rates. ' +
|
||||
link(billingUrl, { text: 'See usage in billing.', dim: false })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warn(warning);
|
||||
// tier === 'over' && paid plan
|
||||
Log.warn(
|
||||
chalk.bold(
|
||||
`You've used ${overageCount} build${overageCount === 1 ? '' : 's'} beyond your included credits this billing period (${formatCents(overageCostCents)} in overages so far).`
|
||||
)
|
||||
);
|
||||
Log.warn(
|
||||
'Additional builds continue at pay-as-you-go rates. ' +
|
||||
link(billingUrl, { text: 'See usage in billing.', dim: false })
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "20.3.0",
|
||||
"version": "20.4.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": "20.3.0",
|
||||
"@expo/build-tools": "20.4.0",
|
||||
"@expo/eas-build-job": "20.1.0",
|
||||
"@expo/logger": "20.0.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "20.3.0",
|
||||
"version": "20.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "20.3.0",
|
||||
"@expo/build-tools": "20.4.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "20.0.0",
|
||||
|
||||
@@ -268,8 +268,8 @@ describe(getBuildEnv.name, () => {
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.NPM_CACHE_URL).toBe('https://npm.example');
|
||||
expect(env.NPM_CONFIG_REGISTRY).toBe('https://npm.example');
|
||||
expect(env.NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.NPM_CONFIG_REGISTRY).toBeUndefined();
|
||||
expect(env.EAS_BUILD_NPM_CACHE_URL).toBe('https://npm.example');
|
||||
expect(env.NVM_NODEJS_ORG_MIRROR).toBe('https://node.example');
|
||||
expect(env.EAS_BUILD_MAVEN_CACHE_URL).toBe('https://maven.example');
|
||||
|
||||
@@ -70,24 +70,30 @@ function printImageDescription(ctx: BuildContext<Job>): void {
|
||||
function printEnvs(ctx: BuildContext<Job>): void {
|
||||
const { logger, job } = ctx;
|
||||
const publicEnv: Record<string, string> = {};
|
||||
const secretEnv: Record<string, string> = {};
|
||||
// We don't expect environment secrets to be missing from ctx.env,
|
||||
// but if they do we want to see it in logs as "=undefined".
|
||||
const secretEnv: Record<string, string | undefined> = {};
|
||||
const instanceEnv: Record<string, string> = {};
|
||||
|
||||
// skip development and testing to avoid leaking local credentials from envs to bucket
|
||||
if (config.env !== Environment.DEVELOPMENT && config.env !== Environment.TEST) {
|
||||
Object.entries(ctx.env).forEach(([key, value]) => {
|
||||
instanceEnv[key] = value;
|
||||
if (value !== undefined) {
|
||||
instanceEnv[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
Object.entries(job.builderEnvironment?.env ?? ({} as Record<string, string>)).forEach(
|
||||
([key, value]) => {
|
||||
Object.entries(job.builderEnvironment?.env ?? {}).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
publicEnv[key] = value;
|
||||
delete instanceEnv[key];
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
job.secrets?.environmentSecrets?.forEach(({ name, type }) => {
|
||||
if (type === EnvironmentSecretType.FILE) {
|
||||
// ctx.env[name] for a FILE-typed environment secret
|
||||
// should be a path to the file.
|
||||
secretEnv[name] = ctx.env[name];
|
||||
} else {
|
||||
secretEnv[name] = '********';
|
||||
|
||||
@@ -31,18 +31,16 @@ export function getBuildEnv({
|
||||
// https://github.com/mobile-dev-inc/maestro/issues/1257#issuecomment-1654803779
|
||||
setEnv(env, 'MAESTRO_DRIVER_STARTUP_TIMEOUT', '120000');
|
||||
setEnv(env, 'MAESTRO_CLI_NO_ANALYTICS', '1');
|
||||
setEnv(env, 'HOMEBREW_NO_AUTO_UPDATE', '1');
|
||||
setEnv(env, 'EAS_BUILD', 'true');
|
||||
setEnv(env, 'EAS_BUILD_RUNNER', 'eas-build');
|
||||
setEnv(env, 'EAS_BUILD_PLATFORM', job.platform);
|
||||
setEnv(env, 'EAS_CLI_SENTRY_DSN', config.sentry.dsn);
|
||||
// NPM_CACHE_URL is deprecated
|
||||
const npmCacheUrl = RuntimeSettings.getNpmCacheUrl();
|
||||
const nodeJsCacheUrl = RuntimeSettings.getNodeJsCacheUrl();
|
||||
const mavenCacheUrl = RuntimeSettings.getMavenCacheUrl();
|
||||
const cocoapodsCacheUrl = RuntimeSettings.getCocoapodsCacheUrl();
|
||||
|
||||
setEnv(env, 'NPM_CACHE_URL', npmCacheUrl);
|
||||
setEnv(env, 'NPM_CONFIG_REGISTRY', npmCacheUrl);
|
||||
setEnv(env, 'NVM_NODEJS_ORG_MIRROR', nodeJsCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_NPM_CACHE_URL', npmCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_PROFILE', metadata.buildProfile);
|
||||
|
||||
@@ -2847,16 +2847,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/apple-utils@npm:2.1.19":
|
||||
version: 2.1.19
|
||||
resolution: "@expo/apple-utils@npm:2.1.19"
|
||||
"@expo/apple-utils@npm:2.1.22":
|
||||
version: 2.1.22
|
||||
resolution: "@expo/apple-utils@npm:2.1.22"
|
||||
bin:
|
||||
apple-utils: bin.js
|
||||
checksum: 10c0/42eb1038bd0906d53b37d830499c7a8ee7ec9f9b9ef4078ff898ca84cb5af1e6ac39d3a392818825151aa2ea237a44698f539b973936733350f314efff020858
|
||||
checksum: 10c0/19a5d21f80cde11e7502d7380b17bb4634568dcd243eb72ce741277dd71aab2b20168e44060143b10bfa01f0b03a6b8a2628b3cf3e5d697b12cf7f78a16d469a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:20.3.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:20.4.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
@@ -3558,7 +3558,7 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:20.3.0"
|
||||
"@expo/build-tools": "npm:20.4.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:20.0.0"
|
||||
@@ -11100,7 +11100,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:20.3.0"
|
||||
"@expo/build-tools": "npm:20.4.0"
|
||||
"@expo/eas-build-job": "npm:20.1.0"
|
||||
"@expo/logger": "npm:20.0.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
@@ -11154,7 +11154,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli@workspace:packages/eas-cli"
|
||||
dependencies:
|
||||
"@expo/apple-utils": "npm:2.1.19"
|
||||
"@expo/apple-utils": "npm:2.1.22"
|
||||
"@expo/code-signing-certificates": "npm:0.0.5"
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
|
||||
Reference in new issue
Block a user