mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fdf47aa7d | ||
|
|
2279b650d5 | ||
|
|
c6a36e2e47 | ||
|
|
27cb8c12d6 | ||
|
|
1122a7f374 | ||
|
|
c06d18138c | ||
|
|
3f4ff1dc3e | ||
|
|
e852ab1cb7 | ||
|
|
d941398ce6 | ||
|
|
bae7684996 | ||
|
|
714aec69c7 | ||
|
|
bec6e670de |
No files matched your search
@@ -8,10 +8,17 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `eas integrations:asc` commands to manage App Store Connect integrations for EAS projects. ([#3558](https://github.com/expo/eas-cli/pull/3558) by [@sswrk](https://github.com/sswrk))
|
||||
- [eas-cli] Allow `eas build:download` to accept a build ID. ([#3655](https://github.com/expo/eas-cli/pull/3655) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [steps] Fix workflow job context interpolation when a string contains multiple `${{ }}` expressions on the same line (for example a changelog built from two context values). ([#3644](https://github.com/expo/eas-cli/pull/3644) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.8.1](https://github.com/expo/eas-cli/releases/tag/v18.8.1) - 2026-04-23
|
||||
|
||||
## [18.8.0](https://github.com/expo/eas-cli/releases/tag/v18.8.0) - 2026-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.8.0",
|
||||
"@expo/steps": "18.9.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
|
||||
@@ -38,6 +38,7 @@ import { runGradleFunction } from './functions/runGradle';
|
||||
import { createSaveBuildCacheFunction } from './functions/saveBuildCache';
|
||||
import { createSaveCacheFunction } from './functions/saveCache';
|
||||
import { createSendSlackMessageFunction } from './functions/sendSlackMessage';
|
||||
import { createStartAgentDeviceRemoteSessionBuildFunction } from './functions/startAgentDeviceRemoteSession';
|
||||
import { createStartAndroidEmulatorBuildFunction } from './functions/startAndroidEmulator';
|
||||
import { createStartCuttlefishDeviceBuildFunction } from './functions/startCuttlefishDevice';
|
||||
import { createStartIosSimulatorBuildFunction } from './functions/startIosSimulator';
|
||||
@@ -77,6 +78,7 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
generateGymfileFromTemplateFunction(),
|
||||
runFastlaneFunction(),
|
||||
parseXcactivitylogFunction(),
|
||||
createStartAgentDeviceRemoteSessionBuildFunction(),
|
||||
createStartAndroidEmulatorBuildFunction(),
|
||||
createStartCuttlefishDeviceBuildFunction(),
|
||||
createStartIosSimulatorBuildFunction(),
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import childProcess from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { sleepAsync } from '../../utils/retry';
|
||||
|
||||
const AGENT_DEVICE_REPO_URL = 'https://github.com/callstackincubator/agent-device.git';
|
||||
const SRC_DIR = '/tmp/agent-device-src';
|
||||
const RUN_DIR = '/tmp/agent-device';
|
||||
const DAEMON_LOG = path.join(RUN_DIR, 'daemon.log');
|
||||
const TUNNEL_LOG = path.join(RUN_DIR, 'cloudflared.log');
|
||||
const DAEMON_JSON_PATH = path.join(os.homedir(), '.agent-device', 'daemon.json');
|
||||
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
||||
const STARTUP_TIMEOUT_MS = 60_000;
|
||||
|
||||
export function createStartAgentDeviceRemoteSessionBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: 'start_agent_device_remote_session',
|
||||
name: 'Start agent device remote session',
|
||||
__metricsId: 'eas/start_agent_device_remote_session',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
id: 'package_version',
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
],
|
||||
fn: async ({ logger }, { inputs, env }) => {
|
||||
const packageVersion = inputs.package_version.value as string | undefined;
|
||||
logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}).`);
|
||||
|
||||
logger.info(`Preparing runtime directory at ${RUN_DIR}.`);
|
||||
await fs.promises.mkdir(RUN_DIR, { recursive: true });
|
||||
|
||||
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
||||
await spawn('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
||||
|
||||
logger.info('Ensuring cloudflared is installed.');
|
||||
await ensureCloudflaredInstalledAsync({ env, logger });
|
||||
|
||||
logger.info('Ensuring bun is installed.');
|
||||
await ensureBunInstalledAsync({ env, logger });
|
||||
|
||||
logger.info(
|
||||
packageVersion
|
||||
? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
|
||||
: `Cloning agent-device (latest) into ${SRC_DIR}.`
|
||||
);
|
||||
await cloneAgentDeviceAsync({ packageVersion, env, logger });
|
||||
|
||||
logger.info('Installing agent-device dependencies.');
|
||||
await spawn('bun', ['install', '--production'], {
|
||||
cwd: SRC_DIR,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info(`Launching agent-device daemon (log file: ${DAEMON_LOG}).`);
|
||||
await spawnDetachedAsync({
|
||||
command: 'bun run src/daemon.ts',
|
||||
cwd: SRC_DIR,
|
||||
logFile: DAEMON_LOG,
|
||||
env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info(`Waiting for daemon credentials at ${DAEMON_JSON_PATH}.`);
|
||||
await waitForFileAsync({
|
||||
filePath: DAEMON_JSON_PATH,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'agent-device daemon',
|
||||
});
|
||||
const { port: daemonPort, token: daemonToken } = readDaemonInfo(DAEMON_JSON_PATH);
|
||||
logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
|
||||
|
||||
logger.info(
|
||||
`Starting cloudflared tunnel to http://localhost:${daemonPort} (log file: ${TUNNEL_LOG}).`
|
||||
);
|
||||
await spawnDetachedAsync({
|
||||
command: `cloudflared tunnel --url "http://localhost:${daemonPort}"`,
|
||||
logFile: TUNNEL_LOG,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info('Waiting for a public tunnel URL.');
|
||||
const tunnelUrl = await waitForMatchInLogAsync({
|
||||
logFile: TUNNEL_LOG,
|
||||
pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'cloudflared tunnel',
|
||||
});
|
||||
logger.info(`Tunnel is ready at ${tunnelUrl}.`);
|
||||
|
||||
logger.info('Emitting agent-device credentials for the CLI to pick up:');
|
||||
logger.info(`export AGENT_DEVICE_DAEMON_BASE_URL="${tunnelUrl}"`);
|
||||
logger.info(`export AGENT_DEVICE_DAEMON_AUTH_TOKEN="${daemonToken}"`);
|
||||
|
||||
logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
|
||||
// Keep the turtle job alive so the daemon and tunnel stay reachable
|
||||
// until stopDeviceRunSession cancels the run.
|
||||
await new Promise<never>(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureCloudflaredInstalledAsync({
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
|
||||
}
|
||||
|
||||
async function ensureBunInstalledAsync({
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
await ensureBrewPackageInstalledAsync({ name: 'bun', env, logger });
|
||||
}
|
||||
|
||||
async function ensureBrewPackageInstalledAsync({
|
||||
name,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
name: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
await spawn(
|
||||
'bash',
|
||||
['-c', `command -v ${name} >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 brew install ${name}`],
|
||||
{ env, logger }
|
||||
);
|
||||
}
|
||||
|
||||
async function cloneAgentDeviceAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
packageVersion: string | undefined;
|
||||
env: NodeJS.ProcessEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
const branchArgs = packageVersion ? ['--branch', `v${packageVersion}`] : [];
|
||||
await spawn('git', ['clone', '--depth', '1', ...branchArgs, AGENT_DEVICE_REPO_URL, SRC_DIR], {
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
async function spawnDetachedAsync({
|
||||
command,
|
||||
cwd,
|
||||
logFile,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
command: string;
|
||||
cwd?: string;
|
||||
logFile: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
// Launch the process fully detached so this function returns immediately and
|
||||
// the grandchild survives the step. Stdio goes to a log file so the daemon
|
||||
// output can be polled, and we unref so Node doesn't wait on it.
|
||||
const fd = fs.openSync(logFile, 'a');
|
||||
try {
|
||||
const child = childProcess.spawn('bash', ['-c', command], {
|
||||
cwd,
|
||||
env,
|
||||
detached: true,
|
||||
stdio: ['ignore', fd, fd],
|
||||
});
|
||||
if (!child.pid) {
|
||||
throw new Error(`Failed to spawn detached process: ${command}`);
|
||||
}
|
||||
child.unref();
|
||||
logger.info(`Started detached process (pid ${child.pid}).`);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForMatchInLogAsync({
|
||||
logFile,
|
||||
pattern,
|
||||
timeoutMs,
|
||||
description,
|
||||
}: {
|
||||
logFile: string;
|
||||
pattern: RegExp;
|
||||
timeoutMs: number;
|
||||
description: string;
|
||||
}): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const content = await readFileOrEmptyAsync(logFile);
|
||||
const match = pattern.exec(content);
|
||||
if (match) {
|
||||
return match[1] ?? match[0];
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
const tail = await readFileOrEmptyAsync(logFile);
|
||||
throw new Error(
|
||||
`Timed out waiting for ${description} to start. Last log contents:\n${tail || '<empty>'}`
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForFileAsync({
|
||||
filePath,
|
||||
timeoutMs,
|
||||
description,
|
||||
}: {
|
||||
filePath: string;
|
||||
timeoutMs: number;
|
||||
description: string;
|
||||
}): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
await fs.promises.access(filePath);
|
||||
return;
|
||||
} catch {
|
||||
// not yet; keep polling
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${description} to write ${filePath}.`);
|
||||
}
|
||||
|
||||
async function readFileOrEmptyAsync(filePath: string): Promise<string> {
|
||||
try {
|
||||
return await fs.promises.readFile(filePath, 'utf8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function readDaemonInfo(filePath: string): { port: number; token: string } {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== 'object' ||
|
||||
typeof (parsed as { httpPort: unknown }).httpPort !== 'number' ||
|
||||
typeof (parsed as { token: unknown }).token !== 'string'
|
||||
) {
|
||||
throw new Error(`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`);
|
||||
}
|
||||
const { httpPort, token } = parsed as { httpPort: number; token: string };
|
||||
return { port: httpPort, token };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.8.0"
|
||||
"@expo/steps": "18.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
+162
-94
@@ -117,6 +117,9 @@ eas --help COMMAND
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas init`](#eas-init)
|
||||
* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory)
|
||||
* [`eas integrations:asc:connect`](#eas-integrationsascconnect)
|
||||
* [`eas integrations:asc:disconnect`](#eas-integrationsascdisconnect)
|
||||
* [`eas integrations:asc:status`](#eas-integrationsascstatus)
|
||||
* [`eas login`](#eas-login)
|
||||
* [`eas logout`](#eas-logout)
|
||||
* [`eas metadata:lint`](#eas-metadatalint)
|
||||
@@ -180,7 +183,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -197,7 +200,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -219,7 +222,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -236,7 +239,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -250,7 +253,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.8.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -302,7 +305,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -323,7 +326,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -343,7 +346,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -363,7 +366,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -386,7 +389,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -424,7 +427,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -444,7 +447,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -462,7 +465,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -482,7 +485,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -504,28 +507,34 @@ 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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
download a simulator/emulator build by build ID or fingerprint hash
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:download --fingerprint <value> [-p ios|android] [--dev-client] [--json] [--non-interactive]
|
||||
$ eas build:download [--build-id <value> | --fingerprint <value> | -p ios|android | --dev-client]
|
||||
[--all-artifacts] [--json] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=<option> <options: ios|android>
|
||||
--all-artifacts Download all available build artifacts (build artifacts archive, Xcode logs, etc.) in
|
||||
addition to the application archive. Without this flag, only the application archive is
|
||||
downloaded and the command errors if it is missing.
|
||||
--build-id=<value> ID of the build to download. Mutually exclusive with --fingerprint, --platform, and
|
||||
--dev-client; the platform is derived from the build itself.
|
||||
--[no-]dev-client Filter only dev-client builds.
|
||||
--fingerprint=<value> (required) Fingerprint hash of the build to download
|
||||
--fingerprint=<value> Fingerprint hash of the build to download
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -563,7 +572,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -603,7 +612,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -633,7 +642,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -659,7 +668,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -711,7 +720,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -729,7 +738,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -747,7 +756,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -764,7 +773,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -785,7 +794,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -806,7 +815,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -828,7 +837,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:insights`
|
||||
|
||||
@@ -852,7 +861,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -872,7 +881,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -894,7 +903,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -916,7 +925,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -951,7 +960,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -974,7 +983,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -995,7 +1004,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/v18.8.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -1012,7 +1021,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1030,7 +1039,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1059,7 +1068,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1084,7 +1093,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1105,7 +1114,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1126,7 +1135,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1163,7 +1172,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1183,7 +1192,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1204,7 +1213,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1225,7 +1234,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1239,7 +1248,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1253,7 +1262,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.8.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1285,7 +1294,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1311,7 +1320,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1333,7 +1342,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1361,7 +1370,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1389,7 +1398,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1412,7 +1421,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1434,7 +1443,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1468,7 +1477,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1512,7 +1521,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1543,7 +1552,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1602,6 +1611,65 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
## `eas integrations:asc:connect`
|
||||
|
||||
connect a project to an App Store Connect app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas integrations:asc:connect [--api-key-id <value>] [--asc-app-id <value>] [--bundle-id <value>] [--json]
|
||||
[--non-interactive]
|
||||
|
||||
FLAGS
|
||||
--api-key-id=<value> Apple App Store Connect API Key ID
|
||||
--asc-app-id=<value> App Store Connect app identifier
|
||||
--bundle-id=<value> Filter discovered apps by bundle identifier
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
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/v18.9.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
|
||||
## `eas integrations:asc:disconnect`
|
||||
|
||||
disconnect the current project from its App Store Connect app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas integrations:asc:disconnect [--yes] [--json] [--non-interactive]
|
||||
|
||||
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.
|
||||
--yes Skip confirmation prompt
|
||||
|
||||
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/v18.9.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
|
||||
## `eas integrations:asc:status`
|
||||
|
||||
show the App Store Connect app link status for the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas integrations:asc:status [--json] [--non-interactive]
|
||||
|
||||
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.
|
||||
|
||||
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/v18.9.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
|
||||
## `eas login`
|
||||
|
||||
log in with your Expo account
|
||||
@@ -1652,7 +1720,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1670,7 +1738,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1688,7 +1756,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1740,7 +1808,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/v18.8.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1754,7 +1822,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1777,7 +1845,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1801,7 +1869,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1819,7 +1887,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1852,7 +1920,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.8.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1895,7 +1963,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1916,7 +1984,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1937,7 +2005,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1962,7 +2030,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:insights GROUPID`
|
||||
|
||||
@@ -1989,7 +2057,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -2016,7 +2084,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -2054,7 +2122,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2082,7 +2150,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2112,7 +2180,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2133,7 +2201,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2157,7 +2225,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2178,7 +2246,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/v18.8.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2200,7 +2268,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2220,7 +2288,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2239,7 +2307,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2262,7 +2330,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2279,7 +2347,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2400,7 +2468,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2420,7 +2488,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2444,7 +2512,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -2484,7 +2552,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2506,7 +2574,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2534,7 +2602,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2554,7 +2622,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2576,5 +2644,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/v18.8.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/v18.9.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+1980
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.8.0",
|
||||
"@expo/steps": "18.9.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
@@ -201,6 +201,9 @@
|
||||
"env": {
|
||||
"description": "manage project and account environment variables"
|
||||
},
|
||||
"integrations": {
|
||||
"description": "manage service connections"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "manage store configuration"
|
||||
},
|
||||
|
||||
@@ -64,6 +64,24 @@ export function getWorkflowRunUrl(
|
||||
).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
|
||||
* tooling where no richer URL exists.
|
||||
*/
|
||||
export function getBareJobRunUrl(
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
jobRunId: string
|
||||
): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/job-runs/${encodeURIComponent(jobRunId)}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { AppPlatform, BuildFragment } from '../../../graphql/generated';
|
||||
import { BuildQuery } from '../../../graphql/queries/BuildQuery';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
import Download from '../download';
|
||||
|
||||
jest.mock('../../../graphql/queries/BuildQuery');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../utils/json');
|
||||
|
||||
const mockByIdAsync = jest.mocked(BuildQuery.byIdAsync);
|
||||
const mockViewBuildsOnAppAsync = jest.mocked(BuildQuery.viewBuildsOnAppAsync);
|
||||
const mockEnableJsonOutput = jest.mocked(enableJsonOutput);
|
||||
const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput);
|
||||
|
||||
function makeBuild(overrides: Partial<BuildFragment> = {}): BuildFragment {
|
||||
return {
|
||||
id: 'build-123',
|
||||
platform: AppPlatform.Ios,
|
||||
project: { id: 'project-1' },
|
||||
artifacts: {
|
||||
applicationArchiveUrl: 'https://example.com/app.tar.gz',
|
||||
buildArtifactsUrl: null,
|
||||
xcodeBuildLogsUrl: null,
|
||||
buildUrl: null,
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as BuildFragment;
|
||||
}
|
||||
|
||||
describe(Download, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const projectId = 'test-project-id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createCommand(argv: string[]): Download {
|
||||
const command = new Download(argv, mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockResolvedValue({
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
// Stub the file-system-touching helpers so we don't hit disk.
|
||||
jest
|
||||
.spyOn(command as any, 'getPathToBuildArtifactAsync')
|
||||
.mockResolvedValue('/cache/path/to/build.app');
|
||||
jest.spyOn(command as any, 'downloadExtraArtifactsAsync').mockResolvedValue({});
|
||||
return command;
|
||||
}
|
||||
|
||||
it('errors when neither --build-id nor --fingerprint is provided', async () => {
|
||||
const command = createCommand([]);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Either --build-id or --fingerprint is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches build by ID when --build-id is provided', async () => {
|
||||
mockByIdAsync.mockResolvedValue(makeBuild());
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockByIdAsync).toHaveBeenCalledWith(graphqlClient, 'build-123');
|
||||
expect(mockViewBuildsOnAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches builds by fingerprint when --fingerprint is provided', async () => {
|
||||
mockViewBuildsOnAppAsync.mockResolvedValue([makeBuild()]);
|
||||
|
||||
const command = createCommand(['--fingerprint', 'fp-abc', '--platform', 'ios']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockViewBuildsOnAppAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockViewBuildsOnAppAsync.mock.calls[0][1].filter?.fingerprintHash).toBe('fp-abc');
|
||||
expect(mockByIdAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses platform from the build itself when --build-id is provided', async () => {
|
||||
mockByIdAsync.mockResolvedValue(makeBuild({ platform: AppPlatform.Android }));
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await command.runAsync();
|
||||
|
||||
const getPathSpy = jest.spyOn(command as any, 'getPathToBuildArtifactAsync');
|
||||
// The spy was set up after instantiation; just ensure runAsync used the build's platform
|
||||
// by checking that no platform prompt was needed.
|
||||
expect(getPathSpy).toBeDefined();
|
||||
});
|
||||
|
||||
it('errors with a helpful message when applicationArchiveUrl is missing and other artifacts exist', async () => {
|
||||
mockByIdAsync.mockResolvedValue(
|
||||
makeBuild({
|
||||
artifacts: {
|
||||
applicationArchiveUrl: null,
|
||||
buildArtifactsUrl: 'https://example.com/build-artifacts.tar.gz',
|
||||
xcodeBuildLogsUrl: 'https://example.com/xcode-logs.tar.gz',
|
||||
buildUrl: null,
|
||||
} as any,
|
||||
})
|
||||
);
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
/Other artifacts are available \(buildArtifacts, xcodeBuildLogs\); re-run with --all-artifacts/
|
||||
);
|
||||
});
|
||||
|
||||
it('errors with the basic message when applicationArchiveUrl is missing and no other artifacts exist', async () => {
|
||||
mockByIdAsync.mockResolvedValue(
|
||||
makeBuild({
|
||||
artifacts: {
|
||||
applicationArchiveUrl: null,
|
||||
buildArtifactsUrl: null,
|
||||
xcodeBuildLogsUrl: null,
|
||||
buildUrl: null,
|
||||
} as any,
|
||||
})
|
||||
);
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await expect(command.runAsync()).rejects.toThrow('Build does not have an application archive url');
|
||||
});
|
||||
|
||||
it('does not throw when applicationArchiveUrl is missing but --all-artifacts is set', async () => {
|
||||
mockByIdAsync.mockResolvedValue(
|
||||
makeBuild({
|
||||
artifacts: {
|
||||
applicationArchiveUrl: null,
|
||||
buildArtifactsUrl: 'https://example.com/build-artifacts.tar.gz',
|
||||
xcodeBuildLogsUrl: null,
|
||||
buildUrl: null,
|
||||
} as any,
|
||||
})
|
||||
);
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123', '--all-artifacts']);
|
||||
const downloadExtrasSpy = jest
|
||||
.spyOn(command as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({ buildArtifacts: '/cache/build-123-artifacts/build-artifacts.tar.gz' });
|
||||
|
||||
await expect(command.runAsync()).resolves.not.toThrow();
|
||||
expect(downloadExtrasSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('downloads extras only when --all-artifacts is set, regardless of --build-id vs --fingerprint', async () => {
|
||||
mockViewBuildsOnAppAsync.mockResolvedValue([makeBuild()]);
|
||||
|
||||
const commandWithoutFlag = createCommand(['--fingerprint', 'fp-abc', '--platform', 'ios']);
|
||||
const downloadExtrasSpyWithout = jest
|
||||
.spyOn(commandWithoutFlag as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({});
|
||||
await commandWithoutFlag.runAsync();
|
||||
expect(downloadExtrasSpyWithout).not.toHaveBeenCalled();
|
||||
|
||||
const commandWithFlag = createCommand([
|
||||
'--fingerprint',
|
||||
'fp-abc',
|
||||
'--platform',
|
||||
'ios',
|
||||
'--all-artifacts',
|
||||
]);
|
||||
const downloadExtrasSpyWith = jest
|
||||
.spyOn(commandWithFlag as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({ buildArtifacts: '/path/to/extras' });
|
||||
await commandWithFlag.runAsync();
|
||||
expect(downloadExtrasSpyWith).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits JSON with path and extras when --json --all-artifacts are set', async () => {
|
||||
mockByIdAsync.mockResolvedValue(makeBuild());
|
||||
|
||||
const command = createCommand([
|
||||
'--build-id',
|
||||
'build-123',
|
||||
'--all-artifacts',
|
||||
'--json',
|
||||
'--non-interactive',
|
||||
]);
|
||||
jest
|
||||
.spyOn(command as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({ buildArtifacts: '/cache/extras/build-artifacts.tar.gz' });
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockEnableJsonOutput).toHaveBeenCalled();
|
||||
expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({
|
||||
path: '/cache/path/to/build.app',
|
||||
buildArtifacts: '/cache/extras/build-artifacts.tar.gz',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
@@ -15,24 +16,43 @@ import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getEasBuildRunCachedAppPath } from '../../run/run';
|
||||
import { downloadAndMaybeExtractAppAsync } from '../../utils/download';
|
||||
import {
|
||||
downloadAndMaybeExtractAppAsync,
|
||||
downloadFileWithProgressTrackerAsync,
|
||||
} from '../../utils/download';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
|
||||
|
||||
export default class Download extends EasCommand {
|
||||
static override description = 'download simulator/emulator builds for a given fingerprint hash';
|
||||
static override description =
|
||||
'download a simulator/emulator build by build ID or fingerprint hash';
|
||||
|
||||
|
||||
static override flags = {
|
||||
'build-id': Flags.string({
|
||||
description:
|
||||
'ID of the build to download. Mutually exclusive with --fingerprint, --platform, and --dev-client; the platform is derived from the build itself.',
|
||||
exclusive: ['fingerprint', 'platform', 'dev-client'],
|
||||
}),
|
||||
fingerprint: Flags.string({
|
||||
description: 'Fingerprint hash of the build to download',
|
||||
required: true,
|
||||
exclusive: ['build-id'],
|
||||
}),
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: [Platform.IOS, Platform.ANDROID] as const,
|
||||
exclusive: ['build-id'],
|
||||
})(),
|
||||
'dev-client': Flags.boolean({
|
||||
description: 'Filter only dev-client builds.',
|
||||
allowNo: true,
|
||||
exclusive: ['build-id'],
|
||||
}),
|
||||
'all-artifacts': Flags.boolean({
|
||||
description:
|
||||
'Download all available build artifacts (build artifacts archive, Xcode logs, etc.) in addition to the application archive. Without this flag, only the application archive is downloaded and the command errors if it is missing.',
|
||||
default: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -45,14 +65,20 @@ export default class Download extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: {
|
||||
'build-id': buildId,
|
||||
platform,
|
||||
fingerprint,
|
||||
'dev-client': developmentClient,
|
||||
'all-artifacts': allArtifacts,
|
||||
...rawFlags
|
||||
},
|
||||
} = await this.parse(Download);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(rawFlags);
|
||||
|
||||
if (!buildId && !fingerprint) {
|
||||
throw new Error('Either --build-id or --fingerprint is required.');
|
||||
}
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
projectId,
|
||||
@@ -64,24 +90,113 @@ export default class Download extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const selectedPlatform = await resolvePlatformAsync({ nonInteractive, platform });
|
||||
const build = await this.getBuildAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform: selectedPlatform,
|
||||
fingerprintHash: fingerprint,
|
||||
developmentClient,
|
||||
});
|
||||
const buildArtifactPath = await this.getPathToBuildArtifactAsync(build, selectedPlatform);
|
||||
let build: BuildFragment;
|
||||
let selectedPlatform: AppPlatform;
|
||||
if (buildId) {
|
||||
build = await this.getBuildByIdAsync({ graphqlClient, buildId });
|
||||
selectedPlatform = build.platform;
|
||||
Log.succeed(`🎯 Found build ${chalk.bold(buildId)} on EAS servers.`);
|
||||
} else {
|
||||
selectedPlatform = await resolvePlatformAsync({ nonInteractive, platform });
|
||||
build = await this.getBuildByFingerprintAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform: selectedPlatform,
|
||||
fingerprintHash: fingerprint!,
|
||||
developmentClient,
|
||||
});
|
||||
}
|
||||
|
||||
let buildArtifactPath: string | null = null;
|
||||
if (build.artifacts?.applicationArchiveUrl) {
|
||||
buildArtifactPath = await this.getPathToBuildArtifactAsync(build, selectedPlatform);
|
||||
} else if (!allArtifacts) {
|
||||
const availableArtifacts = listAvailableExtraArtifactNames(build);
|
||||
if (availableArtifacts.length > 0) {
|
||||
throw new Error(
|
||||
`Build does not have an application archive url. Other artifacts are available (${availableArtifacts.join(', ')}); re-run with --all-artifacts to download them.`
|
||||
);
|
||||
}
|
||||
throw new Error('Build does not have an application archive url');
|
||||
}
|
||||
|
||||
let extraArtifactPaths: Record<string, string> = {};
|
||||
if (allArtifacts) {
|
||||
extraArtifactPaths = await this.downloadExtraArtifactsAsync(build);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
const jsonResults = { path: buildArtifactPath };
|
||||
const jsonResults = {
|
||||
...(buildArtifactPath != null && { path: buildArtifactPath }),
|
||||
...extraArtifactPaths,
|
||||
};
|
||||
printJsonOnlyOutput(jsonResults);
|
||||
} else {
|
||||
Log.log(`Build downloaded to ${chalk.bold(buildArtifactPath)}`);
|
||||
if (buildArtifactPath != null) {
|
||||
Log.log(`Build downloaded to ${chalk.bold(buildArtifactPath)}`);
|
||||
}
|
||||
for (const [name, filePath] of Object.entries(extraArtifactPaths)) {
|
||||
Log.log(`${name} downloaded to ${chalk.bold(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getBuildAsync({
|
||||
private async downloadExtraArtifactsAsync(build: BuildFragment): Promise<Record<string, string>> {
|
||||
const artifacts = build.artifacts;
|
||||
if (!artifacts) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const extraArtifacts: Array<{ name: string; url: string }> = [];
|
||||
if (artifacts.buildArtifactsUrl) {
|
||||
extraArtifacts.push({ name: 'buildArtifacts', url: artifacts.buildArtifactsUrl });
|
||||
}
|
||||
if (artifacts.xcodeBuildLogsUrl) {
|
||||
extraArtifacts.push({ name: 'xcodeBuildLogs', url: artifacts.xcodeBuildLogsUrl });
|
||||
}
|
||||
if (artifacts.buildUrl && artifacts.buildUrl !== artifacts.applicationArchiveUrl) {
|
||||
extraArtifacts.push({ name: 'build', url: artifacts.buildUrl });
|
||||
}
|
||||
|
||||
if (extraArtifacts.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const outputDir = path.join(getEasBuildRunCacheDirectoryPath(), `${build.id}-artifacts`);
|
||||
await fs.ensureDir(outputDir);
|
||||
|
||||
const downloaded: Record<string, string> = {};
|
||||
for (const { name, url } of extraArtifacts) {
|
||||
const fileName = getFileNameFromUrl(url, name);
|
||||
const outputPath = path.join(outputDir, fileName);
|
||||
await downloadFileWithProgressTrackerAsync(
|
||||
url,
|
||||
outputPath,
|
||||
(ratio, total) =>
|
||||
`Downloading ${name} (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
|
||||
`Successfully downloaded ${name}`
|
||||
);
|
||||
downloaded[name] = outputPath;
|
||||
}
|
||||
|
||||
return downloaded;
|
||||
}
|
||||
|
||||
private async getBuildByIdAsync({
|
||||
graphqlClient,
|
||||
buildId,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
buildId: string;
|
||||
}): Promise<BuildFragment> {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(graphqlClient, buildId);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Could not find build with ID ${buildId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBuildByFingerprintAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform,
|
||||
@@ -108,7 +223,7 @@ export default class Download extends EasCommand {
|
||||
limit: 1,
|
||||
});
|
||||
if (builds.length === 0) {
|
||||
throw new Errors.CLIError(
|
||||
throw new Error(
|
||||
`No builds available for ${platform} with fingerprint hash ${fingerprintHash}`
|
||||
);
|
||||
}
|
||||
@@ -123,7 +238,7 @@ export default class Download extends EasCommand {
|
||||
build.id,
|
||||
platform
|
||||
);
|
||||
if (await pathExists(cachedBuildArtifactPath)) {
|
||||
if (await fs.pathExists(cachedBuildArtifactPath)) {
|
||||
Log.newLine();
|
||||
Log.log(`Using cached build...`);
|
||||
return cachedBuildArtifactPath;
|
||||
@@ -167,3 +282,33 @@ async function resolvePlatformAsync({
|
||||
});
|
||||
return selectedPlatform;
|
||||
}
|
||||
|
||||
function listAvailableExtraArtifactNames(build: BuildFragment): string[] {
|
||||
const names: string[] = [];
|
||||
if (build.artifacts?.buildArtifactsUrl) {
|
||||
names.push('buildArtifacts');
|
||||
}
|
||||
if (build.artifacts?.xcodeBuildLogsUrl) {
|
||||
names.push('xcodeBuildLogs');
|
||||
}
|
||||
if (
|
||||
build.artifacts?.buildUrl &&
|
||||
build.artifacts.buildUrl !== build.artifacts.applicationArchiveUrl
|
||||
) {
|
||||
names.push('build');
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function getFileNameFromUrl(url: string, fallbackName: string): string {
|
||||
try {
|
||||
const pathname = new URL(url).pathname;
|
||||
const basename = path.basename(pathname);
|
||||
if (basename) {
|
||||
return basename;
|
||||
}
|
||||
} catch {
|
||||
// fall through to default
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasCommandError } from '../../../../commandUtils/errors';
|
||||
import { selectOrCreateAscApiKeyIdAsync } from '../../../../integrations/asc/ascApiKey';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../../credentials/ios/api/graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AscAppLinkMutation } from '../../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../../graphql/queries/AscAppLinkQuery';
|
||||
import IntegrationsAscConnect from '../connect';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AscAppLinkQuery');
|
||||
jest.mock('../../../../graphql/mutations/AscAppLinkMutation');
|
||||
jest.mock('../../../../credentials/ios/api/graphql/queries/AppStoreConnectApiKeyQuery');
|
||||
jest.mock('../../../../integrations/asc/ascApiKey');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora');
|
||||
|
||||
const testProjectId = 'test-project-id';
|
||||
const mockMetadataConnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
const mockRemoteApps = [
|
||||
{
|
||||
ascAppIdentifier: '9876543210',
|
||||
bundleIdentifier: 'com.test.newapp',
|
||||
name: 'New App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
];
|
||||
|
||||
describe(IntegrationsAscConnect, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const actor = { id: 'actor-id' } as any;
|
||||
const analytics = {} as any;
|
||||
const vcsClient = {} as any;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fails when already connected', async () => {
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockResolvedValueOnce(mockMetadataConnected);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'key-id', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(EasCommandError);
|
||||
});
|
||||
|
||||
it('connects successfully in non-interactive mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected)
|
||||
.mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest
|
||||
.mocked(AscAppLinkMutation.createAppStoreConnectAppAsync)
|
||||
.mockResolvedValueOnce({ id: 'new-link-id', ascAppIdentifier: '9876543210' });
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.createAppStoreConnectAppAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
ascAppIdentifier: '9876543210',
|
||||
appStoreConnectApiKeyId: 'key-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts Apple key identifier in non-interactive mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected)
|
||||
.mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest
|
||||
.mocked(AscAppLinkMutation.createAppStoreConnectAppAsync)
|
||||
.mockResolvedValueOnce({ id: 'new-link-id', ascAppIdentifier: '9876543210' });
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'eas-key-uuid',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.createAppStoreConnectAppAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
ascAppIdentifier: '9876543210',
|
||||
appStoreConnectApiKeyId: 'eas-key-uuid',
|
||||
});
|
||||
});
|
||||
|
||||
it('requires --api-key-id in non-interactive mode', async () => {
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('--api-key-id is required');
|
||||
});
|
||||
|
||||
it('requires --asc-app-id in non-interactive mode', async () => {
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('--asc-app-id is required');
|
||||
});
|
||||
|
||||
it('fails when asc-app-id is not found among discovered apps', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', 'nonexistent', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('was not found among accessible apps');
|
||||
});
|
||||
|
||||
it('fails when passing EAS key id in non-interactive mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'eas-key-uuid',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'eas-key-uuid', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'No App Store Connect API key found with Apple key identifier'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when multiple keys match Apple key identifier', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'eas-key-uuid-1',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
{
|
||||
id: 'eas-key-uuid-2',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Multiple App Store Connect API keys match Apple key identifier'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when no accessible apps are discovered', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce([]);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'No accessible apps found on App Store Connect'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when app discovery throws', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync)
|
||||
.mockRejectedValueOnce(new Error('discovery failed'));
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('discovery failed');
|
||||
});
|
||||
|
||||
it('creates or selects key when no api-key-id is provided', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected)
|
||||
.mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([]);
|
||||
jest.mocked(selectOrCreateAscApiKeyIdAsync).mockResolvedValueOnce('generated-key-id');
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest
|
||||
.mocked(AscAppLinkMutation.createAppStoreConnectAppAsync)
|
||||
.mockResolvedValueOnce({ id: 'new-link-id', ascAppIdentifier: '9876543210' });
|
||||
|
||||
const command = new IntegrationsAscConnect(['--asc-app-id', '9876543210'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(selectOrCreateAscApiKeyIdAsync).toHaveBeenCalled();
|
||||
expect(AscAppLinkMutation.createAppStoreConnectAppAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
ascAppIdentifier: '9876543210',
|
||||
appStoreConnectApiKeyId: 'generated-key-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AscAppLinkMutation } from '../../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../../log';
|
||||
import { toggleConfirmAsync } from '../../../../prompts';
|
||||
import IntegrationsAscDisconnect from '../disconnect';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AscAppLinkQuery');
|
||||
jest.mock('../../../../graphql/mutations/AscAppLinkMutation');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../prompts');
|
||||
|
||||
const testProjectId = 'test-project-id';
|
||||
const mockMetadataConnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
describe(IntegrationsAscDisconnect, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('disconnects successfully with --yes', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataConnected)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AscAppLinkMutation.deleteAppStoreConnectAppAsync).mockResolvedValueOnce(undefined);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
'asc-app-link-id'
|
||||
);
|
||||
expect(AscAppLinkQuery.getAppMetadataAsync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
graphqlClient,
|
||||
testProjectId,
|
||||
{
|
||||
useCache: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('no-op when already disconnected', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints json output when already disconnected in json mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes', '--json'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"action": "disconnect"')
|
||||
);
|
||||
});
|
||||
|
||||
it('handles invalid ASC API key gracefully', async () => {
|
||||
const ascError = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockRejectedValueOnce(ascError);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
expect(jest.mocked(Log.warn)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('revoked or is no longer valid')
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels disconnection when confirmation is rejected', async () => {
|
||||
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
||||
throw new Error('process.exit called');
|
||||
}) as never);
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(toggleConfirmAsync).mockResolvedValueOnce(false);
|
||||
|
||||
const command = new IntegrationsAscDisconnect([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('process.exit called');
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AscAppLinkQuery } from '../../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../../log';
|
||||
import IntegrationsAscStatus from '../status';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AscAppLinkQuery');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora');
|
||||
|
||||
const testProjectId = 'test-project-id';
|
||||
const mockMetadataConnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
describe(IntegrationsAscStatus, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('displays connected status', async () => {
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockResolvedValueOnce(mockMetadataConnected);
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(AscAppLinkQuery.getAppMetadataAsync).toHaveBeenCalledWith(graphqlClient, testProjectId);
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays disconnected status', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints json output in json mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscStatus(['--json'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"action": "status"')
|
||||
);
|
||||
});
|
||||
|
||||
it('handles invalid ASC API key gracefully', async () => {
|
||||
const ascError = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockRejectedValueOnce(ascError);
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.warn)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('revoked or is no longer valid')
|
||||
);
|
||||
});
|
||||
|
||||
it('handles invalid ASC API key with json output', async () => {
|
||||
const ascError = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockRejectedValueOnce(ascError);
|
||||
|
||||
const command = new IntegrationsAscStatus(['--json'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"status": "invalid"')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when fetching status fails', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockRejectedValueOnce(new Error('status failed'));
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('status failed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { EasCommandError } from '../../../commandUtils/errors';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import { CredentialsContext } from '../../../credentials/context';
|
||||
import { buildJsonOutput, formatAscAppLinkStatus } from '../../../integrations/asc/utils';
|
||||
import { selectOrCreateAscApiKeyIdAsync } from '../../../integrations/asc/ascApiKey';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../credentials/ios/api/graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AscAppLinkMutation } from '../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { selectAsync } from '../../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
|
||||
type DiscoveredAscApps = Awaited<ReturnType<typeof AscAppLinkQuery.discoverAccessibleAppsAsync>>;
|
||||
|
||||
export default class IntegrationsAscConnect extends EasCommand {
|
||||
static override description = 'connect a project to an App Store Connect app';
|
||||
|
||||
static override flags = {
|
||||
'api-key-id': Flags.string({
|
||||
description: 'Apple App Store Connect API Key ID',
|
||||
}),
|
||||
'asc-app-id': Flags.string({
|
||||
description: 'App Store Connect app identifier',
|
||||
}),
|
||||
'bundle-id': Flags.string({
|
||||
description: 'Filter discovered apps by bundle identifier',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(IntegrationsAscConnect);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
if (!flags['api-key-id']) {
|
||||
throw new EasCommandError('--api-key-id is required in non-interactive mode.');
|
||||
}
|
||||
if (!flags['asc-app-id']) {
|
||||
throw new EasCommandError('--asc-app-id is required in non-interactive mode.');
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
projectDir,
|
||||
loggedIn: { actor, graphqlClient },
|
||||
analytics,
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(IntegrationsAscConnect, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
// Step 1: Check current status
|
||||
const statusSpinner = ora('Checking current App Store Connect app link status').start();
|
||||
const metadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
statusSpinner.succeed('Checked current status');
|
||||
|
||||
if (metadata.appStoreConnectApp) {
|
||||
throw new EasCommandError(
|
||||
`Project ${chalk.bold(metadata.fullName)} is already connected to App Store Connect app ${chalk.bold(metadata.appStoreConnectApp.ascAppIdentifier)}. Disconnect first with ${chalk.bold('eas integrations:asc:disconnect')}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Get ASC API key
|
||||
const keysSpinner = ora('Fetching App Store Connect API keys').start();
|
||||
const keys = await AppStoreConnectApiKeyQuery.getAllForAccountAsync(
|
||||
graphqlClient,
|
||||
metadata.ownerAccount.name
|
||||
);
|
||||
keysSpinner.succeed(`Found ${keys.length} API key(s)`);
|
||||
|
||||
let apiKeyId = flags['api-key-id'];
|
||||
if (!apiKeyId) {
|
||||
const credentialsContext = new CredentialsContext({
|
||||
projectInfo: null,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
});
|
||||
|
||||
apiKeyId = await selectOrCreateAscApiKeyIdAsync({
|
||||
credentialsContext,
|
||||
existingKeys: keys,
|
||||
ownerAccount: metadata.ownerAccount,
|
||||
});
|
||||
} else {
|
||||
const keysByAppleId = keys.filter(key => key.keyIdentifier === apiKeyId);
|
||||
if (keysByAppleId.length > 1) {
|
||||
throw new EasCommandError(
|
||||
`Multiple App Store Connect API keys match Apple key identifier "${apiKeyId}".`
|
||||
);
|
||||
} else if (keysByAppleId.length === 1) {
|
||||
apiKeyId = keysByAppleId[0].id;
|
||||
} else {
|
||||
throw new EasCommandError(
|
||||
`No App Store Connect API key found with Apple key identifier "${apiKeyId}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!apiKeyId) {
|
||||
throw new EasCommandError('No App Store Connect API key selected.');
|
||||
}
|
||||
|
||||
// Step 3: Discover remote apps
|
||||
const discoverSpinner = ora('Discovering App Store Connect apps').start();
|
||||
let remoteApps: DiscoveredAscApps;
|
||||
try {
|
||||
remoteApps = await AscAppLinkQuery.discoverAccessibleAppsAsync(
|
||||
graphqlClient,
|
||||
apiKeyId,
|
||||
flags['bundle-id']
|
||||
);
|
||||
discoverSpinner.succeed(`Found ${remoteApps.length} app(s) on App Store Connect`);
|
||||
} catch (err) {
|
||||
discoverSpinner.fail('Failed to discover apps');
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (remoteApps.length === 0) {
|
||||
throw new EasCommandError(
|
||||
'No accessible apps found on App Store Connect for the selected API key.' +
|
||||
(flags['bundle-id']
|
||||
? ` Try removing the --bundle-id filter or verify the bundle ID "${flags['bundle-id']}".`
|
||||
: '')
|
||||
);
|
||||
}
|
||||
|
||||
// Step 4: Select remote app
|
||||
let selectedApp: DiscoveredAscApps[number];
|
||||
if (flags['asc-app-id']) {
|
||||
const match = remoteApps.find(app => app.ascAppIdentifier === flags['asc-app-id']);
|
||||
if (!match) {
|
||||
throw new EasCommandError(
|
||||
`App with identifier "${flags['asc-app-id']}" was not found among accessible apps. Run ${chalk.bold('eas integrations:asc:connect')} interactively to discover available apps.`
|
||||
);
|
||||
}
|
||||
selectedApp = match;
|
||||
} else {
|
||||
selectedApp = await selectAsync<DiscoveredAscApps[number]>(
|
||||
'Select an App Store Connect app:',
|
||||
remoteApps.map(app => ({
|
||||
title: `${app.name} (${app.bundleIdentifier}) [${app.ascAppIdentifier}]`,
|
||||
value: app,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Step 5: Create link
|
||||
const createSpinner = ora('Connecting project to App Store Connect app').start();
|
||||
try {
|
||||
await AscAppLinkMutation.createAppStoreConnectAppAsync(graphqlClient, {
|
||||
appId: metadata.id,
|
||||
ascAppIdentifier: selectedApp.ascAppIdentifier,
|
||||
appStoreConnectApiKeyId: apiKeyId,
|
||||
});
|
||||
createSpinner.succeed('Connected project to App Store Connect app');
|
||||
} catch (err) {
|
||||
createSpinner.fail('Failed to connect project');
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Step 6: Refetch and display
|
||||
const refetchSpinner = ora('Verifying connection').start();
|
||||
const updatedMetadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
refetchSpinner.succeed('Verified connection');
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('connect', updatedMetadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(updatedMetadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import {
|
||||
buildInvalidJsonOutput,
|
||||
buildJsonOutput,
|
||||
formatAscAppLinkStatus,
|
||||
isAscAuthenticationError,
|
||||
} from '../../../integrations/asc/utils';
|
||||
import { AscAppLinkMutation } from '../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { toggleConfirmAsync } from '../../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
|
||||
export default class IntegrationsAscDisconnect extends EasCommand {
|
||||
static override description = 'disconnect the current project from its App Store Connect app';
|
||||
|
||||
static override flags = {
|
||||
yes: Flags.boolean({
|
||||
description: 'Skip confirmation prompt',
|
||||
default: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(IntegrationsAscDisconnect);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(IntegrationsAscDisconnect, {
|
||||
nonInteractive: nonInteractive || flags.yes,
|
||||
});
|
||||
|
||||
// Step 1: Check current status
|
||||
const metadata = await this.fetchCurrentStatusAsync(graphqlClient, projectId);
|
||||
if (!metadata) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildInvalidJsonOutput('disconnect', projectId));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
'The App Store Connect API key linked to this project has been revoked or is no longer valid.\nThe connection cannot be resolved from the CLI. Please update the API key on the Expo dashboard.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!metadata.appStoreConnectApp) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('disconnect', metadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`Project ${chalk.bold(metadata.fullName)} is not connected to any App Store Connect app.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Confirm
|
||||
if (!flags.yes && !nonInteractive) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(metadata));
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
'You are about to disconnect this project from its App Store Connect app.\nThis action is reversible by reconnecting.'
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error('Canceled disconnection');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Delete
|
||||
const deleteSpinner = ora('Disconnecting App Store Connect app').start();
|
||||
try {
|
||||
await AscAppLinkMutation.deleteAppStoreConnectAppAsync(
|
||||
graphqlClient,
|
||||
metadata.appStoreConnectApp.id
|
||||
);
|
||||
deleteSpinner.succeed('Disconnected App Store Connect app');
|
||||
} catch (err) {
|
||||
deleteSpinner.fail('Failed to disconnect App Store Connect app');
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Step 4: Refetch and display
|
||||
const refetchSpinner = ora('Verifying disconnection').start();
|
||||
const updatedMetadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId, {
|
||||
useCache: false,
|
||||
});
|
||||
refetchSpinner.succeed('Verified disconnection');
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('disconnect', updatedMetadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(updatedMetadata));
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchCurrentStatusAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<Awaited<ReturnType<typeof AscAppLinkQuery.getAppMetadataAsync>> | null> {
|
||||
const spinner = ora('Checking current App Store Connect app link status').start();
|
||||
try {
|
||||
const metadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
spinner.succeed('Checked current status');
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
if (isAscAuthenticationError(err)) {
|
||||
spinner.fail('App Store Connect connection is invalid');
|
||||
return null;
|
||||
}
|
||||
spinner.fail('Failed to check current status');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import {
|
||||
buildInvalidJsonOutput,
|
||||
buildJsonOutput,
|
||||
formatAscAppLinkStatus,
|
||||
isAscAuthenticationError,
|
||||
} from '../../../integrations/asc/utils';
|
||||
import { AscAppLinkQuery } from '../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
|
||||
export default class IntegrationsAscStatus extends EasCommand {
|
||||
static override description =
|
||||
'show the App Store Connect app link status for the current project';
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(IntegrationsAscStatus);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(IntegrationsAscStatus, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const metadata = await this.fetchStatusAsync(graphqlClient, projectId);
|
||||
if (!metadata) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildInvalidJsonOutput('status', projectId));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
'The App Store Connect API key linked to this project has been revoked or is no longer valid.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('status', metadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(metadata));
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchStatusAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<Awaited<ReturnType<typeof AscAppLinkQuery.getAppMetadataAsync>> | null> {
|
||||
const spinner = ora('Fetching App Store Connect app link status').start();
|
||||
try {
|
||||
const metadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
spinner.succeed('Fetched App Store Connect app link status');
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
if (isAscAuthenticationError(err)) {
|
||||
spinner.fail('App Store Connect connection is invalid');
|
||||
return null;
|
||||
}
|
||||
spinner.fail('Failed to fetch App Store Connect app link status');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { getBareJobRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
AppPlatform,
|
||||
DeviceRunSessionStatus,
|
||||
DeviceRunSessionType,
|
||||
JobRunStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { DeviceRunSessionMutation } from '../../graphql/mutations/DeviceRunSessionMutation';
|
||||
import { DeviceRunSessionQuery } from '../../graphql/queries/DeviceRunSessionQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { sleepAsync } from '../../utils/promise';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
const POLL_INTERVAL_MS = 5_000; // 5 seconds
|
||||
const POLL_TIMEOUT_MS = 15 * 60 * 1_000; // 15 minutes
|
||||
|
||||
// Mapping enum → CLI flag value. Declared as Record<DeviceRunSessionType, string>
|
||||
// so adding a new enum value in codegen fails the build until it is wired up here.
|
||||
const DEVICE_RUN_SESSION_TYPE_FLAG_VALUES: Record<DeviceRunSessionType, string> = {
|
||||
[DeviceRunSessionType.AgentDevice]: 'agent-device',
|
||||
};
|
||||
|
||||
const DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE = Object.fromEntries(
|
||||
(Object.entries(DEVICE_RUN_SESSION_TYPE_FLAG_VALUES) as [DeviceRunSessionType, string][]).map(
|
||||
([type, value]) => [value, type]
|
||||
)
|
||||
) as Record<string, DeviceRunSessionType>;
|
||||
|
||||
type ReadinessResult = { ready: true; message: string } | { ready: false };
|
||||
type ReadinessChecker = (logMessages: readonly string[]) => ReadinessResult;
|
||||
|
||||
export default class SimulatorStart extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] start a remote simulator session on EAS and get the credentials to connect to it with the CLI tool of your choice';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.option({
|
||||
description: 'Device platform',
|
||||
options: ['android', 'ios'] as const,
|
||||
required: true,
|
||||
})(),
|
||||
type: Flags.option({
|
||||
description: 'Type of device run 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.',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SimulatorStart);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(SimulatorStart, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
const platform = flags.platform === 'android' ? AppPlatform.Android : AppPlatform.Ios;
|
||||
|
||||
const createSpinner = ora('🚀 Creating device run session').start();
|
||||
let deviceRunSessionId: string;
|
||||
try {
|
||||
const session = await DeviceRunSessionMutation.createDeviceRunSessionAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
platform,
|
||||
type: DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE[flags.type],
|
||||
packageVersion: flags['package-version'],
|
||||
});
|
||||
deviceRunSessionId = session.id;
|
||||
const jobRunId = nullthrows(session.turtleJobRun?.id, 'Expected device run session to start');
|
||||
const jobRunUrl = getBareJobRunUrl(session.app.ownerAccount.name, session.app.slug, jobRunId);
|
||||
createSpinner.succeed(
|
||||
`Device run session created (id: ${deviceRunSessionId}) ${link(jobRunUrl)}`
|
||||
);
|
||||
} catch (err) {
|
||||
createSpinner.fail('Failed to create device run session');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const checkReadiness = getReadinessCheckerForType(flags.type);
|
||||
|
||||
const pollSpinner = ora(`⏳ Waiting for ${flags.type} daemon to start`).start();
|
||||
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
||||
let result: ReadinessResult = { ready: false };
|
||||
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
const session = await DeviceRunSessionQuery.byIdAsync(graphqlClient, deviceRunSessionId);
|
||||
|
||||
if (
|
||||
session.status === DeviceRunSessionStatus.Errored ||
|
||||
session.status === DeviceRunSessionStatus.Stopped
|
||||
) {
|
||||
throw new Error(
|
||||
`Device run session ${deviceRunSessionId} ${session.status.toLowerCase()} before the ${flags.type} daemon was ready.`
|
||||
);
|
||||
}
|
||||
|
||||
const jobRunStatus = session.turtleJobRun?.status;
|
||||
if (
|
||||
jobRunStatus === JobRunStatus.Errored ||
|
||||
jobRunStatus === JobRunStatus.Canceled ||
|
||||
jobRunStatus === JobRunStatus.Finished
|
||||
) {
|
||||
throw new Error(
|
||||
`Turtle job run for device run session ${deviceRunSessionId} ${jobRunStatus.toLowerCase()} before the ${flags.type} daemon was ready.`
|
||||
);
|
||||
}
|
||||
|
||||
const logMessages = await fetchLogMessagesAsync(session.turtleJobRun?.logFileUrls ?? []);
|
||||
result = checkReadiness(logMessages);
|
||||
if (result.ready) {
|
||||
pollSpinner.succeed(`🎉 ${flags.type} daemon is ready`);
|
||||
break;
|
||||
}
|
||||
|
||||
await sleepAsync(POLL_INTERVAL_MS);
|
||||
}
|
||||
} catch (err) {
|
||||
pollSpinner.fail(`Failed while polling for ${flags.type} daemon logs`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!result.ready) {
|
||||
pollSpinner.fail(`Timed out waiting for ${flags.type} daemon to start`);
|
||||
throw new Error(
|
||||
`Timed out after ${Math.round(POLL_TIMEOUT_MS / 1000)}s waiting for ${flags.type} daemon to start.`
|
||||
);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.log(`🔑 Run the following in your shell to attach to ${flags.type}:`);
|
||||
Log.newLine();
|
||||
Log.log(result.message);
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`When you are done, stop the session with: eas simulator:stop --id ${deviceRunSessionId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getReadinessCheckerForType(type: string): ReadinessChecker {
|
||||
switch (type) {
|
||||
case DEVICE_RUN_SESSION_TYPE_FLAG_VALUES[DeviceRunSessionType.AgentDevice]:
|
||||
return checkAgentDeviceReadiness;
|
||||
default:
|
||||
throw new Error(`Unsupported device run session type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
const AGENT_DEVICE_BASE_URL_ENV_VAR = 'AGENT_DEVICE_DAEMON_BASE_URL';
|
||||
const AGENT_DEVICE_AUTH_TOKEN_ENV_VAR = 'AGENT_DEVICE_DAEMON_AUTH_TOKEN';
|
||||
|
||||
function checkAgentDeviceReadiness(logMessages: readonly string[]): ReadinessResult {
|
||||
let baseUrl: string | undefined;
|
||||
let authToken: string | undefined;
|
||||
for (const msg of logMessages) {
|
||||
baseUrl = baseUrl ?? extractExportedEnvValue(msg, AGENT_DEVICE_BASE_URL_ENV_VAR);
|
||||
authToken = authToken ?? extractExportedEnvValue(msg, AGENT_DEVICE_AUTH_TOKEN_ENV_VAR);
|
||||
if (baseUrl && authToken) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (baseUrl && authToken) {
|
||||
return {
|
||||
ready: true,
|
||||
message: [
|
||||
`export ${AGENT_DEVICE_BASE_URL_ENV_VAR}='${baseUrl}'`,
|
||||
`export ${AGENT_DEVICE_AUTH_TOKEN_ENV_VAR}='${authToken}'`,
|
||||
].join('\n'),
|
||||
};
|
||||
}
|
||||
return { ready: false };
|
||||
}
|
||||
|
||||
async function fetchLogMessagesAsync(logUrls: readonly string[]): Promise<string[]> {
|
||||
const messages: string[] = [];
|
||||
for (const url of logUrls) {
|
||||
const text = await fetchLogTextAsync(url);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
for (const line of text.split('\n')) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
messages.push(extractLogMessage(line));
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function fetchLogTextAsync(url: string): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return await response.text();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function extractLogMessage(line: string): string {
|
||||
// Turtle job run logs are JSONL (bunyan-shaped), e.g.
|
||||
// {"msg":"export FOO=\"bar\"","time":"...","logId":"..."}
|
||||
// Fall back to the raw line if it's not JSON or doesn't have a string msg.
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{')) {
|
||||
return line;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (parsed && typeof parsed === 'object' && 'msg' in parsed) {
|
||||
const msg = (parsed as { msg: unknown }).msg;
|
||||
if (typeof msg === 'string') {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// not JSON, fall through
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function extractExportedEnvValue(text: string, varName: string): string | undefined {
|
||||
// Matches: export NAME=value | export NAME="value" | export NAME='value'
|
||||
const pattern = new RegExp(`export\\s+${escapeRegExp(varName)}=(?:"([^"]*)"|'([^']*)'|(\\S+))`);
|
||||
const match = pattern.exec(text);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
return match[1] ?? match[2] ?? match[3];
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { DeviceRunSessionMutation } from '../../graphql/mutations/DeviceRunSessionMutation';
|
||||
import { ora } from '../../ora';
|
||||
|
||||
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';
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
description: 'Device run session ID',
|
||||
required: true,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SimulatorStop);
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(SimulatorStop, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
const stopSpinner = ora(`🛑 Stopping device run session ${flags.id}`).start();
|
||||
try {
|
||||
const session = await DeviceRunSessionMutation.stopDeviceRunSessionAsync(
|
||||
graphqlClient,
|
||||
flags.id
|
||||
);
|
||||
stopSpinner.succeed(`🎉 Device run session ${session.id} is ${session.status.toLowerCase()}`);
|
||||
} catch (err) {
|
||||
stopSpinner.fail(`Failed to stop device run session ${flags.id}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
|
||||
import { formatAppleTeam } from './AppleTeamFormatting';
|
||||
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../../prompts';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey
|
||||
|
||||
export enum AppStoreApiKeyPurpose {
|
||||
SUBMISSION_SERVICE = 'EAS Submit',
|
||||
ASC_APP_CONNECTION = 'EAS Connect',
|
||||
}
|
||||
|
||||
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
|
||||
@@ -99,12 +101,27 @@ async function generateAscApiKeyAsync(
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const role = await selectRoleForGeneratedAscApiKeyAsync();
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, {
|
||||
nickname: getAscApiKeyName(purpose),
|
||||
roles: [role],
|
||||
});
|
||||
return await getMinimalAscApiKeyAsync(ascApiKey);
|
||||
}
|
||||
|
||||
async function selectRoleForGeneratedAscApiKeyAsync(): Promise<UserRole> {
|
||||
return await selectAsync<UserRole>('Select role for the generated API key:', [
|
||||
{
|
||||
title: 'ADMIN (default)',
|
||||
value: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
title: 'APP_MANAGER (least privilege for app management)',
|
||||
value: UserRole.APP_MANAGER,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
|
||||
const nameParts = ['[Expo]', purpose, nanoid(10)];
|
||||
return nameParts.join(' ');
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { promptAsync, selectAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
|
||||
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
|
||||
import {
|
||||
getCredentialsFromUserAsync,
|
||||
shouldAutoGenerateCredentialsAsync,
|
||||
} from '../../../utils/promptForCredentials';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
getAscApiKeyName,
|
||||
promptForAscApiKeyPathAsync,
|
||||
provideOrGenerateAscApiKeyAsync,
|
||||
} from '../AscApiKeyUtils';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
|
||||
return Object.keys(obj) as K[];
|
||||
@@ -18,7 +26,10 @@ function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
|
||||
|
||||
afterEach(() => {
|
||||
jest.mocked(promptAsync).mockClear();
|
||||
jest.mocked(selectAsync).mockClear();
|
||||
jest.mocked(getCredentialsFromUserAsync).mockClear();
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockClear();
|
||||
jest.mocked(fs.readFile).mockClear();
|
||||
});
|
||||
|
||||
describe(getAscApiKeyName, () => {
|
||||
@@ -87,3 +98,84 @@ describe(promptForAscApiKeyPathAsync, () => {
|
||||
expect(jest.mocked(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
|
||||
});
|
||||
});
|
||||
|
||||
describe(provideOrGenerateAscApiKeyAsync, () => {
|
||||
it('creates a key with ADMIN role by default', async () => {
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockResolvedValue(true);
|
||||
jest.mocked(selectAsync).mockResolvedValue(UserRole.ADMIN);
|
||||
|
||||
const createAscApiKeyAsync = jest.fn(async () => testAscApiKey);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
authCtx: testAuthCtx,
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
createAscApiKeyAsync,
|
||||
},
|
||||
});
|
||||
|
||||
await provideOrGenerateAscApiKeyAsync(ctx, AppStoreApiKeyPurpose.ASC_APP_CONNECTION);
|
||||
|
||||
expect(createAscApiKeyAsync).toHaveBeenCalledWith(ctx.analytics, {
|
||||
nickname: expect.any(String),
|
||||
roles: [UserRole.ADMIN],
|
||||
});
|
||||
});
|
||||
|
||||
it('allows creating a key with APP_MANAGER role', async () => {
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockResolvedValue(true);
|
||||
jest.mocked(selectAsync).mockResolvedValue(UserRole.APP_MANAGER);
|
||||
|
||||
const createAscApiKeyAsync = jest.fn(async () => testAscApiKey);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
authCtx: testAuthCtx,
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
createAscApiKeyAsync,
|
||||
},
|
||||
});
|
||||
|
||||
await provideOrGenerateAscApiKeyAsync(ctx, AppStoreApiKeyPurpose.ASC_APP_CONNECTION);
|
||||
|
||||
expect(createAscApiKeyAsync).toHaveBeenCalledWith(ctx.analytics, {
|
||||
nickname: expect.any(String),
|
||||
roles: [UserRole.APP_MANAGER],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not apply role override when using a user-provided key', async () => {
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockResolvedValue(false);
|
||||
jest.mocked(promptAsync).mockResolvedValue({ keyP8Path: '/asc-api-key.p8' });
|
||||
jest.mocked(fs.readFile).mockImplementation(async () => 'test-key-p8' as any);
|
||||
jest
|
||||
.mocked(getCredentialsFromUserAsync)
|
||||
.mockResolvedValueOnce({ keyId: 'test-key-id' })
|
||||
.mockResolvedValueOnce({ issuerId: 'test-issuer-id' });
|
||||
|
||||
const createAscApiKeyAsync = jest.fn(async () => testAscApiKey);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
authCtx: null,
|
||||
createAscApiKeyAsync,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await provideOrGenerateAscApiKeyAsync(
|
||||
ctx,
|
||||
AppStoreApiKeyPurpose.ASC_APP_CONNECTION
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
keyP8: expect.any(String),
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id',
|
||||
});
|
||||
expect(selectAsync).not.toHaveBeenCalled();
|
||||
expect(createAscApiKeyAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ProfileType } from '@expo/app-store';
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
|
||||
import {
|
||||
AscApiKey,
|
||||
@@ -195,10 +196,10 @@ export default class AppStoreApi {
|
||||
|
||||
public async createAscApiKeyAsync(
|
||||
analytics: Analytics,
|
||||
{ nickname }: { nickname: string }
|
||||
{ nickname, roles }: { nickname: string; roles?: UserRole[] }
|
||||
): Promise<AscApiKey> {
|
||||
const userCtx = await this.ensureUserAuthenticatedAsync();
|
||||
return await createAscApiKeyAsync(analytics, userCtx, { nickname });
|
||||
return await createAscApiKeyAsync(analytics, userCtx, { nickname, roles });
|
||||
}
|
||||
|
||||
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
|
||||
|
||||
@@ -90,6 +90,23 @@ test(`createAscApiKeyAsync`, async () => {
|
||||
expect(result).toEqual({ ...mockAscApiKeyInfo, keyP8: 'super secret' });
|
||||
});
|
||||
|
||||
test(`createAscApiKeyAsync forwards explicit roles`, async () => {
|
||||
const analytics = instance(mock<Analytics>());
|
||||
const mockRequestContext = {};
|
||||
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
|
||||
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
|
||||
await createAscApiKeyAsync(analytics, mockAuthCtx, {
|
||||
nickname: 'test-name',
|
||||
roles: [UserRole.APP_MANAGER],
|
||||
});
|
||||
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
|
||||
nickname: 'test-name',
|
||||
allAppsVisible: true,
|
||||
roles: [UserRole.APP_MANAGER],
|
||||
keyType: ApiKeyType.PUBLIC_API,
|
||||
});
|
||||
});
|
||||
|
||||
test(`revokeAscApiKeyAsync`, async () => {
|
||||
const mockRequestContext = {};
|
||||
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
|
||||
|
||||
+286
@@ -1399,6 +1399,7 @@ export type App = Project & {
|
||||
*/
|
||||
buildsReleaseChannels: Array<Scalars['String']['output']>;
|
||||
channelsPaginated: AppChannelsConnection;
|
||||
convexProject?: Maybe<ConvexProject>;
|
||||
deployment?: Maybe<Deployment>;
|
||||
/** Deployments associated with this app */
|
||||
deployments: DeploymentsConnection;
|
||||
@@ -2086,6 +2087,9 @@ export type AppNotificationPreferenceInput = {
|
||||
export type AppObserve = {
|
||||
__typename?: 'AppObserve';
|
||||
appVersions: Array<AppObserveAppVersion>;
|
||||
customEventCounts: AppObserveCustomEventCounts;
|
||||
customEventList: AppObserveCustomEventListConnection;
|
||||
customEventNames: AppObserveCustomEventNames;
|
||||
environments: Array<Scalars['String']['output']>;
|
||||
events: AppObserveEventsConnection;
|
||||
timeSeries: AppObserveTimeSeries;
|
||||
@@ -2098,6 +2102,28 @@ export type AppObserveAppVersionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveCustomEventCountsArgs = {
|
||||
input: AppObserveCustomEventCountsInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveCustomEventListArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<AppObserveCustomEventListFilter>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveCustomEventNamesArgs = {
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveEnvironmentsArgs = {
|
||||
endTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
@@ -2163,6 +2189,96 @@ export type AppObserveAppVersionMetric = {
|
||||
statistics: AppObserveVersionMarkerStatistics;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEvent = {
|
||||
__typename?: 'AppObserveCustomEvent';
|
||||
appBuildNumber: Scalars['String']['output'];
|
||||
appEasBuildId?: Maybe<Scalars['String']['output']>;
|
||||
appUpdateId?: Maybe<Scalars['String']['output']>;
|
||||
appVersion: Scalars['String']['output'];
|
||||
countryCode?: Maybe<Scalars['String']['output']>;
|
||||
deviceModel: Scalars['String']['output'];
|
||||
deviceOs: Scalars['String']['output'];
|
||||
deviceOsVersion: Scalars['String']['output'];
|
||||
easClientId: Scalars['String']['output'];
|
||||
environment?: Maybe<Scalars['String']['output']>;
|
||||
eventName: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
properties: Array<AppObserveEventProperty>;
|
||||
sessionId?: Maybe<Scalars['String']['output']>;
|
||||
severityNumber?: Maybe<Scalars['Int']['output']>;
|
||||
severityText?: Maybe<Scalars['String']['output']>;
|
||||
timestamp: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventCountBucket = {
|
||||
__typename?: 'AppObserveCustomEventCountBucket';
|
||||
bucket: Scalars['DateTime']['output'];
|
||||
count: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventCounts = {
|
||||
__typename?: 'AppObserveCustomEventCounts';
|
||||
buckets: Array<AppObserveCustomEventCountBucket>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventCountsInput = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
bucketIntervalMinutes?: InputMaybe<Scalars['Int']['input']>;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
eventName: Scalars['String']['input'];
|
||||
platform: AppObservePlatform;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventEdge = {
|
||||
__typename?: 'AppObserveCustomEventEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
node: AppObserveCustomEvent;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventListConnection = {
|
||||
__typename?: 'AppObserveCustomEventListConnection';
|
||||
edges: Array<AppObserveCustomEventEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventListFilter = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
easClientId?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
eventName?: InputMaybe<Scalars['String']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
propertyFilters?: InputMaybe<Array<AppObserveCustomEventPropertyFilter>>;
|
||||
sessionId?: InputMaybe<Scalars['String']['input']>;
|
||||
startTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventName = {
|
||||
__typename?: 'AppObserveCustomEventName';
|
||||
count: Scalars['Int']['output'];
|
||||
eventName: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventNames = {
|
||||
__typename?: 'AppObserveCustomEventNames';
|
||||
isTruncated: Scalars['Boolean']['output'];
|
||||
names: Array<AppObserveCustomEventName>;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventPropertyFilter = {
|
||||
key: Scalars['String']['input'];
|
||||
value: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveEvent = {
|
||||
__typename?: 'AppObserveEvent';
|
||||
appBuildNumber: Scalars['String']['output'];
|
||||
@@ -2202,6 +2318,13 @@ export type AppObserveEventEdge = {
|
||||
node: AppObserveEvent;
|
||||
};
|
||||
|
||||
export type AppObserveEventProperty = {
|
||||
__typename?: 'AppObserveEventProperty';
|
||||
key: Scalars['String']['output'];
|
||||
type: AppObservePropertyType;
|
||||
value: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveEventsConnection = {
|
||||
__typename?: 'AppObserveEventsConnection';
|
||||
edges: Array<AppObserveEventEdge>;
|
||||
@@ -2242,6 +2365,13 @@ export enum AppObservePlatform {
|
||||
Ios = 'IOS'
|
||||
}
|
||||
|
||||
export enum AppObservePropertyType {
|
||||
Boolean = 'BOOLEAN',
|
||||
Json = 'JSON',
|
||||
Number = 'NUMBER',
|
||||
String = 'STRING'
|
||||
}
|
||||
|
||||
export type AppObserveReleasesInput = {
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -2850,6 +2980,23 @@ export type AppleDeviceRegistrationRequestMutationCreateAppleDeviceRegistrationR
|
||||
appleTeamId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
/** Publicly visible data for an AppleDeviceRegistrationRequest. */
|
||||
export type AppleDeviceRegistrationRequestPublicData = {
|
||||
__typename?: 'AppleDeviceRegistrationRequestPublicData';
|
||||
id: Scalars['ID']['output'];
|
||||
};
|
||||
|
||||
export type AppleDeviceRegistrationRequestPublicDataQuery = {
|
||||
__typename?: 'AppleDeviceRegistrationRequestPublicDataQuery';
|
||||
/** Get AppleDeviceRegistrationRequest public data by ID */
|
||||
byId?: Maybe<AppleDeviceRegistrationRequestPublicData>;
|
||||
};
|
||||
|
||||
|
||||
export type AppleDeviceRegistrationRequestPublicDataQueryByIdArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AppleDeviceRegistrationRequestQuery = {
|
||||
__typename?: 'AppleDeviceRegistrationRequestQuery';
|
||||
byId: AppleDeviceRegistrationRequest;
|
||||
@@ -4072,6 +4219,18 @@ export type ConvexIntegrationQuery = {
|
||||
clientIdentifier: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ConvexProject = {
|
||||
__typename?: 'ConvexProject';
|
||||
app: App;
|
||||
convexProjectIdentifier: Scalars['String']['output'];
|
||||
convexProjectName: Scalars['String']['output'];
|
||||
convexProjectSlug: Scalars['String']['output'];
|
||||
convexTeamConnection: ConvexTeamConnection;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type ConvexTeamConnection = {
|
||||
__typename?: 'ConvexTeamConnection';
|
||||
account: Account;
|
||||
@@ -4173,6 +4332,17 @@ export type CreateConvexTeamConnectionInput = {
|
||||
deploymentRegion: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CreateDeviceRunSessionInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
/**
|
||||
* The version of the package backing the device run session (e.g. "0.1.3-alpha.3").
|
||||
* If omitted, consumers treat the session as pinned to "latest".
|
||||
*/
|
||||
packageVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
platform: AppPlatform;
|
||||
type: DeviceRunSessionType;
|
||||
};
|
||||
|
||||
export type CreateEchoChatInput = {
|
||||
agentMetadata?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
agentType?: InputMaybe<EchoAgentType>;
|
||||
@@ -4770,6 +4940,65 @@ export type DeploymentsMutationDeleteWorkerDeploymentByIdentifierArgs = {
|
||||
deploymentIdentifier: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type DeviceRunSession = {
|
||||
__typename?: 'DeviceRunSession';
|
||||
app: App;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
finishedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
/**
|
||||
* The version of the package backing the device run session. Null means the session is
|
||||
* pinned to "latest" at the consumer side.
|
||||
*/
|
||||
packageVersion?: Maybe<Scalars['String']['output']>;
|
||||
platform: AppPlatform;
|
||||
startedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
status: DeviceRunSessionStatus;
|
||||
turtleJobRun?: Maybe<JobRun>;
|
||||
type: DeviceRunSessionType;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionMutation = {
|
||||
__typename?: 'DeviceRunSessionMutation';
|
||||
/** Create a device run session */
|
||||
createDeviceRunSession: DeviceRunSession;
|
||||
/** Stop a device run session */
|
||||
stopDeviceRunSession: DeviceRunSession;
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutationCreateDeviceRunSessionArgs = {
|
||||
deviceRunSessionInput: CreateDeviceRunSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionMutationStopDeviceRunSessionArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionQuery = {
|
||||
__typename?: 'DeviceRunSessionQuery';
|
||||
byId: DeviceRunSession;
|
||||
};
|
||||
|
||||
|
||||
export type DeviceRunSessionQueryByIdArgs = {
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export enum DeviceRunSessionStatus {
|
||||
Errored = 'ERRORED',
|
||||
InProgress = 'IN_PROGRESS',
|
||||
New = 'NEW',
|
||||
Stopped = 'STOPPED'
|
||||
}
|
||||
|
||||
export enum DeviceRunSessionType {
|
||||
AgentDevice = 'AGENT_DEVICE'
|
||||
}
|
||||
|
||||
export type DiscordUser = {
|
||||
__typename?: 'DiscordUser';
|
||||
discordIdentifier: Scalars['String']['output'];
|
||||
@@ -5523,6 +5752,8 @@ export enum EntityTypeName {
|
||||
BillingContractEntity = 'BillingContractEntity',
|
||||
BranchEntity = 'BranchEntity',
|
||||
ChannelEntity = 'ChannelEntity',
|
||||
ConvexProjectEntity = 'ConvexProjectEntity',
|
||||
ConvexTeamConnectionEntity = 'ConvexTeamConnectionEntity',
|
||||
CustomerEntity = 'CustomerEntity',
|
||||
EchoProjectEntity = 'EchoProjectEntity',
|
||||
EchoVersionEntity = 'EchoVersionEntity',
|
||||
@@ -7610,6 +7841,8 @@ export type RootMutation = {
|
||||
deployments: DeploymentsMutation;
|
||||
/** Mutations that assign or modify DevDomainNames for apps */
|
||||
devDomainName: AppDevDomainNameMutation;
|
||||
/** Mutations that create and stop device run sessions */
|
||||
deviceRunSession: DeviceRunSessionMutation;
|
||||
/** Mutations for Discord users */
|
||||
discordUser: DiscordUserMutation;
|
||||
/** Mutations for Echo chats */
|
||||
@@ -7745,6 +7978,8 @@ export type RootQuery = {
|
||||
appStoreConnectApiKey: AppStoreConnectApiKeyQuery;
|
||||
/** Top-level query object for querying Apple Device registration requests. */
|
||||
appleDeviceRegistrationRequest: AppleDeviceRegistrationRequestQuery;
|
||||
/** Top-level query object for querying AppleDeviceRegistrationRequest publicly. */
|
||||
appleDeviceRegistrationRequestPublicData: AppleDeviceRegistrationRequestPublicDataQuery;
|
||||
/** Top-level query object for querying Apple distribution certificates. */
|
||||
appleDistributionCertificate?: Maybe<AppleDistributionCertificateQuery>;
|
||||
/** Top-level query object for querying Apple provisioning profiles. */
|
||||
@@ -7768,6 +8003,7 @@ export type RootQuery = {
|
||||
convexIntegration: ConvexIntegrationQuery;
|
||||
/** Top-level query object for querying Deployments. */
|
||||
deployments: DeploymentQuery;
|
||||
deviceRunSessions: DeviceRunSessionQuery;
|
||||
/** Top-level query object for querying Echo chats. */
|
||||
echoChat: EchoChatQuery;
|
||||
/** Top-level query object for querying Echo messages. */
|
||||
@@ -11618,6 +11854,20 @@ export type CreateAppVersionMutationVariables = Exact<{
|
||||
|
||||
export type CreateAppVersionMutation = { __typename?: 'RootMutation', appVersion: { __typename?: 'AppVersionMutation', createAppVersion: { __typename?: 'AppVersion', id: string } } };
|
||||
|
||||
export type CreateAppStoreConnectAppMutationVariables = Exact<{
|
||||
appStoreConnectAppInput: AppStoreConnectAppInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAppStoreConnectAppMutation = { __typename?: 'RootMutation', appStoreConnectApp: { __typename?: 'AppStoreConnectAppMutation', createAppStoreConnectApp: { __typename?: 'AppStoreConnectApp', id: string, ascAppIdentifier: string } } };
|
||||
|
||||
export type DeleteAppStoreConnectAppMutationVariables = Exact<{
|
||||
appStoreConnectAppId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteAppStoreConnectAppMutation = { __typename?: 'RootMutation', appStoreConnectApp: { __typename?: 'AppStoreConnectAppMutation', deleteAppStoreConnectApp: { __typename?: 'DeleteAppStoreConnectAppResult', id: string } } };
|
||||
|
||||
export type CreateAndroidBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
job: AndroidJobInput;
|
||||
@@ -11654,6 +11904,20 @@ export type RetryIosBuildMutationVariables = Exact<{
|
||||
|
||||
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __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 CreateDeviceRunSessionMutationVariables = Exact<{
|
||||
deviceRunSessionInput: CreateDeviceRunSessionInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateDeviceRunSessionMutation = { __typename?: 'RootMutation', deviceRunSession: { __typename?: 'DeviceRunSessionMutation', createDeviceRunSession: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, turtleJobRun?: { __typename?: 'JobRun', id: string } | null } } };
|
||||
|
||||
export type StopDeviceRunSessionMutationVariables = Exact<{
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type StopDeviceRunSessionMutation = { __typename?: 'RootMutation', deviceRunSession: { __typename?: 'DeviceRunSessionMutation', stopDeviceRunSession: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus } } };
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
accountId: Scalars['String']['input'];
|
||||
@@ -11963,6 +12227,21 @@ export type LatestAppVersionQueryVariables = Exact<{
|
||||
|
||||
export type LatestAppVersionQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, latestAppVersionByPlatformAndApplicationIdentifier?: { __typename?: 'AppVersion', id: string, storeVersion: string, buildVersion: string } | null } } };
|
||||
|
||||
export type AscAppLinkAppMetadataQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AscAppLinkAppMetadataQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, fullName: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'PartnerActor', id: string } | { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, appStoreConnectApp?: { __typename?: 'AppStoreConnectApp', id: string, ascAppIdentifier: string, remoteAppStoreConnectApp: { __typename?: 'RemoteAppStoreConnectApp', ascAppIdentifier: string, bundleIdentifier: string, name?: string | null, appStoreIconUrl?: string | null } } | null } } };
|
||||
|
||||
export type DiscoverAccessibleAppStoreConnectAppsQueryVariables = Exact<{
|
||||
appStoreConnectApiKeyId: Scalars['ID']['input'];
|
||||
bundleIdentifier?: InputMaybe<Scalars['String']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type DiscoverAccessibleAppStoreConnectAppsQuery = { __typename?: 'RootQuery', appStoreConnectApiKey: { __typename?: 'AppStoreConnectApiKeyQuery', byId: { __typename?: 'AppStoreConnectApiKey', id: string, remoteAppStoreConnectApps: Array<{ __typename?: 'RemoteAppStoreConnectApp', ascAppIdentifier: string, bundleIdentifier: string, name?: string | null, appStoreIconUrl?: string | null }> } } };
|
||||
|
||||
export type GetAssetSignedUrlsQueryVariables = Exact<{
|
||||
updateId: Scalars['ID']['input'];
|
||||
storageKeys: Array<Scalars['String']['input']> | Scalars['String']['input'];
|
||||
@@ -12106,6 +12385,13 @@ export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{
|
||||
|
||||
export type ViewUpdateChannelsPaginatedOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, channelsPaginated: { __typename?: 'AppChannelsConnection', edges: Array<{ __typename?: 'AppChannelEdge', cursor: string, node: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } } };
|
||||
|
||||
export type DeviceRunSessionByIdQueryVariables = Exact<{
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeviceRunSessionByIdQuery = { __typename?: 'RootQuery', deviceRunSessions: { __typename?: 'DeviceRunSessionQuery', byId: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, turtleJobRun?: { __typename?: 'JobRun', id: string, status: JobRunStatus, logFileUrls: Array<string> } | null } } };
|
||||
|
||||
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
|
||||
export const AscAppLinkMutation = {
|
||||
async createAppStoreConnectAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appStoreConnectAppInput: {
|
||||
appId: string;
|
||||
ascAppIdentifier: string;
|
||||
appStoreConnectApiKeyId: string;
|
||||
}
|
||||
): Promise<{ id: string; ascAppIdentifier: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appStoreConnectApp: {
|
||||
createAppStoreConnectApp: {
|
||||
id: string;
|
||||
ascAppIdentifier: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
gql`
|
||||
mutation CreateAppStoreConnectApp($appStoreConnectAppInput: AppStoreConnectAppInput!) {
|
||||
appStoreConnectApp {
|
||||
createAppStoreConnectApp(appStoreConnectAppInput: $appStoreConnectAppInput) {
|
||||
id
|
||||
ascAppIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appStoreConnectAppInput }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.appStoreConnectApp.createAppStoreConnectApp;
|
||||
},
|
||||
|
||||
async deleteAppStoreConnectAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appStoreConnectAppId: string
|
||||
): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appStoreConnectApp: {
|
||||
deleteAppStoreConnectApp: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
gql`
|
||||
mutation DeleteAppStoreConnectApp($appStoreConnectAppId: ID!) {
|
||||
appStoreConnectApp {
|
||||
deleteAppStoreConnectApp(appStoreConnectAppId: $appStoreConnectAppId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appStoreConnectAppId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
CreateDeviceRunSessionInput,
|
||||
CreateDeviceRunSessionMutation,
|
||||
CreateDeviceRunSessionMutationVariables,
|
||||
StopDeviceRunSessionMutation,
|
||||
StopDeviceRunSessionMutationVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const DeviceRunSessionMutation = {
|
||||
async createDeviceRunSessionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
deviceRunSessionInput: CreateDeviceRunSessionInput
|
||||
): Promise<CreateDeviceRunSessionMutation['deviceRunSession']['createDeviceRunSession']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateDeviceRunSessionMutation, CreateDeviceRunSessionMutationVariables>(
|
||||
gql`
|
||||
mutation CreateDeviceRunSessionMutation($deviceRunSessionInput: CreateDeviceRunSessionInput!) {
|
||||
deviceRunSession {
|
||||
createDeviceRunSession(deviceRunSessionInput: $deviceRunSessionInput) {
|
||||
id
|
||||
status
|
||||
app {
|
||||
id
|
||||
slug
|
||||
ownerAccount {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
turtleJobRun {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ deviceRunSessionInput },
|
||||
{ noRetry: true }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.deviceRunSession.createDeviceRunSession;
|
||||
},
|
||||
async stopDeviceRunSessionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
deviceRunSessionId: string
|
||||
): Promise<StopDeviceRunSessionMutation['deviceRunSession']['stopDeviceRunSession']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<StopDeviceRunSessionMutation, StopDeviceRunSessionMutationVariables>(
|
||||
gql`
|
||||
mutation StopDeviceRunSessionMutation($deviceRunSessionId: ID!) {
|
||||
deviceRunSession {
|
||||
stopDeviceRunSession(deviceRunSessionId: $deviceRunSessionId) {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ deviceRunSessionId },
|
||||
{ noRetry: true }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.deviceRunSession.stopDeviceRunSession;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AscAppLinkAppMetadataQuery,
|
||||
AscAppLinkAppMetadataQueryVariables,
|
||||
DiscoverAccessibleAppStoreConnectAppsQuery,
|
||||
DiscoverAccessibleAppStoreConnectAppsQueryVariables,
|
||||
} from '../generated';
|
||||
import { AccountFragmentNode } from '../types/Account';
|
||||
|
||||
export const AscAppLinkQuery = {
|
||||
async getAppMetadataAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string,
|
||||
options?: {
|
||||
useCache?: boolean;
|
||||
}
|
||||
): Promise<AscAppLinkAppMetadataQuery['app']['byId']> {
|
||||
const useCache = options?.useCache ?? true;
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AscAppLinkAppMetadataQuery, AscAppLinkAppMetadataQueryVariables>(
|
||||
gql`
|
||||
query AscAppLinkAppMetadata($appId: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
fullName
|
||||
ownerAccount {
|
||||
id
|
||||
...AccountFragment
|
||||
}
|
||||
appStoreConnectApp {
|
||||
id
|
||||
ascAppIdentifier
|
||||
remoteAppStoreConnectApp {
|
||||
ascAppIdentifier
|
||||
bundleIdentifier
|
||||
name
|
||||
appStoreIconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AccountFragmentNode)}
|
||||
`,
|
||||
{ appId },
|
||||
{
|
||||
requestPolicy: useCache ? 'cache-first' : 'network-only',
|
||||
additionalTypenames: ['App', 'AppStoreConnectApp'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app.byId;
|
||||
},
|
||||
|
||||
async discoverAccessibleAppsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appStoreConnectApiKeyId: string,
|
||||
bundleIdentifier?: string
|
||||
): Promise<
|
||||
DiscoverAccessibleAppStoreConnectAppsQuery['appStoreConnectApiKey']['byId']['remoteAppStoreConnectApps']
|
||||
> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<
|
||||
DiscoverAccessibleAppStoreConnectAppsQuery,
|
||||
DiscoverAccessibleAppStoreConnectAppsQueryVariables
|
||||
>(
|
||||
gql`
|
||||
query DiscoverAccessibleAppStoreConnectApps(
|
||||
$appStoreConnectApiKeyId: ID!
|
||||
$bundleIdentifier: String
|
||||
) {
|
||||
appStoreConnectApiKey {
|
||||
byId(id: $appStoreConnectApiKeyId) {
|
||||
id
|
||||
remoteAppStoreConnectApps(bundleIdentifier: $bundleIdentifier) {
|
||||
ascAppIdentifier
|
||||
bundleIdentifier
|
||||
name
|
||||
appStoreIconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appStoreConnectApiKeyId, bundleIdentifier },
|
||||
{ additionalTypenames: ['AppStoreConnectApp'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.appStoreConnectApiKey.byId?.remoteAppStoreConnectApps ?? [];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import { DeviceRunSessionByIdQuery, DeviceRunSessionByIdQueryVariables } from '../generated';
|
||||
|
||||
export const DeviceRunSessionQuery = {
|
||||
async byIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
deviceRunSessionId: string
|
||||
): Promise<DeviceRunSessionByIdQuery['deviceRunSessions']['byId']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<DeviceRunSessionByIdQuery, DeviceRunSessionByIdQueryVariables>(
|
||||
gql`
|
||||
query DeviceRunSessionByIdQuery($deviceRunSessionId: ID!) {
|
||||
deviceRunSessions {
|
||||
byId(deviceRunSessionId: $deviceRunSessionId) {
|
||||
id
|
||||
status
|
||||
turtleJobRun {
|
||||
id
|
||||
status
|
||||
logFileUrls
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ deviceRunSessionId },
|
||||
{ requestPolicy: 'network-only' }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.deviceRunSessions.byId;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import {
|
||||
buildInvalidJsonOutput,
|
||||
buildJsonOutput,
|
||||
formatAscAppLinkStatus,
|
||||
isAscAuthenticationError,
|
||||
} from '../utils';
|
||||
|
||||
const mockMetadataConnected = {
|
||||
id: 'app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: 'app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
describe('buildJsonOutput', () => {
|
||||
it('returns connected output', () => {
|
||||
const output = buildJsonOutput('status', mockMetadataConnected);
|
||||
expect(output.action).toBe('status');
|
||||
expect(output.project).toBe('@testuser/testapp');
|
||||
expect(output.status).toBe('connected');
|
||||
expect(output.appStoreConnectApp).not.toBeNull();
|
||||
expect(output.appStoreConnectApp!.ascAppIdentifier).toBe('1234567890');
|
||||
expect(output.appStoreConnectApp!.name).toBe('Test App');
|
||||
expect(output.appStoreConnectApp!.bundleIdentifier).toBe('com.test.app');
|
||||
});
|
||||
|
||||
it('returns disconnected output', () => {
|
||||
const output = buildJsonOutput('status', mockMetadataDisconnected);
|
||||
expect(output.status).toBe('not-connected');
|
||||
expect(output.appStoreConnectApp).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInvalidJsonOutput', () => {
|
||||
it('returns invalid output', () => {
|
||||
const output = buildInvalidJsonOutput('status', 'project-id');
|
||||
expect(output.status).toBe('invalid');
|
||||
expect(output.project).toBe('project-id');
|
||||
expect(output.appStoreConnectApp).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAscAuthenticationError', () => {
|
||||
it('returns true for ASC authentication errors', () => {
|
||||
const error = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
expect(isAscAuthenticationError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for other CombinedErrors', () => {
|
||||
const error = new CombinedError({
|
||||
graphQLErrors: [new GraphQLError('Some other error')],
|
||||
});
|
||||
expect(isAscAuthenticationError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-CombinedError errors', () => {
|
||||
expect(isAscAuthenticationError(new Error('random error'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatAscAppLinkStatus', () => {
|
||||
it('formats connected status', () => {
|
||||
const status = formatAscAppLinkStatus(mockMetadataConnected);
|
||||
expect(status).toContain('Connected');
|
||||
expect(status).toContain('1234567890');
|
||||
expect(status).toContain('Test App');
|
||||
expect(status).toContain('com.test.app');
|
||||
});
|
||||
|
||||
it('formats disconnected status', () => {
|
||||
const status = formatAscAppLinkStatus(mockMetadataDisconnected);
|
||||
expect(status).toContain('Not connected');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
formatAscApiKey,
|
||||
provideOrGenerateAscApiKeyAsync,
|
||||
sortAscApiKeysByUpdatedAtDesc,
|
||||
} from '../../credentials/ios/actions/AscApiKeyUtils';
|
||||
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../graphql/generated';
|
||||
|
||||
export async function selectOrCreateAscApiKeyIdAsync({
|
||||
credentialsContext,
|
||||
existingKeys,
|
||||
ownerAccount,
|
||||
}: {
|
||||
credentialsContext: CredentialsContext;
|
||||
existingKeys: AppStoreConnectApiKeyFragment[];
|
||||
ownerAccount: AccountFragment;
|
||||
}): Promise<string> {
|
||||
const sortedKeys = sortAscApiKeysByUpdatedAtDesc(existingKeys);
|
||||
const createKeyOption = {
|
||||
title: '[Create or upload a new API key]',
|
||||
value: '__create_new_key__',
|
||||
};
|
||||
|
||||
const selectedValue =
|
||||
sortedKeys.length === 0
|
||||
? createKeyOption.value
|
||||
: await selectAsync<string>('Select an App Store Connect API key:', [
|
||||
...sortedKeys.map(key => ({
|
||||
title: formatAscApiKey(key),
|
||||
value: key.id,
|
||||
})),
|
||||
createKeyOption,
|
||||
]);
|
||||
|
||||
if (selectedValue !== createKeyOption.value) {
|
||||
return selectedValue;
|
||||
}
|
||||
|
||||
const newKey = await credentialsContext.ios.createAscApiKeyAsync(
|
||||
credentialsContext.graphqlClient,
|
||||
ownerAccount,
|
||||
await provideOrGenerateAscApiKeyAsync(
|
||||
credentialsContext,
|
||||
AppStoreApiKeyPurpose.ASC_APP_CONNECTION
|
||||
)
|
||||
);
|
||||
return newKey.id;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AscAppLinkQuery } from '../../graphql/queries/AscAppLinkQuery';
|
||||
|
||||
type AscAppLinkMetadata = Awaited<ReturnType<typeof AscAppLinkQuery.getAppMetadataAsync>>;
|
||||
|
||||
export type AscAppLinkStatus = 'not-connected' | 'connected' | 'invalid';
|
||||
|
||||
export interface AscAppLinkJsonOutput {
|
||||
action: string;
|
||||
project: string;
|
||||
status: AscAppLinkStatus;
|
||||
appStoreConnectApp: {
|
||||
id: string;
|
||||
ascAppIdentifier: string;
|
||||
name: string | null;
|
||||
bundleIdentifier: string | null;
|
||||
appleUrl: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function buildJsonOutput(
|
||||
action: string,
|
||||
metadata: AscAppLinkMetadata
|
||||
): AscAppLinkJsonOutput {
|
||||
const link = metadata.appStoreConnectApp;
|
||||
return {
|
||||
action,
|
||||
project: metadata.fullName,
|
||||
status: link ? 'connected' : 'not-connected',
|
||||
appStoreConnectApp: link
|
||||
? {
|
||||
id: link.id,
|
||||
ascAppIdentifier: link.ascAppIdentifier,
|
||||
name: link.remoteAppStoreConnectApp.name ?? null,
|
||||
bundleIdentifier: link.remoteAppStoreConnectApp.bundleIdentifier ?? null,
|
||||
appleUrl: getAppleAppUrl(link.ascAppIdentifier),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInvalidJsonOutput(action: string, projectId: string): AscAppLinkJsonOutput {
|
||||
return {
|
||||
action,
|
||||
project: projectId,
|
||||
status: 'invalid',
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function isAscAuthenticationError(error: unknown): error is CombinedError {
|
||||
return (
|
||||
error instanceof CombinedError &&
|
||||
error.graphQLErrors.some(e => e.message.includes('App Store Connect rejected this API key'))
|
||||
);
|
||||
}
|
||||
|
||||
export function formatAscAppLinkStatus(metadata: AscAppLinkMetadata): string {
|
||||
const link = metadata.appStoreConnectApp;
|
||||
if (!link) {
|
||||
return `Project ${chalk.bold(metadata.fullName)}: ${chalk.yellow('Not connected')} to App Store Connect.`;
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
`Project ${chalk.bold(metadata.fullName)}: ${chalk.green('Connected')} to App Store Connect.`,
|
||||
` ASC App ID: ${chalk.bold(link.ascAppIdentifier)}`,
|
||||
];
|
||||
|
||||
const remote = link.remoteAppStoreConnectApp;
|
||||
lines.push(` Name: ${remote.name}`);
|
||||
lines.push(` Bundle ID: ${remote.bundleIdentifier}`);
|
||||
lines.push(` Apple URL: ${getAppleAppUrl(link.ascAppIdentifier)}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getAppleAppUrl(ascAppIdentifier: string): string {
|
||||
return `https://appstoreconnect.apple.com/apps/${encodeURIComponent(ascAppIdentifier)}/distribution`;
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function wrapFetchWithProgress(): (
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadFileWithProgressTrackerAsync(
|
||||
export async function downloadFileWithProgressTrackerAsync(
|
||||
url: string,
|
||||
outputPath: string,
|
||||
progressTrackerMessage: string | ((ratio: number, total: number) => string),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.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": "18.8.0",
|
||||
"@expo/build-tools": "18.9.0",
|
||||
"@expo/eas-build-job": "18.8.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { JobInterpolationContext } from '@expo/eas-build-job';
|
||||
|
||||
import { interpolateJobContext } from '../interpolation';
|
||||
|
||||
describe(interpolateJobContext, () => {
|
||||
it('interpolates multiple ${{ }} segments when the string starts with ${{ and ends with }}', () => {
|
||||
const context = {
|
||||
github: {
|
||||
event: {
|
||||
pull_request: {
|
||||
html_url: 'https://github.com/acme/app/pull/42',
|
||||
title: 'Fix crash',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as JobInterpolationContext;
|
||||
|
||||
expect(
|
||||
interpolateJobContext({
|
||||
target: '${{ github.event.pull_request.html_url }}: ${{ github.event.pull_request.title }}',
|
||||
context,
|
||||
})
|
||||
).toBe('https://github.com/acme/app/pull/42: Fix crash');
|
||||
});
|
||||
|
||||
it('still preserves non-string types for a single whole-string interpolation', () => {
|
||||
const context = {
|
||||
build: { profile: 'production', id: '123' },
|
||||
} as unknown as JobInterpolationContext;
|
||||
|
||||
expect(
|
||||
interpolateJobContext({
|
||||
target: '${{ build }}',
|
||||
context,
|
||||
})
|
||||
).toEqual({ profile: 'production', id: '123' });
|
||||
});
|
||||
|
||||
it("handles a single interpolation when the expression contains '}}' in a string literal", () => {
|
||||
const context = {
|
||||
build: { profile: 'production}}' },
|
||||
} as unknown as JobInterpolationContext;
|
||||
|
||||
expect(
|
||||
interpolateJobContext({
|
||||
target: "${{ build.profile == 'production}}' }}",
|
||||
context,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,10 @@ import { JobInterpolationContext } from '@expo/eas-build-job';
|
||||
|
||||
import { jsepEval } from './utils/jsepEval';
|
||||
|
||||
function isSingleWholeStringInterpolation(value: string): boolean {
|
||||
return value.startsWith('${{') && value.endsWith('}}') && !value.includes('${{', 3);
|
||||
}
|
||||
|
||||
export function interpolateJobContext({
|
||||
target,
|
||||
context,
|
||||
@@ -12,7 +16,7 @@ export function interpolateJobContext({
|
||||
if (typeof target === 'string') {
|
||||
// If the value is e.g. `build: ${{ inputs.build }}`, we will interpolate the value
|
||||
// without changing `inputs.build` type, i.e. if it is an object it'll be like `build: {...inputs.build}`.
|
||||
if (target.startsWith('${{') && target.endsWith('}}')) {
|
||||
if (isSingleWholeStringInterpolation(target)) {
|
||||
return jsepEval(target.slice(3, -2), context);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,3 +14,4 @@ if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
|
||||
|
||||
// Always mock:
|
||||
jest.mock('../src/CacheManager');
|
||||
jest.mock('../src/utils/turtleFetch');
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.8.0",
|
||||
"@expo/build-tools": "18.9.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "18.5.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.8.0",
|
||||
"@expo/steps": "18.9.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@hapi/boom": "10.0.1",
|
||||
|
||||
@@ -70,6 +70,7 @@ jest.mock('../config', () => {
|
||||
default: {
|
||||
...config.default,
|
||||
buildId: 'e9b99e52-fb74-4927-be63-33d7447ddfd4',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ jest.mock('../config', () => {
|
||||
return {
|
||||
...config,
|
||||
buildId: 'e9b99e52-fb74-4927-be63-33d7447ddfd4',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ jest.mock('../config', () => {
|
||||
return {
|
||||
...config,
|
||||
buildId: 'f38532aa-81a8-4db7-915f-6e7afe46e22f',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ jest.mock('../config', () => {
|
||||
return {
|
||||
...config,
|
||||
buildId: 'e9b99e52-fb74-4927-be63-33d7447ddfd4',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
};
|
||||
});
|
||||
jest.mock('../upload', () => {
|
||||
|
||||
@@ -5,9 +5,11 @@ import { getBuildEnv } from '../env';
|
||||
|
||||
describe(getBuildEnv.name, () => {
|
||||
const originalSentryDsn = config.sentry.dsn;
|
||||
const originalCocoapodsCacheUrl = config.cocoapodsCacheUrl;
|
||||
|
||||
afterEach(() => {
|
||||
config.sentry.dsn = originalSentryDsn;
|
||||
config.cocoapodsCacheUrl = originalCocoapodsCacheUrl;
|
||||
});
|
||||
|
||||
it('passes the CLI sentry DSN to worker child processes', () => {
|
||||
@@ -55,6 +57,36 @@ describe(getBuildEnv.name, () => {
|
||||
});
|
||||
|
||||
expect(env.EXPO_USE_PRECOMPILED_MODULES).toBe('1');
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_BASE_URL).toBe(
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules/'
|
||||
);
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_PATH).toBeUndefined();
|
||||
});
|
||||
|
||||
it('passes precompiled modules through the CocoaPods cache URL when available', () => {
|
||||
config.cocoapodsCacheUrl = 'https://cache.example.com';
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
builderEnvironment: {
|
||||
env: {
|
||||
EAS_USE_PRECOMPILED_MODULES: '1',
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_BASE_URL).toBe(
|
||||
'https://cache.example.com/storage.googleapis.com/eas-build-precompiled-modules/'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
} from './external/turtle';
|
||||
import { getAccessedEnvs } from './utils/env';
|
||||
|
||||
const PRECOMPILED_MODULES_BASE_URL =
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules/';
|
||||
|
||||
// keep in sync with local-build-plugin env vars
|
||||
// see packages/local-build-plugin/src/build.ts
|
||||
export function getBuildEnv({
|
||||
@@ -55,6 +58,7 @@ export function getBuildEnv({
|
||||
setEnv(env, 'COMPILER_INDEX_STORE_ENABLE', 'NO');
|
||||
if (job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES === '1') {
|
||||
setEnv(env, 'EXPO_USE_PRECOMPILED_MODULES', '1');
|
||||
setEnv(env, 'EXPO_PRECOMPILED_MODULES_BASE_URL', getPrecompiledModulesBaseUrl());
|
||||
}
|
||||
|
||||
if (job.builderEnvironment?.env?.EAS_USE_CACHE === '1') {
|
||||
@@ -166,6 +170,18 @@ function setEnv(env: Env, key: string, value: string | null | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getPrecompiledModulesBaseUrl(): string {
|
||||
if (!config.cocoapodsCacheUrl) {
|
||||
return PRECOMPILED_MODULES_BASE_URL;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(PRECOMPILED_MODULES_BASE_URL);
|
||||
return PRECOMPILED_MODULES_BASE_URL.replace(
|
||||
`${parsedUrl.protocol}//${parsedUrl.host}`,
|
||||
`${config.cocoapodsCacheUrl.replace(/\/$/, '')}/${parsedUrl.host}`
|
||||
);
|
||||
}
|
||||
|
||||
const ResourceClassToMaxHeapSize: Record<ResourceClass, string | null> = {
|
||||
[ResourceClass.LINUX_C3D_STANDARD_4]: '4g', // 4 out of 16 GB, 12 GB left
|
||||
[ResourceClass.LINUX_C3D_STANDARD_8]: '8g', // 8 out of 32 GB, 24 GB left
|
||||
|
||||
@@ -332,7 +332,7 @@ export default class BuildService {
|
||||
});
|
||||
|
||||
const robotAccessToken = job.secrets?.robotAccessToken;
|
||||
if (robotAccessToken && err.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
|
||||
if (robotAccessToken) {
|
||||
let rawErrorMessage: string = '';
|
||||
if (maybeRawError?.stderr) {
|
||||
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stderr);
|
||||
@@ -340,23 +340,42 @@ export default class BuildService {
|
||||
if (maybeRawError?.stdout) {
|
||||
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stdout);
|
||||
}
|
||||
if (!rawErrorMessage) {
|
||||
rawErrorMessage = maybeRawError?.message ?? err.message;
|
||||
}
|
||||
|
||||
await turtleFetch(
|
||||
new URL('turtle-builds/error-logs', config.wwwApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: {
|
||||
buildId: this.buildId,
|
||||
message: rawErrorMessage,
|
||||
buildPhase: err.buildPhase ?? null,
|
||||
errorCode: err.errorCode,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
shouldThrowOnNotOk: false,
|
||||
}
|
||||
);
|
||||
try {
|
||||
await turtleFetch(
|
||||
new URL('turtle-builds/error-logs', config.wwwApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: {
|
||||
buildId: this.buildId,
|
||||
message: rawErrorMessage,
|
||||
buildPhase: err.buildPhase ?? null,
|
||||
errorCode: err.errorCode,
|
||||
tags: {
|
||||
platform: job.platform,
|
||||
workflow: job.type,
|
||||
sdk_version: metadata?.sdkVersion ?? null,
|
||||
react_native_version: metadata?.reactNativeVersion ?? null,
|
||||
app_id: job.appId ?? null,
|
||||
build_profile: metadata?.buildProfile ?? null,
|
||||
app_name: metadata?.appName ?? null,
|
||||
app_identifier: metadata?.appIdentifier ?? null,
|
||||
distribution: metadata?.distribution ?? null,
|
||||
cli_version: metadata?.cliVersion ?? null,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
shouldThrowOnNotOk: false,
|
||||
}
|
||||
);
|
||||
} catch (fetchError: any) {
|
||||
logger.warn({ err: fetchError }, 'Failed to send build error log');
|
||||
}
|
||||
}
|
||||
|
||||
await this.finishError(err, maybeArtifacts);
|
||||
|
||||
@@ -2856,7 +2856,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:18.8.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:18.9.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
@@ -2871,7 +2871,7 @@ __metadata:
|
||||
"@expo/repack-app": "npm:~0.4.1"
|
||||
"@expo/results": "npm:^1.0.0"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@expo/xcpretty": "npm:^4.3.1"
|
||||
@@ -3481,7 +3481,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/steps@npm:18.8.0, @expo/steps@workspace:packages/steps":
|
||||
"@expo/steps@npm:18.9.0, @expo/steps@workspace:packages/steps":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/steps@workspace:packages/steps"
|
||||
dependencies:
|
||||
@@ -3554,7 +3554,7 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:18.8.0"
|
||||
"@expo/build-tools": "npm:18.9.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:18.5.0"
|
||||
@@ -3563,7 +3563,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@hapi/boom": "npm:10.0.1"
|
||||
@@ -10331,7 +10331,7 @@ __metadata:
|
||||
resolution: "create-eas-build-function@workspace:packages/create-eas-build-function"
|
||||
dependencies:
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -10944,7 +10944,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:18.8.0"
|
||||
"@expo/build-tools": "npm:18.9.0"
|
||||
"@expo/eas-build-job": "npm:18.8.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
@@ -11018,7 +11018,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@expo/timeago.js": "npm:1.0.0"
|
||||
"@graphql-codegen/cli": "npm:5.0.0"
|
||||
"@graphql-codegen/introspection": "npm:4.0.0"
|
||||
|
||||
Reference in new issue
Block a user