Compare commits

...
Author SHA1 Message Date
Krystof Woldrich eb3caef49b feat(simulator): expose topic commands 2026-05-20 16:24:09 +02:00
Stanisław Chmiela 6ec2ea0530 [eas-cli] Capture iOS config introspection fallback in Sentry (#3759)
## Summary
- capture local Expo CLI iOS entitlements introspection failures in Sentry before falling back to bundled config resolution
- tag the event with `app_id` from `EAS_BUILD_PROJECT_ID` and the config resolution path
- capture a short fallback message so this is easier to distinguish from generic spawn failures
- include the failed command stdout, stderr, and exit status as Sentry extras

## Why
If `expo config --json --type introspect` fails but the bundled config fallback succeeds, users no longer hit a hard failure. Capturing the original command failure gives us visibility into the remaining fallback cases without breaking builds, and the explicit message makes these events easier to identify.

## Validation

CI should pass.
2026-05-20 15:51:37 +02:00
Szymon Dziedzic d5a6b13dae [eas-cli] Add --json mode to simulator:start (#3751)
# Why

`eas simulator:start` currently only emits human-readable connect instructions, which is awkward to consume from scripts/automation. `--json` (which implies `--non-interactive`) makes the session id, job run URL, and remote config directly parseable.

# How

- Swapped `EASNonInteractiveFlag` for `EasNonInteractiveAndJsonFlags` and resolved both via `resolveNonInteractiveAndJsonFlags`, matching the pattern used by `eas upload` and other JSON-capable commands.
- Calls `enableJsonOutput()` early so spinners/log lines redirect to stderr when JSON is enabled.
- After the remote config is ready, emits a single JSON payload (`deviceRunSessionId`, `type`, `jobRunUrl`, `remoteConfig`) and returns without entering the interactive wait loop.
- Existing `--non-interactive` (without `--json`) behavior is preserved.

# Test Plan

- `yarn typecheck` reports no new errors (the two pre-existing `src/build/metadata.ts` errors are unrelated to this PR).
- `yarn run -T oxlint packages/eas-cli/src/commands/simulator/start.ts` → 0 errors.
- `yarn run -T oxfmt --check packages/eas-cli/src/commands/simulator/start.ts` → clean.
- Manual (TODO before merge): run `eas simulator:start --platform ios --json`, confirm a single JSON object lands on stdout while spinners/log output go to stderr, and that the payload contains the expected fields.
2026-05-20 12:47:00 +02:00
Ash Wu 41eb9f1499 [build-tools] Enable build time statistics for xcodebuild (#3750)
* [build-tools] Enable build time statistics for xcodebuild

Signed-off-by: Ash Wu <[email protected]>

* address comment

Signed-off-by: Ash Wu <[email protected]>

---------

Signed-off-by: Ash Wu <[email protected]>
2026-05-20 15:45:59 +08:00
Ash Wu 5c9b93fc34 [build-tools] Pass ctx.env to parse xcactivitylog (#3758)
Signed-off-by: Ash Wu <[email protected]>
2026-05-20 15:08:55 +08:00
Ash Wu fbc57831cc [build-tools] Use pre-install xclogparser (#3747)
Signed-off-by: Ash Wu <[email protected]>
2026-05-20 09:40:19 +08:00
Mustafa Abban 34ca7b0ab9 [build-tools] ignore no Gradle profile found error (#3755)
* [build-tools] ignore no Gradle profile found error

* [build-tools] ignore no Gradle profile found error

* [build-tools] ignore no Gradle profile found error
2026-05-19 15:42:33 -04:00
Expo CI 7d6e4db4e8 update CHANGELOG.md 2026-05-19 18:28:20 +00:00
Expo CI 32355274ec v19.0.1 2026-05-19 18:26:02 +00:00
Stanisław Chmiela 50ca4cbec1 [eas-cli] Restore iOS entitlements introspection fallback (#3753)
## Summary
- wrap managed iOS `expo config --json --type introspect` entitlement resolution in a local Expo CLI try/catch
- fall back to bundled `getPrebuildConfigAsync` + `compileModsAsync`, matching the previous behavior
- include stderr/stdout in the thrown error when both the local CLI and fallback fail
- check `isExpoInstalled` before spawning the local Expo CLI, consistent with the regular config reader

## Why
After `18.13.1`, managed iOS builds fail hard if the local Expo CLI introspection command exits non-zero. Previously this path could still resolve entitlements through the bundled config fallback. Restoring that fallback limits the regression while preserving the local CLI path when it is available.

## Validation

CI should pass.
2026-05-19 20:24:00 +02:00
Stanisław Chmiela 2dbb4372d8 [eas-cli] Add EXPO_NO_DOTENV when resolving iOS entitlements back (#3752)
## Summary
- restore `EXPO_NO_DOTENV=1` when resolving managed iOS entitlements through `expo config --type introspect`
- keep the introspection environment consistent with the regular Expo config path

## Why
`18.13.1` changed this path to call the local Expo CLI directly, but the entitlements-specific call no longer disabled dotenv loading. This can make the iOS-only entitlements introspection evaluate app config under a different environment than the rest of EAS CLI config resolution.

## Validation

CI should pass. Repacks should work.
2026-05-19 20:14:45 +02:00
Expo CI 4bf38452a9 update CHANGELOG.md 2026-05-19 16:42:12 +00:00
Expo CI fc7c2950e7 v19.0.0 2026-05-19 16:38:56 +00:00
Byron Karlen 30248528ad [auth] Make browser-based login the default (#3746) 2026-05-19 09:19:39 -07:00
Mustafa Abban ecf58ab8a8 [build-tools] - Gradle build profile report with task execution breakdown (#3629)
* [build] gradle parse profiler

* gradleProfileTasks

* individual tasks nested underneath with tree characters

* filter out whats less than 1s

* parseGradleProfile

* profile flag

* enable by default

* gradleProfile utils

* parse profile HTML with fast-xml-parser

* parse profile HTML with fast-xml-parser

* error handling, send to sentry

* sentry capture
2026-05-19 12:07:36 -04:00
Douglas Lowder 10c7ac64ba [eas-cli] filter observe:routes results by route names (#3744)
# Why

The `eas observe:routes` command lacked the ability to filter navigation route results by specific route names. Users had no way to narrow down the routes data to only the routes they cared about.

# How

A new `--route-name` flag (repeatable) was added to the command. When one or more values are provided, they are collected into a `routeNames` array and forwarded to the `fetchObserveNavigationRoutesAsync` function, which passes them along in the GraphQL filter. An empty array is treated the same as omitting the flag — no `routeNames` key is sent in the filter.

Example:

```
$ easd observe:routes 
EAS Observe is in preview and subject to breaking changes.

Med values (navigation count) for the last 60 days

Android
Route           Nav Cold TTR  Nav Warm TTR  Nav TTI    
--------------  ------------  ------------  -----------
/               0.18s (63)    0.08s (217)   0.22s (280)
/task/[taskId]  0.55s (39)    0.17s (139)   0.35s (178)
/new            0.37s (20)    0.13s (83)    0.26s (103)
/stats          0.88s (19)    0.36s (94)    0.64s (113)
/settings       0.15s (13)    0.06s (41)    0.12s (54) 

iOS
Route           Nav Cold TTR  Nav Warm TTR  Nav TTI    
--------------  ------------  ------------  -----------
/               0.18s (106)   0.08s (504)   0.22s (610)
/task/[taskId]  0.51s (88)    0.18s (325)   0.34s (413)
/stats          1.06s (68)    0.34s (243)   0.62s (311)
/new            0.32s (67)    0.13s (219)   0.27s (286)
/settings       0.16s (17)    0.06s (87)    0.13s (104)

$ easd observe:routes --route-name /new --route-name /settings
EAS Observe is in preview and subject to breaking changes.

Med values (navigation count) for the last 60 days

Android
Route      Nav Cold TTR  Nav Warm TTR  Nav TTI    
---------  ------------  ------------  -----------
/new       0.37s (20)    0.13s (83)    0.26s (103)
/settings  0.15s (13)    0.06s (41)    0.12s (54) 

iOS
Route      Nav Cold TTR  Nav Warm TTR  Nav TTI    
---------  ------------  ------------  -----------
/new       0.32s (67)    0.13s (219)   0.27s (286)
/settings  0.16s (17)    0.06s (87)    0.13s (104)
```

# Test Plan

- New unit tests cover both the command flag parsing and the fetch function behavior
- Manual testing with the kadiexpo/cadence app in staging
2026-05-19 03:50:12 -07:00
Phil Pluckthun 7ee81e856b fix(eas-cli): Check if expo is resolvable before assuming it's installed (#3748)
* Add resolution check to `isExpoCommand` shared with `spawnExpoCommand`

* Add CHANGELOG entry
2026-05-19 11:02:14 +01:00
Gabe Debes 8328172827 [worker] report build phase duration to www metrics endpoint (#3732)
# Why

Turtle reports build-phase durations to Datadog. Orchestrator-routed builds don't, so we lose those stats post-migration.

Ref ENG-21208

# How

Adds a `Datadog` helper in `@expo/build-tools`, following the existing Sentry singleton pattern. Worker configures it once per task with the www API base URL, turtle build id, and robot token when a token is available. `BuildContext` emits `eas.build.phase_duration` distribution samples when build phases end, while the existing build phase stats callback remains only for launcher websocket reporting.

The payload matches what shipped in https://github.com/expo/universe/pull/27475: `POST /v2/turtle-builds/:buildId/metrics` with `metrics: [{ name: 'eas.build.phase_duration', type: 'distribution', value, tags }]`. Upload failures are captured through Sentry and never fail the build. Worker flushes pending metric sends before sending terminal build results.

# Test Plan

- CI
- `yarn exec oxfmt --check CHANGELOG.md packages/build-tools/src/datadog.ts packages/build-tools/src/index.ts packages/build-tools/src/context.ts packages/build-tools/src/__tests__/datadog.test.ts packages/build-tools/src/__tests__/context.test.ts packages/worker/src/context.ts packages/worker/src/service.ts packages/worker/src/__unit__/service.test.ts`
- `yarn workspace @expo/build-tools build`
- `yarn workspace @expo/build-tools jest-unit src/__tests__/datadog.test.ts src/__tests__/context.test.ts --runInBand`
- `yarn workspace @expo/worker test:unit packages/worker/src/__unit__/service.test.ts --runInBand`
2026-05-19 11:13:53 +02:00
Expo CI 29909b223d update CHANGELOG.md 2026-05-18 21:34:02 +00:00
49 changed files with 2535 additions and 279 deletions

No files matched your search

+33 -2
View File
@@ -8,14 +8,45 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
### 🧹 Chores
## [19.0.1](https://github.com/expo/eas-cli/releases/tag/v19.0.1) - 2026-05-19
### 🐛 Bug fixes
- [eas-cli] Disable dotenv loading when resolving managed iOS entitlements. ([#3752](https://github.com/expo/eas-cli/pull/3752) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Restore the bundled config fallback for managed iOS entitlements introspection. ([#3753](https://github.com/expo/eas-cli/pull/3753) by [@sjchmiela](https://github.com/sjchmiela))
## [19.0.0](https://github.com/expo/eas-cli/releases/tag/v19.0.0) - 2026-05-19
### 🛠 Breaking changes
- [eas-cli] Make browser-based authentication the default for `eas login`. Use `--no-browser` for CLI-based login. ([#3746](https://github.com/expo/eas-cli/pull/3746) by [@byronkarlen](https://github.com/byronkarlen))
### 🎉 New features
- [eas-cli] Allow command `observe:routes` to filter results to specific route names. ([#3744](https://github.com/expo/eas-cli/pull/3744) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- [eas-cli] Check that `expo` is resolvable before assuming it's installed ([#3748](https://github.com/expo/eas-cli/pull/3748) by [@kitten](https://github.com/kitten))
### 🧹 Chores
- [worker] Report build phase durations stats. ([#3732](https://github.com/expo/eas-cli/pull/3732) by [@gwdp](https://github.com/gwdp))
## [18.13.1](https://github.com/expo/eas-cli/releases/tag/v18.13.1) - 2026-05-18
### 🎉 New features
- [eas-cli] Send the installed `expo` package version in build metadata. ([#3725](https://github.com/expo/eas-cli/pull/3725) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- [eas-cli] Prevent `npx` invocations that can be unreliable and fail when retrieving entitlements or project configs ([#3282](https://github.com/expo/eas-cli/pull/3282) by [@kitten](https://github.com/kitten))
### 🧹 Chores
## [18.13.0](https://github.com/expo/eas-cli/releases/tag/v18.13.0) - 2026-05-14
### 🎉 New features
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "18.13.1",
"version": "19.0.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/build-tools",
"version": "18.13.1",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -37,17 +37,17 @@
"dependencies": {
"@expo/config": "55.0.10",
"@expo/config-plugins": "55.0.7",
"@expo/downloader": "18.5.0",
"@expo/eas-build-job": "18.13.1",
"@expo/downloader": "19.0.0",
"@expo/eas-build-job": "19.0.0",
"@expo/env": "^0.4.0",
"@expo/logger": "18.5.0",
"@expo/logger": "19.0.0",
"@expo/package-manager": "1.9.10",
"@expo/plist": "^0.2.0",
"@expo/results": "^1.0.0",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "18.13.1",
"@expo/template-file": "18.5.0",
"@expo/turtle-spawn": "18.5.0",
"@expo/steps": "19.0.0",
"@expo/template-file": "19.0.0",
"@expo/turtle-spawn": "19.0.0",
"@expo/xcpretty": "^4.3.1",
"@google-cloud/storage": "^7.11.2",
"@sentry/node": "7.77.0",
@@ -1,14 +1,36 @@
import { BuildTrigger, Job, Metadata } from '@expo/eas-build-job';
import {
BuildPhase,
BuildPhaseResult,
BuildTrigger,
Job,
Metadata,
Platform,
} from '@expo/eas-build-job';
import { randomUUID } from 'crypto';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { createMockLogger } from './utils/logger';
import { BuildContext } from '../context';
import { Datadog } from '../datadog';
jest.mock('fs');
jest.mock('fs-extra');
jest.mock('../datadog', () => ({
Datadog: {
distribution: jest.fn(),
},
}));
const datadogDistributionMock = jest.mocked(Datadog.distribution);
describe('BuildContext', () => {
beforeEach(() => {
vol.reset();
jest.clearAllMocks();
(fs.readdir as unknown as jest.Mock).mockResolvedValue([]);
});
it('should merge secrets', async () => {
const robotAccessToken = randomUUID();
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
@@ -116,4 +138,68 @@ describe('BuildContext', () => {
foo: 'bar',
});
});
it('emits build phase duration metrics for successful build phases', async () => {
const ctx = createTestBuildContext({ platform: Platform.IOS });
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {});
expect(datadogDistributionMock).toHaveBeenCalledWith(
'eas.build.phase_duration',
expect.any(Number),
{
build_phase: 'run_fastlane',
platform: Platform.IOS,
result: BuildPhaseResult.SUCCESS,
}
);
});
it('emits build phase duration metrics for failed build phases', async () => {
const ctx = createTestBuildContext({ platform: Platform.ANDROID });
await expect(
ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
throw new Error('boom');
})
).rejects.toThrow();
expect(datadogDistributionMock).toHaveBeenCalledWith(
'eas.build.phase_duration',
expect.any(Number),
{
build_phase: 'install_dependencies',
platform: Platform.ANDROID,
result: BuildPhaseResult.FAIL,
}
);
});
it('does not emit build phase duration metrics for non-platform jobs', async () => {
const ctx = createTestBuildContext({});
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {});
expect(datadogDistributionMock).not.toHaveBeenCalled();
});
});
function createTestBuildContext({ platform }: { platform?: Platform }): BuildContext {
vol.mkdirSync('/workingdir/env', { recursive: true });
return new BuildContext(
{
...(platform ? { platform } : {}),
builderEnvironment: { env: {} },
} as Job,
{
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
workingdir: '/workingdir',
logger: createMockLogger(),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
uploadArtifact: jest.fn(),
}
);
}
@@ -0,0 +1,150 @@
import { Response } from 'node-fetch';
import { Datadog } from '../datadog';
import { Sentry } from '../sentry';
import { turtleFetch } from '../utils/turtleFetch';
jest.mock('../sentry', () => ({
Sentry: {
capture: jest.fn(),
},
}));
jest.mock('../utils/turtleFetch', () => ({
turtleFetch: jest.fn(),
}));
const turtleFetchMock = jest.mocked(turtleFetch);
const sentryCaptureMock = jest.mocked(Sentry.capture);
describe('Datadog singleton', () => {
beforeEach(() => {
jest.clearAllMocks();
Datadog.setup(null);
});
it('POSTs distribution metrics to the turtle build metrics endpoint', () => {
turtleFetchMock.mockResolvedValueOnce({} as Response);
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
Datadog.distribution('eas.build.phase_duration', 1234, {
build_phase: 'install_dependencies',
platform: 'ios',
result: 'success',
});
expect(turtleFetchMock).toHaveBeenCalledWith(
'https://api.expo.test/v2/turtle-builds/build-id/metrics',
'POST',
{
json: {
metrics: [
{
name: 'eas.build.phase_duration',
type: 'distribution',
value: 1234,
tags: {
build_phase: 'install_dependencies',
platform: 'ios',
result: 'success',
},
},
],
},
headers: { Authorization: 'Bearer token-abc' },
retries: 2,
}
);
});
it('is a no-op when setup is null', () => {
Datadog.setup(null);
Datadog.distribution('eas.build.phase_duration', 1);
expect(turtleFetchMock).not.toHaveBeenCalled();
});
it('swallows upload failures', async () => {
turtleFetchMock.mockRejectedValueOnce(new Error('network down'));
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
Datadog.distribution('eas.build.phase_duration', 1);
await flushPromises();
expect(turtleFetchMock).toHaveBeenCalled();
expect(sentryCaptureMock).toHaveBeenCalledWith(
'Failed to report turtle build metric',
expect.any(Error),
expect.objectContaining({
extras: {
metrics: [{ name: 'eas.build.phase_duration', type: 'distribution', value: 1, tags: {} }],
},
})
);
});
it('uses the latest setup options', () => {
turtleFetchMock.mockResolvedValue({} as Response);
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'first-build-id',
robotAccessToken: 'first-token',
});
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'second-build-id',
robotAccessToken: 'second-token',
});
Datadog.distribution('eas.build.phase_duration', 1);
expect(turtleFetchMock).toHaveBeenCalledWith(
'https://api.expo.test/v2/turtle-builds/second-build-id/metrics',
'POST',
expect.objectContaining({
headers: { Authorization: 'Bearer second-token' },
})
);
});
it('flushes pending metric uploads', async () => {
let resolveUpload!: (response: Response) => void;
turtleFetchMock.mockReturnValueOnce(
new Promise<Response>(resolve => {
resolveUpload = resolve;
})
);
Datadog.setup({
expoApiV2BaseUrl: 'https://api.expo.test/v2/',
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
let flushed = false;
Datadog.distribution('eas.build.phase_duration', 1);
const flushPromise = Datadog.flushAsync().then(() => {
flushed = true;
});
await Promise.resolve();
expect(flushed).toBe(false);
resolveUpload({} as Response);
await flushPromise;
expect(flushed).toBe(true);
});
});
async function flushPromises(): Promise<void> {
await new Promise(resolve => setImmediate(resolve));
}
+21 -12
View File
@@ -32,18 +32,27 @@ export async function runGradleCommand(
await fs.chmod(path.join(androidDir, 'gradlew'), 0o755);
const verboseFlag = ctx.env['EAS_VERBOSE'] === '1' ? '--info' : '';
const spawnPromise = spawn('bash', ['-c', `./gradlew ${gradleCommand} ${verboseFlag}`], {
cwd: androidDir,
logger,
lineTransformer: (line?: string) => {
if (!line || /^\.+$/.exec(line)) {
return null;
} else {
return line;
}
},
env: { ...ctx.env, ...extraEnv, ...resolveVersionOverridesEnvs(ctx), LC_ALL: 'C.UTF-8' },
});
const spawnPromise = spawn(
'bash',
['-c', `./gradlew ${gradleCommand} --profile ${verboseFlag}`],
{
cwd: androidDir,
logger,
lineTransformer: (line?: string) => {
if (!line || /^\.+$/.exec(line)) {
return null;
} else {
return line;
}
},
env: {
...ctx.env,
...extraEnv,
...resolveVersionOverridesEnvs(ctx),
LC_ALL: 'C.UTF-8',
},
}
);
if (ctx.env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
adjustOOMScore(spawnPromise, logger);
}
@@ -0,0 +1,231 @@
import { XMLParser } from 'fast-xml-parser';
import fs from 'fs-extra';
import path from 'path';
export interface GradleProfileTask {
path: string;
durationMs: number;
result: string;
}
export async function parseGradleProfile(androidDir: string): Promise<GradleProfileTask[]> {
const profileDir = path.join(androidDir, 'build', 'reports', 'profile');
// Gradle profile may not exist for all builds (e.g. custom, repack
// jobs). We should short circuit and not unnecessarily log.
if (!(await fs.pathExists(profileDir))) {
return [];
}
const files = await fs.readdir(profileDir);
const htmlFile = files
.filter(f => f.startsWith('profile-') && f.endsWith('.html'))
.sort()
.pop();
if (!htmlFile) {
throw new Error(`No Gradle profile HTML found in ${profileDir}`);
}
const html = await fs.readFile(path.join(profileDir, htmlFile), 'utf8');
// Locate the <h2>Task Execution</h2> heading and extract its <table>
const headingMatch = html.match(/<h2[^>]*>\s*Task Execution\s*<\/h2>/i);
if (!headingMatch || headingMatch.index === undefined) {
throw new Error('Could not find Task Execution section in Gradle profile');
}
const sectionStart = headingMatch.index;
const tableStart = html.indexOf('<table', sectionStart);
const tableEnd = html.indexOf('</table>', tableStart);
if (tableStart === -1 || tableEnd === -1) {
throw new Error('Could not find task execution table in Gradle profile');
}
const tableHtml = html.slice(tableStart, tableEnd + '</table>'.length);
const parser = new XMLParser({
ignoreAttributes: true,
isArray: name => name === 'tr' || name === 'td',
trimValues: true,
});
const parsed = parser.parse(tableHtml);
const rows: any[] = parsed?.table?.tbody?.tr ?? parsed?.table?.tr ?? [];
const tasks: GradleProfileTask[] = [];
for (const row of rows) {
const cells: unknown[] = row?.td;
if (!cells || cells.length < 2) {
continue;
}
const taskPath = String(cells[0] ?? '').trim();
const durationStr = String(cells[1] ?? '').trim();
const result = String(cells[2] ?? '').trim() || 'executed';
if (!taskPath || !durationStr) {
continue;
}
const durationMs = parseDurationToMs(durationStr);
tasks.push({ path: taskPath, durationMs, result: result.toLowerCase() });
}
return tasks;
}
function parseDurationToMs(duration: string): number {
// Gradle profile durations can be like "1.234s", "0.045s", "12.5s"
const secondsMatch = duration.match(/^([\d.]+)s$/);
if (secondsMatch) {
return Math.round(parseFloat(secondsMatch[1]) * 1000);
}
return 0;
}
function formatSeconds(ms: number): string {
const s = ms / 1000;
if (s < 0.1) {
return `${ms}ms`;
}
return `${s.toFixed(1)}s`;
}
export function formatGradleProfileReport(tasks: GradleProfileTask[]): string {
// Filter out tasks under 1 second
const significantTasks = tasks.filter(t => t.durationMs >= 1000);
// Separate module totals from individual tasks
const moduleTotals = significantTasks.filter(t => t.result === '(total)');
const individualTasks = significantTasks.filter(t => t.result !== '(total)');
// Group individual tasks by their module prefix
const moduleChildren = new Map<string, GradleProfileTask[]>();
const orphanTasks: GradleProfileTask[] = [];
for (const task of individualTasks) {
const parent = moduleTotals.find(m => task.path.startsWith(m.path + ':'));
if (parent) {
const children = moduleChildren.get(parent.path) ?? [];
children.push(task);
moduleChildren.set(parent.path, children);
} else {
orphanTasks.push(task);
}
}
// Sort module totals by duration, sort children within each module
const sortedModules = [...moduleTotals].sort((a, b) => b.durationMs - a.durationMs);
for (const children of moduleChildren.values()) {
children.sort((a, b) => b.durationMs - a.durationMs);
}
orphanTasks.sort((a, b) => b.durationMs - a.durationMs);
// Build display rows: [displayName, task]
const rows: { displayName: string; task: GradleProfileTask }[] = [];
for (const mod of sortedModules) {
rows.push({ displayName: mod.path, task: mod });
const children = moduleChildren.get(mod.path) ?? [];
for (let i = 0; i < children.length; i++) {
const isLast = i === children.length - 1;
const prefix = isLast ? ' └─ ' : ' ├─ ';
const shortName = children[i].path.slice(mod.path.length + 1);
rows.push({ displayName: prefix + shortName, task: children[i] });
}
}
for (const task of orphanTasks) {
rows.push({ displayName: task.path, task });
}
// Compute totals from individual tasks only (avoid double-counting)
const totalMs = individualTasks.reduce((sum, t) => sum + t.durationMs, 0);
const maxMs = totalMs || 1;
const nameWidth = Math.max(4, ...rows.map(r => r.displayName.length)) + 2;
const barMaxWidth = 20;
const header =
'┌─' +
'─'.repeat(nameWidth) +
'─┬────────────┬──────────┬────────────┬─' +
'─'.repeat(barMaxWidth) +
'─┐';
const divider =
'├─' +
'─'.repeat(nameWidth) +
'─┼────────────┼──────────┼────────────┼─' +
'─'.repeat(barMaxWidth) +
'─┤';
const footer =
'└─' +
'─'.repeat(nameWidth) +
'─┴────────────┴──────────┴────────────┴─' +
'─'.repeat(barMaxWidth) +
'─┘';
const taskCount = individualTasks.length;
const cachedCount = individualTasks.filter(t => t.result !== 'executed').length;
const lines: string[] = [];
lines.push('Gradle Build — Task Execution Profile');
const cachedSuffix = cachedCount > 0 ? ` (${cachedCount} cached/up-to-date)` : '';
lines.push(`${taskCount} tasks${cachedSuffix}, total task time: ${formatSeconds(totalMs)}`);
lines.push('% Time = share of total task execution time');
lines.push('');
lines.push(header);
lines.push(
'│ ' +
'Task'.padEnd(nameWidth) +
' │ ' +
'Duration'.padStart(10) +
' │ ' +
'% Time'.padStart(8) +
' │ ' +
'Result'.padEnd(10) +
' │ ' +
' '.repeat(barMaxWidth) +
' │'
);
lines.push(divider);
for (const row of rows) {
const pct = totalMs === 0 ? 0 : (row.task.durationMs / totalMs) * 100;
const barLength = Math.round((row.task.durationMs / maxMs) * barMaxWidth);
const bar = '█'.repeat(barLength) + '░'.repeat(barMaxWidth - barLength);
const result = row.task.result === '(total)' ? 'total' : row.task.result;
lines.push(
'│ ' +
row.displayName.padEnd(nameWidth) +
' │ ' +
formatSeconds(row.task.durationMs).padStart(10) +
' │ ' +
`${pct.toFixed(1)}%`.padStart(8) +
' │ ' +
result.padEnd(10) +
' │ ' +
bar +
' │'
);
}
lines.push(divider);
lines.push(
'│ ' +
'TOTAL'.padEnd(nameWidth) +
' │ ' +
formatSeconds(totalMs).padStart(10) +
' │ ' +
'100.0%'.padStart(8) +
' │ ' +
' '.repeat(10) +
' │ ' +
' '.repeat(barMaxWidth) +
' │'
);
lines.push(footer);
lines.push('');
return lines.join('\n');
}
+14 -5
View File
@@ -16,6 +16,7 @@ import { runFastlaneGym, runFastlaneResign } from '../ios/fastlane';
import { installPods } from '../ios/pod';
import { downloadApplicationArchiveAsync } from '../ios/resign';
import { resolveArtifactPath, resolveBuildConfiguration, resolveScheme } from '../ios/resolve';
import { Sentry } from '../sentry';
import { parseAndReportXcactivitylog } from '../steps/utils/ios/xcactivitylog';
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
@@ -173,17 +174,25 @@ async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
});
}
if (ctx.env.EXPERIMENTAL_EAS_XCACTIVITYLOG === '1') {
const { derivedDataPath, workspacePath } = nullthrows(fastlaneResult);
await ctx.runBuildPhase(BuildPhase.PARSE_XCACTIVITYLOG, async () => {
await ctx.runBuildPhase(BuildPhase.PARSE_XCACTIVITYLOG, async () => {
if (ctx.isLocal) {
ctx.logger.info('Local builds skip build performance analysis.');
return;
}
try {
const { derivedDataPath, workspacePath } = nullthrows(fastlaneResult);
await parseAndReportXcactivitylog({
derivedDataPath,
workspacePath,
logger: ctx.logger,
proxyBaseUrl: ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL,
env: ctx.env,
});
});
}
} catch (err: any) {
Sentry.capture('Failed to parse xcactivitylog', err);
ctx.markBuildPhaseSkipped();
}
});
await ctx.runBuildPhase(BuildPhase.PRE_UPLOAD_ARTIFACTS_HOOK, async () => {
await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS);
+8
View File
@@ -20,6 +20,7 @@ import fs from 'fs-extra';
import path from 'path';
import { resolveBuildPhaseErrorAsync } from './buildErrors/detectError';
import { Datadog } from './datadog';
import { readAppConfig } from './utils/appConfig';
import { createTemporaryEnvironmentSecretFile } from './utils/environmentSecrets';
import { PackageManager, resolvePackageManager } from './utils/packageManager';
@@ -355,6 +356,13 @@ export class BuildContext<TJob extends Job = Job> {
await this.collectAndUpdateEnvVariablesAsync();
this.reportBuildPhaseStats?.({ buildPhase: this.buildPhase, result, durationMs });
if (this.job.platform) {
Datadog.distribution('eas.build.phase_duration', durationMs, {
build_phase: this.buildPhase.toLowerCase(),
platform: this.job.platform,
result,
});
}
if (!doNotMarkEnd) {
this.logger.info(
+54
View File
@@ -0,0 +1,54 @@
import { Sentry } from './sentry';
import { turtleFetch } from './utils/turtleFetch';
type DatadogSetupOptions = {
expoApiV2BaseUrl: string;
turtleBuildId: string;
robotAccessToken: string;
};
let setupOptions: DatadogSetupOptions | null = null;
let pendingMetricUploads: Promise<unknown>[] = [];
export const Datadog = {
setup(opts: DatadogSetupOptions | null): void {
setupOptions = opts;
},
distribution(name: string, value: number, tags: Record<string, string> = {}): void {
if (!setupOptions) {
return;
}
const { expoApiV2BaseUrl, turtleBuildId, robotAccessToken } = setupOptions;
const metrics = [
{
name,
type: 'distribution' as const,
value,
tags,
},
];
const uploadPromise = turtleFetch(
new URL(`turtle-builds/${turtleBuildId}/metrics`, expoApiV2BaseUrl).toString(),
'POST',
{
json: { metrics },
headers: {
Authorization: `Bearer ${robotAccessToken}`,
},
retries: 2,
}
).catch(err => {
Sentry.capture('Failed to report turtle build metric', err, {
extras: { metrics },
});
});
pendingMetricUploads.push(uploadPromise);
},
async flushAsync(): Promise<void> {
await Promise.allSettled(pendingMetricUploads);
},
};
+4
View File
@@ -20,6 +20,10 @@ export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
export { Hook, runHookIfPresent } from './utils/hooks';
export { parseGradleProfile, formatGradleProfileReport } from './android/gradleProfile';
export type { GradleProfileTask } from './android/gradleProfile';
export * from './generic';
export { Datadog } from './datadog';
export { Sentry } from './sentry';
@@ -45,6 +45,7 @@ export function parseXcactivitylogFunction(): BuildFunction {
xclogparserVersion: version,
logger: stepCtx.logger,
proxyBaseUrl: env.EAS_BUILD_COCOAPODS_CACHE_URL,
env,
});
},
});
@@ -23,18 +23,27 @@ export async function runGradleCommand({
logger.info(`Running 'gradlew ${gradleCommand} ${verboseFlag}' in ${androidDir}`);
await fs.chmod(path.join(androidDir, 'gradlew'), 0o755);
const spawnPromise = spawn('bash', ['-c', `./gradlew ${gradleCommand} ${verboseFlag}`], {
cwd: androidDir,
logger,
lineTransformer: (line?: string) => {
if (!line || /^\.+$/.exec(line)) {
return null;
} else {
return line;
}
},
env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' },
});
const spawnPromise = spawn(
'bash',
['-c', `./gradlew ${gradleCommand} --profile ${verboseFlag}`],
{
cwd: androidDir,
logger,
lineTransformer: (line?: string) => {
if (!line || /^\.+$/.exec(line)) {
return null;
} else {
return line;
}
},
env: {
...env,
...extraEnv,
LC_ALL: 'C.UTF-8',
},
}
);
if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
adjustOOMScore(spawnPromise, logger);
}
@@ -195,6 +195,8 @@ const FIXTURE_OVERLAP_WITH_GAP = {
const mockedDownloadFile = jest.mocked(downloadFile);
const mockedSpawn = jest.mocked(spawn);
const TEST_ENV = { PATH: '/custom/bin:/usr/bin' };
function mockFilesystem({
tempDir = '/tmp/xclogparser-123',
jsonOutput = JSON.stringify(FIXTURE_TWO_MODULES),
@@ -224,32 +226,38 @@ describe('parseAndReportXcactivitylog', () => {
jest.restoreAllMocks();
});
it('downloads xclogparser from GCS and logs only the final report on success', async () => {
it('downloads xclogparser from GCS and logs resolution + final report when no binary is preinstalled', async () => {
const logger = createMockLogger();
const { tempDir, reportPath } = mockFilesystem();
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
});
expect(mockedSpawn).toHaveBeenNthCalledWith(1, 'xclogparser', ['version'], {
stdio: 'pipe',
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenCalledWith(
'https://storage.googleapis.com/turtle-v2/xclogparser/XCLogParser-macOS-x86-64-arm64-v0.2.47.zip',
path.join(tempDir, 'XCLogParser-macOS-x86-64-arm64-v0.2.47.zip'),
{ retry: 3, timeout: 20_000 }
);
expect(mockedSpawn).toHaveBeenNthCalledWith(
1,
2,
'unzip',
['-q', path.join(tempDir, 'XCLogParser-macOS-x86-64-arm64-v0.2.47.zip'), '-d', tempDir],
{ stdio: 'pipe' }
{ stdio: 'pipe', env: TEST_ENV }
);
expect(mockedSpawn).toHaveBeenNthCalledWith(
2,
3,
path.join(tempDir, 'xclogparser'),
[
'parse',
@@ -262,10 +270,10 @@ describe('parseAndReportXcactivitylog', () => {
'--output',
reportPath,
],
{ stdio: 'pipe' }
{ stdio: 'pipe', env: TEST_ENV }
);
expect(fs.readFile).toHaveBeenCalledWith(reportPath, 'utf8');
expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Using downloaded xclogparser v0.2.47.');
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('Xcode Build — Compile Metrics by Module')
);
@@ -281,6 +289,7 @@ describe('parseAndReportXcactivitylog', () => {
mockedDownloadFile
.mockRejectedValueOnce(new Error('proxy failed'))
.mockResolvedValueOnce(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await parseAndReportXcactivitylog({
@@ -288,6 +297,7 @@ describe('parseAndReportXcactivitylog', () => {
workspacePath: '/tmp/workspace',
logger,
proxyBaseUrl: 'https://cache.example.com',
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenNthCalledWith(
@@ -315,6 +325,7 @@ describe('parseAndReportXcactivitylog', () => {
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn
.mockRejectedValueOnce(new Error('xclogparser not found'))
.mockRejectedValueOnce(new Error('invalid zip'))
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any)
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any);
@@ -324,6 +335,7 @@ describe('parseAndReportXcactivitylog', () => {
workspacePath: '/tmp/workspace',
logger,
proxyBaseUrl: 'https://cache.example.com',
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenNthCalledWith(
@@ -353,6 +365,7 @@ describe('parseAndReportXcactivitylog', () => {
mockFilesystem({ jsonOutput: '{not valid json' });
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await expect(
@@ -360,6 +373,7 @@ describe('parseAndReportXcactivitylog', () => {
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
})
).resolves.toBeUndefined();
@@ -382,6 +396,7 @@ describe('parseAndReportXcactivitylog', () => {
});
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await expect(
@@ -389,6 +404,7 @@ describe('parseAndReportXcactivitylog', () => {
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
})
).resolves.toBeUndefined();
@@ -407,6 +423,7 @@ describe('parseAndReportXcactivitylog', () => {
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn
.mockRejectedValueOnce(new Error('xclogparser not found')) // version detect
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any) // unzip
.mockRejectedValueOnce(new Error('spawn xclogparser ENOENT')); // xclogparser run
@@ -415,6 +432,7 @@ describe('parseAndReportXcactivitylog', () => {
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
})
).resolves.toBeUndefined();
@@ -432,6 +450,7 @@ describe('parseAndReportXcactivitylog', () => {
mockFilesystem();
mockedDownloadFile.mockRejectedValue(new Error('network unreachable'));
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await expect(
@@ -439,6 +458,7 @@ describe('parseAndReportXcactivitylog', () => {
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
})
).resolves.toBeUndefined();
@@ -459,6 +479,7 @@ describe('parseAndReportXcactivitylog', () => {
});
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await expect(
@@ -466,13 +487,171 @@ describe('parseAndReportXcactivitylog', () => {
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
})
).resolves.toBeUndefined();
expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.debug).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
describe('xclogparser version resolution', () => {
it('uses preinstalled binary when no version is requested', async () => {
const logger = createMockLogger();
const { reportPath } = mockFilesystem();
mockedSpawn
.mockResolvedValueOnce({ stdout: 'XCLogParser 0.2.50\n', stderr: '' } as any) // version detect
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any); // xclogparser parse
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
});
expect(mockedDownloadFile).not.toHaveBeenCalled();
expect(mockedSpawn).not.toHaveBeenCalledWith('unzip', expect.any(Array), expect.any(Object));
expect(mockedSpawn).toHaveBeenNthCalledWith(
2,
'xclogparser',
[
'parse',
'--derived_data',
'/tmp/derived-data',
'--workspace',
'/tmp/workspace',
'--reporter',
'json',
'--output',
reportPath,
],
{ stdio: 'pipe', env: TEST_ENV }
);
expect(logger.info).toHaveBeenCalledWith('Using preinstalled xclogparser v0.2.50.');
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('downloaded'));
expect(Sentry.capture).not.toHaveBeenCalled();
});
it('downloads default version when no version requested and no preinstall found', async () => {
const logger = createMockLogger();
mockFilesystem();
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenCalledWith(
expect.stringContaining('XCLogParser-macOS-x86-64-arm64-v0.2.47.zip'),
expect.any(String),
expect.any(Object)
);
expect(logger.info).toHaveBeenCalledWith('Using downloaded xclogparser v0.2.47.');
});
it('uses preinstalled binary when requested version matches', async () => {
const logger = createMockLogger();
mockFilesystem();
mockedSpawn
.mockResolvedValueOnce({ stdout: 'XCLogParser 0.2.50\n', stderr: '' } as any) // version detect
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any); // xclogparser parse
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
xclogparserVersion: 'v0.2.50',
logger,
env: TEST_ENV,
});
expect(mockedDownloadFile).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith('Using preinstalled xclogparser v0.2.50.');
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('downloaded'));
});
it('downloads requested version when it does not match the preinstalled binary', async () => {
const logger = createMockLogger();
mockFilesystem();
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn
.mockResolvedValueOnce({ stdout: 'XCLogParser 0.2.48\n', stderr: '' } as any) // version detect
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any) // unzip
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any); // xclogparser parse
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
xclogparserVersion: 'v0.2.50',
logger,
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenCalledWith(
expect.stringContaining('XCLogParser-macOS-x86-64-arm64-v0.2.50.zip'),
expect.any(String),
expect.any(Object)
);
expect(logger.info).toHaveBeenCalledWith('Using downloaded xclogparser v0.2.50.');
});
it('downloads requested version when no preinstall is found', async () => {
const logger = createMockLogger();
mockFilesystem();
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn.mockRejectedValueOnce(new Error('xclogparser not found'));
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
xclogparserVersion: 'v0.2.50',
logger,
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenCalledWith(
expect.stringContaining('XCLogParser-macOS-x86-64-arm64-v0.2.50.zip'),
expect.any(String),
expect.any(Object)
);
expect(logger.info).toHaveBeenCalledWith('Using downloaded xclogparser v0.2.50.');
});
it('falls back to download when the version output cannot be parsed', async () => {
const logger = createMockLogger();
mockFilesystem();
mockedDownloadFile.mockResolvedValue(undefined);
mockedSpawn
.mockResolvedValueOnce({ stdout: 'something unexpected', stderr: '' } as any) // version detect
.mockResolvedValue({ stdout: '', stderr: '' } as any);
await parseAndReportXcactivitylog({
derivedDataPath: '/tmp/derived-data',
workspacePath: '/tmp/workspace',
logger,
env: TEST_ENV,
});
expect(mockedDownloadFile).toHaveBeenCalledWith(
expect.stringContaining('XCLogParser-macOS-x86-64-arm64-v0.2.47.zip'),
expect.any(String),
expect.any(Object)
);
expect(logger.info).toHaveBeenCalledWith('Using downloaded xclogparser v0.2.47.');
});
});
});
describe('isCompileStep', () => {
@@ -1,6 +1,7 @@
import downloadFile from '@expo/downloader';
import { bunyan } from '@expo/logger';
import { asyncResult } from '@expo/results';
import { BuildStepEnv } from '@expo/steps';
import spawn from '@expo/turtle-spawn';
import fs from 'fs-extra';
import os from 'os';
@@ -22,35 +23,54 @@ const XCLOGPARSER_OUTPUT_FILENAME = 'xcactivitylog.json';
export async function parseAndReportXcactivitylog({
derivedDataPath,
workspacePath,
xclogparserVersion = DEFAULT_XCLOGPARSER_VERSION,
xclogparserVersion,
logger,
proxyBaseUrl,
env,
}: {
derivedDataPath: string;
workspacePath: string;
xclogparserVersion?: string;
logger: bunyan;
proxyBaseUrl?: string;
env: BuildStepEnv;
}): Promise<void> {
let tempDir: string | undefined;
let phase = 'creating_temp_directory';
try {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'xclogparser-'));
phase = 'downloading_xclogparser';
const xclogparserPath = await downloadXclogparser(
tempDir,
xclogparserVersion,
logger,
proxyBaseUrl
);
phase = 'resolving_xclogparser';
const preinstalledVersion = await detectPreinstalledXclogparserVersion(env);
let binaryPath: string;
let resolvedVersion: string;
if (
preinstalledVersion &&
(!xclogparserVersion || preinstalledVersion === xclogparserVersion)
) {
binaryPath = 'xclogparser';
resolvedVersion = preinstalledVersion;
logger.info(`Using preinstalled xclogparser ${resolvedVersion}.`);
} else {
phase = 'downloading_xclogparser';
resolvedVersion = xclogparserVersion ?? DEFAULT_XCLOGPARSER_VERSION;
binaryPath = await downloadXclogparser({
tempDir,
version: resolvedVersion,
logger,
proxyBaseUrl,
env,
});
logger.info(`Using downloaded xclogparser ${resolvedVersion}.`);
}
phase = 'running_xclogparser';
const jsonOutputPath = await runXclogparser({
binaryPath: xclogparserPath,
binaryPath,
derivedDataPath,
workspacePath,
outputDir: tempDir,
env,
});
phase = 'parsing_xclogparser_output';
@@ -70,12 +90,28 @@ export async function parseAndReportXcactivitylog({
}
}
async function downloadXclogparser(
tempDir: string,
version: string,
logger: bunyan,
proxyBaseUrl?: string
): Promise<string> {
async function detectPreinstalledXclogparserVersion(env: BuildStepEnv): Promise<string | null> {
const result = await asyncResult(spawn('xclogparser', ['version'], { stdio: 'pipe', env }));
if (!result.ok) {
return null;
}
const match = result.value.stdout.match(/(\d+\.\d+\.\d+)/);
return match ? `v${match[1]}` : null;
}
async function downloadXclogparser({
tempDir,
version,
logger,
proxyBaseUrl,
env,
}: {
tempDir: string;
version: string;
logger: bunyan;
proxyBaseUrl?: string;
env: BuildStepEnv;
}): Promise<string> {
const zipName = getXclogparserZipName(version);
const zipPath = path.join(tempDir, zipName);
const directUrl = `${XCLOGPARSER_DOWNLOAD_URL}/${zipName}`;
@@ -83,7 +119,7 @@ async function downloadXclogparser(
if (proxiedUrl) {
const proxiedDownloadResult = await asyncResult(
downloadAndUnpackXclogparser({ tempDir, zipPath, sourceUrl: proxiedUrl })
downloadAndUnpackXclogparser({ tempDir, zipPath, sourceUrl: proxiedUrl, env })
);
if (!proxiedDownloadResult.ok) {
logger.debug(
@@ -96,31 +132,35 @@ async function downloadXclogparser(
}
}
return await downloadAndUnpackXclogparser({ tempDir, zipPath, sourceUrl: directUrl });
return await downloadAndUnpackXclogparser({ tempDir, zipPath, sourceUrl: directUrl, env });
}
async function downloadAndUnpackXclogparser({
tempDir,
zipPath,
sourceUrl,
env,
}: {
tempDir: string;
zipPath: string;
sourceUrl: string;
env: BuildStepEnv;
}): Promise<string> {
await downloadFile(sourceUrl, zipPath, { retry: 3, timeout: XCLOGPARSER_DOWNLOAD_TIMEOUT_MS });
return await unpackXclogparser({ tempDir, zipPath });
return await unpackXclogparser({ tempDir, zipPath, env });
}
async function unpackXclogparser({
tempDir,
zipPath,
env,
}: {
tempDir: string;
zipPath: string;
env: BuildStepEnv;
}): Promise<string> {
await spawn('unzip', ['-q', zipPath, '-d', tempDir], { stdio: 'pipe' });
await spawn('unzip', ['-q', zipPath, '-d', tempDir], { stdio: 'pipe', env });
const binaryPath = path.join(tempDir, 'xclogparser');
await fs.chmod(binaryPath, 0o755);
@@ -143,11 +183,13 @@ async function runXclogparser({
derivedDataPath,
workspacePath,
outputDir,
env,
}: {
binaryPath: string;
derivedDataPath: string;
workspacePath: string;
outputDir: string;
env: BuildStepEnv;
}): Promise<string> {
const outputPath = path.join(outputDir, XCLOGPARSER_OUTPUT_FILENAME);
@@ -164,7 +206,7 @@ async function runXclogparser({
'--output',
outputPath,
],
{ stdio: 'pipe' }
{ stdio: 'pipe', env }
);
return outputPath;
@@ -420,7 +462,7 @@ export function formatReport(data: XcactivitylogData): string {
' │ ' +
'100.0%'.padStart(pctWidth) +
' │ ' +
'n/a'.padStart(wallWidth) +
' '.repeat(wallWidth) +
' │ ' +
' '.repeat(barMaxWidth) +
' │'
@@ -1,6 +1,6 @@
{
"name": "create-eas-build-function",
"version": "18.13.1",
"version": "19.0.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.13.1"
"@expo/steps": "19.0.0"
},
"devDependencies": {
"@expo/package-manager": "1.9.10",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/downloader",
"version": "18.5.0",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -1,6 +1,6 @@
{
"name": "eas-build-cache-provider",
"version": "18.5.0",
"version": "19.0.0",
"description": "A build cache provider plugin for the Expo CLI",
"homepage": "https://github.com/expo/eas-cli",
"bugs": "https://github.com/expo/eas-cli/issues",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/eas-build-job",
"version": "18.13.1",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "MIT",
"author": "Expo <[email protected]>",
@@ -25,9 +25,9 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/logger": "18.5.0",
"@expo/logger": "19.0.0",
"@expo/results": "1.0.0",
"@expo/turtle-spawn": "18.5.0",
"@expo/turtle-spawn": "19.0.0",
"joi": "^17.13.1",
"semver": "^7.6.2",
"zod": "^4.3.5"
+104 -104
View File
@@ -180,8 +180,8 @@ USAGE
$ eas account:login [-s] [-b]
FLAGS
-b, --browser Login with your browser
-s, --sso Login with SSO
-b, --[no-]browser Log in with your browser (default; use --no-browser for CLI-based login)
-s, --sso Log in with SSO
DESCRIPTION
log in with your Expo account
@@ -190,7 +190,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -207,7 +207,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:usage [ACCOUNT_NAME]`
@@ -229,7 +229,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/account/usage.ts)_
## `eas account:view`
@@ -246,7 +246,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -260,7 +260,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -312,7 +312,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -333,7 +333,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -353,7 +353,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -373,7 +373,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -396,7 +396,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -434,7 +434,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -454,7 +454,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -472,7 +472,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -492,7 +492,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -517,7 +517,7 @@ DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
@@ -544,7 +544,7 @@ DESCRIPTION
download a simulator/emulator build by build ID or fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -582,7 +582,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -622,7 +622,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -652,7 +652,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -680,7 +680,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -732,7 +732,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -750,7 +750,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -768,7 +768,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -785,7 +785,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -806,7 +806,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:delete [NAME]`
@@ -827,7 +827,7 @@ DESCRIPTION
Delete a channel
```
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/delete.ts)_
## `eas channel:edit [NAME]`
@@ -849,7 +849,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:insights`
@@ -873,7 +873,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/insights.ts)_
## `eas channel:list`
@@ -893,7 +893,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -915,7 +915,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -937,7 +937,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -972,7 +972,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -995,7 +995,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -1016,7 +1016,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.13.1/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -1033,7 +1033,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -1051,7 +1051,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -1080,7 +1080,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -1105,7 +1105,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/deploy/alias/index.ts)_
## `eas deploy:alias:delete [ALIAS_NAME]`
@@ -1126,7 +1126,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
## `eas deploy:delete [DEPLOYMENT_ID]`
@@ -1147,7 +1147,7 @@ ALIASES
$ eas worker:delete
```
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/deploy/delete.ts)_
## `eas deploy:promote`
@@ -1184,7 +1184,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1204,7 +1204,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1225,7 +1225,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1246,7 +1246,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1260,7 +1260,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1274,7 +1274,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1306,7 +1306,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1332,7 +1332,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1354,7 +1354,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1382,7 +1382,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1410,7 +1410,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1433,7 +1433,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1455,7 +1455,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1489,7 +1489,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
@@ -1533,7 +1533,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
@@ -1564,7 +1564,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1643,7 +1643,7 @@ DESCRIPTION
connect a project to an App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
## `eas integrations:asc:disconnect`
@@ -1662,7 +1662,7 @@ DESCRIPTION
disconnect the current project from its App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
## `eas integrations:asc:status`
@@ -1680,7 +1680,7 @@ DESCRIPTION
show the App Store Connect app link status for the current project
```
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/asc/status.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/asc/status.ts)_
## `eas integrations:convex:connect`
@@ -1702,7 +1702,7 @@ DESCRIPTION
connect Convex to your Expo project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
## `eas integrations:convex:dashboard`
@@ -1716,7 +1716,7 @@ DESCRIPTION
open the Convex dashboard for the linked Convex project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
## `eas integrations:convex:project`
@@ -1730,7 +1730,7 @@ DESCRIPTION
display the Convex project linked to the current Expo app
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/project.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/project.ts)_
## `eas integrations:convex:project:delete`
@@ -1748,7 +1748,7 @@ DESCRIPTION
remove the Convex project link for the current Expo app from EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
## `eas integrations:convex:team`
@@ -1762,7 +1762,7 @@ DESCRIPTION
display Convex teams linked to the current Expo app's owner account
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/team.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/team.ts)_
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
@@ -1783,7 +1783,7 @@ DESCRIPTION
remove a Convex team link from the current Expo app owner account's EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
@@ -1803,7 +1803,7 @@ DESCRIPTION
send a Convex team invitation to your verified email address
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
## `eas login`
@@ -1814,8 +1814,8 @@ USAGE
$ eas login [-s] [-b]
FLAGS
-b, --browser Login with your browser
-s, --sso Login with SSO
-b, --[no-]browser Log in with your browser (default; use --no-browser for CLI-based login)
-s, --sso Log in with SSO
DESCRIPTION
log in with your Expo account
@@ -1855,7 +1855,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1873,7 +1873,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1891,7 +1891,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas new [PATH]`
@@ -1943,7 +1943,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.13.1/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1957,7 +1957,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1980,7 +1980,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:new [PATH]`
@@ -2004,7 +2004,7 @@ ALIASES
$ eas new
```
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/project/new.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -2022,7 +2022,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -2055,7 +2055,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.13.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -2098,7 +2098,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -2119,7 +2119,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -2140,7 +2140,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -2165,7 +2165,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:insights GROUPID`
@@ -2192,7 +2192,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/insights.ts)_
## `eas update:list`
@@ -2219,7 +2219,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -2257,7 +2257,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:revert-update-rollout`
@@ -2285,7 +2285,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
## `eas update:roll-back-to-embedded`
@@ -2315,7 +2315,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -2336,7 +2336,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -2360,7 +2360,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas upload`
@@ -2381,7 +2381,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.13.1/packages/eas-cli/src/commands/upload.ts)_
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/upload.ts)_
## `eas webhook:create`
@@ -2403,7 +2403,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -2423,7 +2423,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -2442,7 +2442,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -2465,7 +2465,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -2482,7 +2482,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2603,7 +2603,7 @@ DESCRIPTION
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/cancel.ts)_
## `eas workflow:create [NAME]`
@@ -2623,7 +2623,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:logs [ID]`
@@ -2647,7 +2647,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/logs.ts)_
## `eas workflow:run FILE`
@@ -2687,7 +2687,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:runs`
@@ -2709,7 +2709,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/runs.ts)_
## `eas workflow:status [WORKFLOW_RUN_ID]`
@@ -2737,7 +2737,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/status.ts)_
## `eas workflow:validate PATH`
@@ -2757,7 +2757,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/validate.ts)_
## `eas workflow:view [ID]`
@@ -2779,5 +2779,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.13.1/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/v19.0.1/packages/eas-cli/src/commands/workflow/view.ts)_
<!-- commandsstop -->
File diff suppressed because it is too large. Load diff
+8 -5
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli",
"version": "18.13.1",
"version": "19.0.1",
"description": "EAS command line tool",
"keywords": [
"cli",
@@ -47,11 +47,11 @@
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "55.0.10",
"@expo/config-plugins": "55.0.7",
"@expo/eas-build-job": "18.13.1",
"@expo/eas-json": "18.13.1",
"@expo/eas-build-job": "19.0.0",
"@expo/eas-json": "19.0.0",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "18.5.0",
"@expo/logger": "19.0.0",
"@expo/multipart-body-parser": "2.0.0",
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.9.10",
@@ -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.13.1",
"@expo/steps": "19.0.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^4.8.3",
"@oclif/plugin-autocomplete": "^3.2.40",
@@ -221,6 +221,9 @@
"secret": {
"description": "manage project and account secrets"
},
"simulator": {
"description": "manage remote simulator sessions"
},
"update": {
"description": "manage individual updates"
},
@@ -13,14 +13,15 @@ export default class AccountLogin extends EasCommand {
static override flags = {
// can pass either --sso or -s
sso: Flags.boolean({
description: 'Login with SSO',
description: 'Log in with SSO',
char: 's',
default: false,
}),
browser: Flags.boolean({
description: 'Login with your browser',
description: 'Log in with your browser (default; use --no-browser for CLI-based login)',
char: 'b',
default: false,
default: true,
allowNo: true,
}),
};
@@ -152,6 +152,37 @@ describe(ObserveRoutes, () => {
expect(options.buildNumber).toBe('42');
});
it('passes --route-name flags through as routeNames array', async () => {
const command = createCommand(['--route-name', '/home', '--route-name', '/profile']);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.routeNames).toEqual(['/home', '/profile']);
});
it('dedupes duplicate --route-name flags', async () => {
const command = createCommand([
'--route-name',
'/home',
'--route-name',
'/profile',
'--route-name',
'/home',
]);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.routeNames).toEqual(['/home', '/profile']);
});
it('does not pass routeNames when --route-name is not provided', async () => {
const command = createCommand([]);
await command.runAsync();
const options = mockFetchObserveNavigationRoutesAsync.mock.calls[0][2];
expect(options.routeNames).toBeUndefined();
});
it('resolves --stat aliases and passes them through', async () => {
const command = createCommand(['--stat', 'p90', '--stat', 'eventCount']);
await command.runAsync();
@@ -62,6 +62,11 @@ export default class ObserveRoutes extends EasCommand {
'build-number': Flags.string({
description: 'Filter by app build number',
}),
'route-name': Flags.string({
description:
'Filter by route name (can be specified multiple times to include several routes)',
multiple: true,
}),
...ObserveProjectIdFlag,
...EasNonInteractiveAndJsonFlags,
};
@@ -100,6 +105,10 @@ export default class ObserveRoutes extends EasCommand {
? Array.from(new Set(flags.stat.map(resolveNavigationStatKey)))
: undefined;
const routeNames = flags['route-name']?.length
? Array.from(new Set(flags['route-name']))
: undefined;
const { daysBack, startTime, endTime } = resolveTimeRange(flags);
const platforms = appPlatformsFromFlag(flags.platform);
@@ -115,6 +124,7 @@ export default class ObserveRoutes extends EasCommand {
appVersion: flags['app-version'],
updateId: flags['update-id'],
buildNumber: flags['build-number'],
routeNames,
}
);
@@ -10,7 +10,6 @@ import { ora } from '../../ora';
import { formatRemoteSessionInstructions } from '../../simulator/utils';
export default class SimulatorGet extends EasCommand {
static override hidden = true;
static override description =
'[EXPERIMENTAL] get info about a remote simulator session on EAS by its device run session ID';
@@ -3,7 +3,10 @@ import { Flags } from '@oclif/core';
import { getBareJobRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import {
EasNonInteractiveAndJsonFlags,
resolveNonInteractiveAndJsonFlags,
} from '../../commandUtils/flags';
import {
AppPlatform,
DeviceRunSessionStatus,
@@ -18,6 +21,7 @@ import {
DeviceRunSessionRemoteConfig,
formatRemoteSessionInstructions,
} from '../../simulator/utils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { sleepAsync } from '../../utils/promise';
import nullthrows from 'nullthrows';
@@ -28,6 +32,7 @@ const POLL_TIMEOUT_MS = 15 * 60 * 1_000; // 15 minutes
// 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',
[DeviceRunSessionType.Argent]: 'argent',
[DeviceRunSessionType.ServeSim]: 'serve-sim',
};
@@ -38,7 +43,6 @@ const DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE = Object.fromEntries(
) as Record<string, DeviceRunSessionType>;
export default class SimulatorStart extends EasCommand {
static override hidden = true;
static override description =
'[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it';
@@ -57,7 +61,7 @@ export default class SimulatorStart extends EasCommand {
description:
'Version of the package backing the device run session (e.g. "0.1.3-alpha.3"). Defaults to "latest" when omitted.',
}),
...EASNonInteractiveFlag,
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
@@ -67,12 +71,17 @@ export default class SimulatorStart extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = await this.parse(SimulatorStart);
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
if (jsonFlag) {
enableJsonOutput();
}
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(SimulatorStart, {
nonInteractive: flags['non-interactive'],
nonInteractive,
});
const platform = flags.platform === 'android' ? AppPlatform.Android : AppPlatform.Ios;
@@ -148,11 +157,21 @@ export default class SimulatorStart extends EasCommand {
);
}
if (jsonFlag) {
printJsonOnlyOutput({
id: deviceRunSessionId,
type: flags.type,
jobRunUrl,
remoteConfig,
});
return;
}
Log.newLine();
Log.log(formatRemoteSessionInstructions(remoteConfig));
Log.newLine();
if (flags['non-interactive']) {
if (nonInteractive) {
Log.log(
`When you are done, stop the session with: eas simulator:stop --id ${deviceRunSessionId}`
);
@@ -6,7 +6,6 @@ import { DeviceRunSessionMutation } from '../../graphql/mutations/DeviceRunSessi
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';
+127 -2
View File
@@ -120,6 +120,7 @@ export type Account = {
/** Billing information. Only visible to members with the ADMIN or OWNER role. */
billing?: Maybe<Billing>;
billingPeriod: BillingPeriod;
concurrencyConsumers: Array<ConcurrencyConsumer>;
/** Convex team connections for this account */
convexTeamConnections: Array<ConvexTeamConnection>;
createdAt: Scalars['DateTime']['output'];
@@ -399,6 +400,15 @@ export type AccountBillingPeriodArgs = {
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountConcurrencyConsumersArgs = {
limit?: Scalars['Int']['input'];
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
@@ -2167,6 +2177,7 @@ export type AppObserveCustomEventNamesArgs = {
appVersion?: InputMaybe<Scalars['String']['input']>;
endTime: Scalars['DateTime']['input'];
environment?: InputMaybe<Scalars['String']['input']>;
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
orderBy?: InputMaybe<AppObserveCustomEventNamesOrderBy>;
platform?: InputMaybe<AppObservePlatform>;
startTime: Scalars['DateTime']['input'];
@@ -2209,10 +2220,23 @@ export type AppObserveUpdatesArgs = {
input: AppObserveUpdatesInput;
};
export type AppObserveAppBuildEmbeddedSummary = {
__typename?: 'AppObserveAppBuildEmbeddedSummary';
eventCount: Scalars['Int']['output'];
firstSeenAt: Scalars['DateTime']['output'];
uniqueUserCount: Scalars['Int']['output'];
};
export type AppObserveAppBuildNumber = {
__typename?: 'AppObserveAppBuildNumber';
appBuildNumber: Scalars['String']['output'];
easBuilds: Array<AppObserveAppEasBuild>;
/**
* Summary restricted to events running this build's embedded bundle
* (no OTA update applied). Null when the build has no such events
* in the queried time range.
*/
embedded?: Maybe<AppObserveAppBuildEmbeddedSummary>;
eventCount: Scalars['Int']['output'];
firstSeenAt: Scalars['DateTime']['output'];
uniqueUserCount: Scalars['Int']['output'];
@@ -2229,6 +2253,7 @@ export type AppObserveAppEasBuild = {
export type AppObserveAppUpdate = {
__typename?: 'AppObserveAppUpdate';
appUpdateId: Scalars['String']['output'];
appUpdateMessage?: Maybe<Scalars['String']['output']>;
easBuilds: Array<AppObserveAppEasBuild>;
eventCount: Scalars['Int']['output'];
firstSeenAt: Scalars['DateTime']['output'];
@@ -2259,6 +2284,7 @@ export type AppObserveCustomEvent = {
appEasBuildId?: Maybe<Scalars['String']['output']>;
appIdentifier: Scalars['String']['output'];
appUpdateId?: Maybe<Scalars['String']['output']>;
appUpdateMessage?: Maybe<Scalars['String']['output']>;
appVersion: Scalars['String']['output'];
clientVersion?: Maybe<Scalars['String']['output']>;
countryCode?: Maybe<Scalars['String']['output']>;
@@ -2300,6 +2326,7 @@ export type AppObserveCustomEventCountsInput = {
endTime: Scalars['DateTime']['input'];
environment?: InputMaybe<Scalars['String']['input']>;
eventName: Scalars['String']['input'];
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
platform: AppObservePlatform;
startTime: Scalars['DateTime']['input'];
};
@@ -2325,6 +2352,7 @@ export type AppObserveCustomEventListFilter = {
endTime?: InputMaybe<Scalars['DateTime']['input']>;
environment?: InputMaybe<Scalars['String']['input']>;
eventName?: InputMaybe<Scalars['String']['input']>;
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
platform?: InputMaybe<AppObservePlatform>;
propertyFilters?: InputMaybe<Array<AppObserveCustomEventPropertyFilter>>;
sessionId?: InputMaybe<Scalars['String']['input']>;
@@ -2369,6 +2397,7 @@ export type AppObserveEvent = {
appIdentifier: Scalars['String']['output'];
appName: Scalars['String']['output'];
appUpdateId?: Maybe<Scalars['String']['output']>;
appUpdateMessage?: Maybe<Scalars['String']['output']>;
appVersion: Scalars['String']['output'];
clientVersion?: Maybe<Scalars['String']['output']>;
countryCode?: Maybe<Scalars['String']['output']>;
@@ -2423,6 +2452,7 @@ export type AppObserveEventsFilter = {
easClientId?: InputMaybe<Scalars['String']['input']>;
endTime?: InputMaybe<Scalars['DateTime']['input']>;
environment?: InputMaybe<Scalars['String']['input']>;
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
metricName?: InputMaybe<Scalars['String']['input']>;
platform?: InputMaybe<AppObservePlatform>;
routeName?: InputMaybe<Scalars['String']['input']>;
@@ -2479,6 +2509,7 @@ export type AppObserveNavigationRoutesFilter = {
endTime: Scalars['DateTime']['input'];
environment?: InputMaybe<Scalars['String']['input']>;
platform: AppObservePlatform;
routeNames?: InputMaybe<Array<Scalars['String']['input']>>;
startTime: Scalars['DateTime']['input'];
};
@@ -2556,6 +2587,7 @@ export type AppObserveTimeSeriesInput = {
bucketIntervalMinutes?: InputMaybe<Scalars['Int']['input']>;
endTime: Scalars['DateTime']['input'];
environment?: InputMaybe<Scalars['String']['input']>;
isEmbeddedUpdate?: InputMaybe<Scalars['Boolean']['input']>;
metricName: Scalars['String']['input'];
platform: AppObservePlatform;
routeName?: InputMaybe<Scalars['String']['input']>;
@@ -2577,6 +2609,7 @@ export type AppObserveTimeSeriesStatistics = {
export type AppObserveUpdate = {
__typename?: 'AppObserveUpdate';
appUpdateId: Scalars['String']['output'];
appUpdateMessage?: Maybe<Scalars['String']['output']>;
appVersion: Scalars['String']['output'];
downloadCount: Scalars['Int']['output'];
firstSeenAt: Scalars['DateTime']['output'];
@@ -3470,6 +3503,16 @@ export enum AppsFilter {
New = 'NEW'
}
export type ArgentRunSessionRemoteConfig = {
__typename?: 'ArgentRunSessionRemoteConfig';
toolsUrl: Scalars['String']['output'];
/**
* URL of the web preview surface for the session. Null when web previews are
* not available for the platform (e.g. Android).
*/
webPreviewUrl?: Maybe<Scalars['String']['output']>;
};
export type AscApiKeyInput = {
issuerIdentifier: Scalars['String']['input'];
keyIdentifier: Scalars['String']['input'];
@@ -4179,6 +4222,7 @@ export enum BuildPhase {
EasBuildInternal = 'EAS_BUILD_INTERNAL',
FailBuild = 'FAIL_BUILD',
FixGradlew = 'FIX_GRADLEW',
GradleBuildProfile = 'GRADLE_BUILD_PROFILE',
InstallCustomTools = 'INSTALL_CUSTOM_TOOLS',
InstallDependencies = 'INSTALL_DEPENDENCIES',
InstallPods = 'INSTALL_PODS',
@@ -4187,6 +4231,7 @@ export enum BuildPhase {
OnBuildErrorHook = 'ON_BUILD_ERROR_HOOK',
OnBuildSuccessHook = 'ON_BUILD_SUCCESS_HOOK',
ParseCustomWorkflowConfig = 'PARSE_CUSTOM_WORKFLOW_CONFIG',
ParseXcactivitylog = 'PARSE_XCACTIVITYLOG',
PostInstallHook = 'POST_INSTALL_HOOK',
Prebuild = 'PREBUILD',
PrepareArtifacts = 'PREPARE_ARTIFACTS',
@@ -4410,6 +4455,8 @@ export type Concurrencies = {
total: Scalars['Int']['output'];
};
export type ConcurrencyConsumer = Build | JobRun;
export enum ContinentCode {
Af = 'AF',
An = 'AN',
@@ -5230,7 +5277,7 @@ export type DeviceRunSessionQueryByIdArgs = {
deviceRunSessionId: Scalars['ID']['input'];
};
export type DeviceRunSessionRemoteConfig = AgentDeviceRunSessionRemoteConfig | ServeSimRunSessionRemoteConfig;
export type DeviceRunSessionRemoteConfig = AgentDeviceRunSessionRemoteConfig | ArgentRunSessionRemoteConfig | ServeSimRunSessionRemoteConfig;
export enum DeviceRunSessionStatus {
Errored = 'ERRORED',
@@ -5241,6 +5288,7 @@ export enum DeviceRunSessionStatus {
export enum DeviceRunSessionType {
AgentDevice = 'AGENT_DEVICE',
Argent = 'ARGENT',
ServeSim = 'SERVE_SIM'
}
@@ -6000,6 +6048,67 @@ export type EditUpdateBranchInput = {
newName: Scalars['String']['input'];
};
export type EmbeddedUpdate = {
__typename?: 'EmbeddedUpdate';
channel: Scalars['String']['output'];
createdAt: Scalars['DateTime']['output'];
/** The manifest UUID baked into the binary by expo-updates at build time. */
id: Scalars['ID']['output'];
launchAsset: EmbeddedUpdateAsset;
platform: AppPlatform;
runtimeVersion: Scalars['String']['output'];
};
export type EmbeddedUpdateAsset = {
__typename?: 'EmbeddedUpdateAsset';
contentType: Scalars['String']['output'];
fileSHA256: Scalars['String']['output'];
fileSize: Scalars['Int']['output'];
finalFileSize?: Maybe<Scalars['Int']['output']>;
id: Scalars['ID']['output'];
storageKey: Scalars['String']['output'];
};
export type EmbeddedUpdateAssetMutation = {
__typename?: 'EmbeddedUpdateAssetMutation';
/**
* Returns a presigned POST URL for uploading an embedded update bundle.
* Requires PUBLISH permission on the app.
*/
getSignedEmbeddedUpdateAssetUploadSpecifications: EmbeddedUpdateAssetUploadSpec;
};
export type EmbeddedUpdateAssetMutationGetSignedEmbeddedUpdateAssetUploadSpecificationsArgs = {
appId: Scalars['ID']['input'];
contentType: Scalars['String']['input'];
embeddedUpdateId: Scalars['ID']['input'];
};
export type EmbeddedUpdateAssetUploadSpec = {
__typename?: 'EmbeddedUpdateAssetUploadSpec';
/** Form fields that must be included with the POST request alongside the file. */
fields: Scalars['JSONObject']['output'];
/** Presigned POST URL targeting the upload bucket. Valid for one hour. */
presignedUrl: Scalars['String']['output'];
/** Storage key (`{appId}/{embeddedUpdateId}`). Same key in both upload and destination buckets. */
storageKey: Scalars['String']['output'];
};
export type EmbeddedUpdateMutation = {
__typename?: 'EmbeddedUpdateMutation';
/**
* Register an embedded bundle as the launch asset for a given app/platform/channel.
* Returns EMBEDDED_UPDATE_ASSET_NOT_AVAILABLE if the asset has not been finalized yet.
*/
uploadEmbeddedUpdate: EmbeddedUpdate;
};
export type EmbeddedUpdateMutationUploadEmbeddedUpdateArgs = {
input: UploadEmbeddedUpdateInput;
};
export enum EntityTypeName {
AccountEntity = 'AccountEntity',
AccountSsoConfigurationEntity = 'AccountSSOConfigurationEntity',
@@ -7264,6 +7373,7 @@ export type JobRun = {
createdAt: Scalars['DateTime']['output'];
displayName?: Maybe<Scalars['String']['output']>;
endedAt?: Maybe<Scalars['DateTime']['output']>;
enqueuedAt?: Maybe<Scalars['DateTime']['output']>;
errors: Array<JobRunError>;
expiresAt: Scalars['DateTime']['output'];
gitCommitHash?: Maybe<Scalars['String']['output']>;
@@ -7275,6 +7385,8 @@ export type JobRun = {
logFileUrls: Array<Scalars['String']['output']>;
name: Scalars['String']['output'];
priority: JobRunPriority;
/** String describing the worker profile used to run this job run. */
resourceClassDisplayName: Scalars['String']['output'];
startedAt?: Maybe<Scalars['DateTime']['output']>;
status: JobRunStatus;
updateGroups: Array<Array<Update>>;
@@ -8154,6 +8266,9 @@ export type RootMutation = {
echoTurn: EchoTurnMutation;
/** Mutations for Echo versions */
echoVersion: EchoVersionMutation;
/** Mutations that register embedded update bundles for bundle diffing. */
embeddedUpdate: EmbeddedUpdateMutation;
embeddedUpdateAsset: EmbeddedUpdateAssetMutation;
/** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation;
/** Mutations that create and delete EnvironmentVariables */
@@ -9701,6 +9816,16 @@ export type UpdatesMetricsData = {
labels: Array<Scalars['String']['output']>;
};
export type UploadEmbeddedUpdateInput = {
appId: Scalars['ID']['input'];
channel: Scalars['String']['input'];
/** UUID baked into the binary by expo-updates at build time (from app.manifest id field). */
embeddedUpdateId: Scalars['ID']['input'];
platform: AppPlatform;
runtimeVersion: Scalars['String']['input'];
turtleBuildId?: InputMaybe<Scalars['ID']['input']>;
};
export type UploadSession = {
__typename?: 'UploadSession';
/** Create an Upload Session for a specific account */
@@ -12781,7 +12906,7 @@ export type DeviceRunSessionByIdQueryVariables = Exact<{
}>;
export type DeviceRunSessionByIdQuery = { __typename?: 'RootQuery', deviceRunSessions: { __typename?: 'DeviceRunSessionQuery', byId: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, type: DeviceRunSessionType, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, remoteConfig?: { __typename: 'AgentDeviceRunSessionRemoteConfig', agentDeviceRemoteSessionUrl: string, agentDeviceRemoteSessionToken: string, webPreviewUrl?: string | null } | { __typename: 'ServeSimRunSessionRemoteConfig', previewUrl: string, streamUrl: string } | null, turtleJobRun?: { __typename?: 'JobRun', id: string, status: JobRunStatus } | null } } };
export type DeviceRunSessionByIdQuery = { __typename?: 'RootQuery', deviceRunSessions: { __typename?: 'DeviceRunSessionQuery', byId: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, type: DeviceRunSessionType, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, remoteConfig?: { __typename: 'AgentDeviceRunSessionRemoteConfig', agentDeviceRemoteSessionUrl: string, agentDeviceRemoteSessionToken: string, webPreviewUrl?: string | null } | { __typename: 'ArgentRunSessionRemoteConfig', toolsUrl: string, webPreviewUrl?: string | null } | { __typename: 'ServeSimRunSessionRemoteConfig', previewUrl: string, streamUrl: string } | null, turtleJobRun?: { __typename?: 'JobRun', id: string, status: JobRunStatus } | null } } };
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -34,6 +34,10 @@ export const DeviceRunSessionQuery = {
agentDeviceRemoteSessionToken
webPreviewUrl
}
... on ArgentRunSessionRemoteConfig {
toolsUrl
webPreviewUrl
}
... on ServeSimRunSessionRemoteConfig {
previewUrl
streamUrl
@@ -85,6 +85,34 @@ describe('fetchObserveNavigationRoutesAsync', () => {
expect(filter).not.toHaveProperty('appVersion');
expect(filter).not.toHaveProperty('appUpdateId');
expect(filter).not.toHaveProperty('appBuildNumber');
expect(filter).not.toHaveProperty('routeNames');
});
it('forwards routeNames filter when provided', async () => {
await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', {
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios],
limit: 50,
routeNames: ['/home', '/profile'],
});
expect(mockNavigationRoutesAsync.mock.calls[0][1].filter.routeNames).toEqual([
'/home',
'/profile',
]);
});
it('does not send routeNames when array is empty', async () => {
await fetchObserveNavigationRoutesAsync(mockGraphqlClient, 'project-123', {
startTime: '2025-01-01T00:00:00.000Z',
endTime: '2025-03-01T00:00:00.000Z',
platforms: [AppPlatform.Ios],
limit: 50,
routeNames: [],
});
expect(mockNavigationRoutesAsync.mock.calls[0][1].filter).not.toHaveProperty('routeNames');
});
it('forwards after cursor when provided', async () => {
@@ -25,6 +25,7 @@ export interface FetchNavigationRoutesOptions {
appVersion?: string;
updateId?: string;
buildNumber?: string;
routeNames?: string[];
orderBy?: AppObserveNavigationRoutesOrderBy;
}
@@ -55,6 +56,7 @@ export async function fetchObserveNavigationRoutesAsync(
...(options.appVersion && { appVersion: options.appVersion }),
...(options.updateId && { appUpdateId: options.updateId }),
...(options.buildNumber && { appBuildNumber: options.buildNumber }),
...(options.routeNames?.length && { routeNames: options.routeNames }),
},
first: options.limit,
...(options.after && { after: options.after }),
+100 -10
View File
@@ -1,8 +1,14 @@
import { ExportedConfig, IOSConfig } from '@expo/config-plugins';
import { ExportedConfig, IOSConfig, compileModsAsync } from '@expo/config-plugins';
import { JSONObject } from '@expo/json-file';
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
import Log from '../../log';
import Sentry from '../../sentry';
import { spawnExpoCommand } from '../../utils/expoCli';
import { readPlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
import { isExpoInstalled } from '../projectUtils';
import { hasIgnoredIosProjectAsync } from '../workflow';
interface Target {
buildConfiguration?: string;
@@ -11,17 +17,101 @@ interface Target {
export async function getManagedApplicationTargetEntitlementsAsync(
projectDir: string,
env: Record<string, string>
env: Record<string, string>,
vcsClient: Client
): Promise<JSONObject> {
const { stdout } = await spawnExpoCommand(
projectDir,
['config', '--json', '--type', 'introspect'],
{
env,
let expoConfigError: Error | undefined;
if (isExpoInstalled(projectDir)) {
try {
const { stdout } = await spawnExpoCommand(
projectDir,
['config', '--json', '--type', 'introspect'],
{
env: {
...env,
EXPO_NO_DOTENV: '1',
},
}
);
const expWithMods: ExportedConfig = JSON.parse(stdout);
return expWithMods.ios?.entitlements ?? {};
} catch (error: any) {
try {
Sentry.withScope(scope => {
if (process.env.EAS_BUILD_PROJECT_ID) {
scope.setTag('app_id', process.env.EAS_BUILD_PROJECT_ID);
}
if (process.env.EAS_BUILD_ID) {
scope.setTag('build_id', process.env.EAS_BUILD_ID);
}
scope.setTag('config_resolution', 'ios_entitlements_introspection');
scope.setExtra('message', 'iOS entitlements config fallback');
scope.setExtra('stdout', error.stdout);
scope.setExtra('stderr', error.stderr);
Sentry.captureException(error);
});
} catch {
// do nothing
}
expoConfigError = error;
Log.warn(
`Failed to read the app config from the project using the local Expo CLI: ${formatError(error)}`
);
Log.warn('Falling back to the version of "@expo/config" shipped with the EAS CLI.');
}
);
const expWithMods: ExportedConfig = JSON.parse(stdout);
return expWithMods.ios?.entitlements ?? {};
}
try {
return await resolveManagedApplicationTargetEntitlementsWithBundledConfigAsync(
projectDir,
env,
vcsClient
);
} catch (fallbackError: any) {
if (expoConfigError) {
throw new Error(
`Failed to resolve iOS entitlements from Expo config. The local Expo CLI failed with: ${formatError(
expoConfigError
)}. The bundled config fallback also failed with: ${formatError(fallbackError)}`,
{ cause: fallbackError }
);
}
throw new Error(
`Failed to resolve iOS entitlements from Expo config using the bundled config fallback: ${formatError(
fallbackError
)}`,
{ cause: fallbackError }
);
}
}
async function resolveManagedApplicationTargetEntitlementsWithBundledConfigAsync(
projectDir: string,
env: Record<string, string>,
vcsClient: Client
): Promise<JSONObject> {
const originalProcessEnv = process.env;
try {
process.env = {
...process.env,
...env,
};
const { exp } = await getPrebuildConfigAsync(projectDir, { platforms: ['ios'] });
const expWithMods = await compileModsAsync(exp, {
projectRoot: projectDir,
platforms: ['ios'],
introspect: true,
ignoreExistingNativeFiles: await hasIgnoredIosProjectAsync(projectDir, vcsClient),
});
return expWithMods.ios?.entitlements ?? {};
} finally {
process.env = originalProcessEnv;
}
}
function formatError(error: Error & { stderr?: string; stdout?: string }): string {
return error.stderr?.trim() || error.stdout?.trim() || error.message;
}
export async function getNativeTargetEntitlementsAsync(
+2 -1
View File
@@ -60,7 +60,8 @@ export async function resolveManagedProjectTargetsAsync({
);
const applicationTargetEntitlements = await getManagedApplicationTargetEntitlementsAsync(
projectDir,
env ?? {}
env ?? {},
vcsClient
);
const appExtensions: UserDefinedTarget[] =
exp.extra?.eas?.build?.experimental?.ios?.appExtensions ?? [];
+17 -2
View File
@@ -10,7 +10,7 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
import { AccountFragment } from '../graphql/generated';
import { AppQuery } from '../graphql/queries/AppQuery';
import Log, { learnMore } from '../log';
import { expoCommandAsync } from '../utils/expoCli';
import { expoCommandAsync, resolveExpoCli } from '../utils/expoCli';
/**
* Return a useful name describing the project config.
@@ -45,7 +45,22 @@ export function isExpoNotificationsInstalled(projectDir: string): boolean {
export function isExpoInstalled(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return !!(packageJson.dependencies && 'expo' in packageJson.dependencies);
if (!!(packageJson.dependencies && 'expo' in packageJson.dependencies)) {
// NOTE(@kitten): We usually don't apply strict checks for installed packages, but
// `isExpoInstalled` is often used to check if we should call the Expo CLI, so we're
// also checking if we can resolve `expo` here
try {
return !!resolveExpoCli(projectDir);
} catch (e: any) {
if (e.code === 'MODULE_NOT_FOUND') {
return false;
} else {
throw e;
}
}
} else {
return false;
}
}
export function isExpoUpdatesInstalledAsDevDependency(projectDir: string): boolean {
+16
View File
@@ -24,6 +24,22 @@ export function formatRemoteSessionInstructions(
}
return lines.join('\n');
}
case 'ArgentRunSessionRemoteConfig': {
const lines = [
'🔑 Open the following URL to access the Argent tools for this session:',
'',
remoteConfig.toolsUrl,
];
if (remoteConfig.webPreviewUrl) {
lines.push(
'',
'🌐 Open the following URL in your browser to preview the simulator:',
'',
remoteConfig.webPreviewUrl
);
}
return lines.join('\n');
}
case 'ServeSimRunSessionRemoteConfig':
return [
'🌐 Open the following URL in your browser to access the simulator:',
@@ -294,7 +294,7 @@ describe(SessionManager, () => {
username: 'USERNAME',
});
await sessionManager.showLoginPromptAsync();
await sessionManager.showLoginPromptAsync({ browser: false });
expect(sessionManagerRetryUsernamePasswordAuthWithOTPAsyncSpy).toHaveBeenCalledWith(
'hello',
@@ -313,7 +313,7 @@ describe(SessionManager, () => {
);
});
it('calls regular login if the sso flag is false', async () => {
it('calls regular login by default', async () => {
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ username: 'USERNAME', password: 'PASSWORD' }))
@@ -321,14 +321,19 @@ describe(SessionManager, () => {
throw new Error("shouldn't happen");
});
jest.mocked(fetchSessionSecretAndUserAsync).mockResolvedValue({
sessionSecret: 'SESSION_SECRET',
id: 'USER_ID',
username: 'USERNAME',
});
const sessionManager = new SessionManager(analytics);
// Regular login
await sessionManager.showLoginPromptAsync({ sso: false });
await sessionManager.showLoginPromptAsync();
expect(fetchSessionSecretAndUserAsync).toHaveBeenCalled();
});
it('calls regular login if the sso flag is undefined', async () => {
it('calls regular login when browser is false', async () => {
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ username: 'USERNAME', password: 'PASSWORD' }))
@@ -338,8 +343,21 @@ describe(SessionManager, () => {
const sessionManager = new SessionManager(analytics);
// Regular login
await sessionManager.showLoginPromptAsync({ sso: undefined });
await sessionManager.showLoginPromptAsync({ browser: false });
expect(fetchSessionSecretAndUserAsync).toHaveBeenCalled();
});
it('calls regular login if the sso flag is false and browser is false', async () => {
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ username: 'USERNAME', password: 'PASSWORD' }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager(analytics);
await sessionManager.showLoginPromptAsync({ sso: false, browser: false });
expect(fetchSessionSecretAndUserAsync).toHaveBeenCalled();
});
+7 -2
View File
@@ -78,6 +78,12 @@ export const shouldUseVersionedExpoCLIWithExplicitPlatforms = memoize(
shouldUseVersionedExpoCLIWithExplicitPlatformsExpensive
);
export function resolveExpoCli(projectDir: string): string {
return (
silentResolveFrom(projectDir, 'expo/bin/cli') ?? resolveFrom(projectDir, 'expo/bin/cli.js')
);
}
export function spawnExpoCommand(
projectDir: string,
args: string[],
@@ -85,8 +91,7 @@ export function spawnExpoCommand(
): spawnAsync.SpawnPromise<spawnAsync.SpawnResult> {
let expoCliPath;
try {
expoCliPath =
silentResolveFrom(projectDir, 'expo/bin/cli') ?? resolveFrom(projectDir, 'expo/bin/cli.js');
expoCliPath = resolveExpoCli(projectDir);
} catch (e: any) {
if (e.code === 'MODULE_NOT_FOUND') {
throw new Error(
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/eas-json",
"version": "18.13.1",
"version": "19.0.0",
"description": "A library for interacting with eas.json",
"homepage": "https://github.com/expo/eas-cli",
"bugs": "https://github.com/expo/eas-cli/issues",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/eas-build-job": "18.13.1",
"@expo/eas-build-job": "19.0.0",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "11.2.0",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli-local-build-plugin",
"version": "18.13.1",
"version": "19.0.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,11 +26,11 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/build-tools": "18.13.1",
"@expo/eas-build-job": "18.13.1",
"@expo/logger": "18.5.0",
"@expo/build-tools": "19.0.0",
"@expo/eas-build-job": "19.0.0",
"@expo/logger": "19.0.0",
"@expo/spawn-async": "^1.7.2",
"@expo/turtle-spawn": "18.5.0",
"@expo/turtle-spawn": "19.0.0",
"bunyan": "^1.8.15",
"chalk": "4.1.2",
"env-paths": "^2.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/logger",
"version": "18.5.0",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/steps",
"version": "18.13.1",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -27,8 +27,8 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/eas-build-job": "18.13.1",
"@expo/logger": "18.5.0",
"@expo/eas-build-job": "19.0.0",
"@expo/logger": "19.0.0",
"@expo/spawn-async": "^1.7.2",
"arg": "^5.0.2",
"fs-extra": "^11.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/template-file",
"version": "18.5.0",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/turtle-spawn",
"version": "18.5.0",
"version": "19.0.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
@@ -25,7 +25,7 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/logger": "18.5.0",
"@expo/logger": "19.0.0",
"@expo/spawn-async": "^1.7.2"
},
"devDependencies": {
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/worker",
"version": "18.13.1",
"version": "19.0.0",
"private": true,
"scripts": {
"start": "yarn watch",
@@ -19,18 +19,18 @@
},
"dependencies": {
"@babel/core": "7.24.7",
"@expo/build-tools": "18.13.1",
"@expo/build-tools": "19.0.0",
"@expo/config-plugins": "55.0.7",
"@expo/config-types": "55.0.5",
"@expo/downloader": "18.5.0",
"@expo/eas-build-job": "18.13.1",
"@expo/logger": "18.5.0",
"@expo/downloader": "19.0.0",
"@expo/eas-build-job": "19.0.0",
"@expo/logger": "19.0.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "18.13.1",
"@expo/template-file": "18.5.0",
"@expo/turtle-spawn": "18.5.0",
"@expo/steps": "19.0.0",
"@expo/template-file": "19.0.0",
"@expo/turtle-spawn": "19.0.0",
"@hapi/boom": "10.0.1",
"check-disk-space": "3.4.0",
"fast-glob": "3.3.2",
+168 -2
View File
@@ -1,12 +1,57 @@
import { errors } from '@expo/eas-build-job';
import { Datadog } from '@expo/build-tools';
import { BuildPhase, BuildPhaseResult, Job, errors } from '@expo/eas-build-job';
import spawn from '@expo/turtle-spawn';
import { vol } from 'memfs';
import path from 'path';
import { getExpoPackageVersionAsync } from '../service';
import { build } from '../build';
import { createBuildContext } from '../context';
import { createBuildLoggerWithSecretsFilter } from '../logger';
import BuildService, { getExpoPackageVersionAsync } from '../service';
jest.mock('fs');
jest.mock('@expo/build-tools', () => {
const actual = jest.requireActual('@expo/build-tools');
return {
...actual,
GCS: { uploadWithSignedUrl: jest.fn() },
Datadog: {
setup: jest.fn(),
flushAsync: jest.fn(async () => {}),
},
};
});
jest.mock('@expo/turtle-spawn', () => jest.fn());
jest.mock('../build', () => ({
build: jest.fn(),
}));
jest.mock('../context', () => ({
createBuildContext: jest.fn(() => ({ job: {} })),
}));
jest.mock('../config', () => {
const actual = jest.requireActual('../config').default;
return {
__esModule: true,
default: { ...actual, buildId: 'build-id' },
};
});
jest.mock('../logger', () => ({
__esModule: true,
default: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
level: jest.fn(),
},
createBuildLoggerWithSecretsFilter: jest.fn(),
}));
const buildMock = jest.mocked(build);
const createBuildContextMock = jest.mocked(createBuildContext);
const createBuildLoggerWithSecretsFilterMock = jest.mocked(createBuildLoggerWithSecretsFilter);
const datadogSetupMock = jest.mocked(Datadog.setup);
const datadogFlushAsyncMock = jest.mocked(Datadog.flushAsync);
describe(getExpoPackageVersionAsync, () => {
const projectRoot = '/test-project';
@@ -59,3 +104,124 @@ describe(getExpoPackageVersionAsync, () => {
await expect(getExpoPackageVersionAsync(buildContext)).rejects.toBeInstanceOf(errors.UserError);
});
});
describe('BuildService.reportBuildPhaseStats', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('sends phase stats to the launcher websocket', () => {
const send = jest.fn();
const service = new BuildService();
service.setWS({ send } as any);
const stats = {
buildPhase: BuildPhase.RUN_FASTLANE,
result: BuildPhaseResult.SUCCESS,
durationMs: 1234,
};
service.reportBuildPhaseStats(stats);
expect(send).toHaveBeenCalledWith({
type: 'build-phase-stats',
...stats,
});
});
it('does not send phase stats when there is no launcher websocket', () => {
const service = new BuildService();
service.reportBuildPhaseStats({
buildPhase: BuildPhase.PREPARE_PROJECT,
result: BuildPhaseResult.SUCCESS,
durationMs: 1,
});
expect(datadogSetupMock).not.toHaveBeenCalled();
});
});
describe('BuildService Datadog setup', () => {
const buildLogger = { child: jest.fn(() => buildLogger) } as any;
beforeEach(() => {
jest.clearAllMocks();
buildMock.mockResolvedValue({});
createBuildContextMock.mockReturnValue({ job: {} } as any);
createBuildLoggerWithSecretsFilterMock.mockResolvedValue({
logger: buildLogger,
cleanUp: jest.fn(async () => {}),
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] } as any,
outputStream: {} as any,
});
});
it('configures Datadog for build jobs', async () => {
const service = new BuildService();
service.checkForHangingWorker = jest.fn(async () => {});
await (service as any).startBuildInternal({
job: {
platform: 'ios',
secrets: { robotAccessToken: 'token-abc' },
} as Job,
metadata: {},
projectId: 'project-id',
initiatingUserId: 'user-id',
});
expect(datadogSetupMock).toHaveBeenCalledWith({
expoApiV2BaseUrl: expect.any(String),
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
expect(createBuildContextMock).toHaveBeenCalledWith(
expect.objectContaining({
reportBuildPhaseStatsFn: expect.any(Function),
})
);
expect(datadogFlushAsyncMock).toHaveBeenCalled();
});
it('configures Datadog for non-platform jobs with the turtle build id', async () => {
const service = new BuildService();
service.checkForHangingWorker = jest.fn(async () => {});
await (service as any).startBuildInternal({
job: {
secrets: { robotAccessToken: 'token-abc' },
} as Job,
metadata: {},
projectId: 'project-id',
initiatingUserId: 'user-id',
});
expect(datadogSetupMock).toHaveBeenCalledWith({
expoApiV2BaseUrl: expect.any(String),
turtleBuildId: 'build-id',
robotAccessToken: 'token-abc',
});
expect(createBuildContextMock).toHaveBeenCalledWith(
expect.objectContaining({
reportBuildPhaseStatsFn: expect.any(Function),
})
);
});
it('leaves Datadog unconfigured when the robot access token is unavailable', async () => {
const service = new BuildService();
service.checkForHangingWorker = jest.fn(async () => {});
await (service as any).startBuildInternal({
job: {
platform: 'ios',
secrets: {},
} as Job,
metadata: {},
projectId: 'project-id',
initiatingUserId: 'user-id',
});
expect(datadogSetupMock).not.toHaveBeenCalled();
});
});
+25 -1
View File
@@ -1,4 +1,12 @@
import { Artifacts, BuildContext, Builders, runGenericJobAsync } from '@expo/build-tools';
import {
Artifacts,
BuildContext,
Builders,
Sentry,
parseGradleProfile,
formatGradleProfileReport,
runGenericJobAsync,
} from '@expo/build-tools';
import {
Android,
BuildJob,
@@ -13,6 +21,7 @@ import {
} from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import omit from 'lodash/omit';
import path from 'path';
import config from './config';
import { displayWorkerRuntimeInfo } from './displayRuntimeInfo';
@@ -83,6 +92,21 @@ export async function build({
analytics.logEvent(Event.WORKER_BUILD_SUCCESS, {});
if (job.platform === Platform.ANDROID) {
try {
await ctx.runBuildPhase(BuildPhase.GRADLE_BUILD_PROFILE, async () => {
const androidDir = path.join(ctx.getReactNativeProjectDirectory(), 'android');
const profileTasks = await parseGradleProfile(androidDir);
if (profileTasks.length > 0) {
const report = formatGradleProfileReport(profileTasks);
ctx.logger.info(report);
}
});
} catch (err: any) {
Sentry.capture('Failed to parse Gradle build profile', err);
}
}
return artifacts;
} catch (err: any) {
if ('mode' in job && ![BuildMode.CUSTOM, BuildMode.REPACK].includes(job.mode)) {
+12
View File
@@ -1,6 +1,7 @@
import {
Artifacts,
BuildContext,
Datadog,
Hook,
findAndUploadXcodeBuildLogsAsync,
runHookIfPresent,
@@ -122,6 +123,7 @@ export default class BuildService {
const isSocketClosed: boolean = !this.ws;
// wait 5 seconds to make sure all logs are flushed
await setTimeoutAsync(5 * 1000);
await Datadog.flushAsync();
await this.logsCleanUp?.();
if (this.ws) {
logger.info('Send build result - error');
@@ -143,6 +145,7 @@ export default class BuildService {
buildArtifactsName: artifacts.BUILD_ARTIFACTS ?? null,
});
const isSocketClosed: boolean = !this.ws;
await Datadog.flushAsync();
await this.logsCleanUp?.();
if (this.ws) {
logger.info('Send build result - success');
@@ -195,6 +198,7 @@ export default class BuildService {
}
const isSocketClosed: boolean = !this.ws;
await Datadog.flushAsync();
await this.logsCleanUp?.();
if (this.ws) {
logger.info('Send build result - aborted');
@@ -286,6 +290,14 @@ export default class BuildService {
this.logsCleanUp = cleanUp;
const analytics = new Analytics(initiatingUserId, metadata?.trackingContext ?? {});
const robotAccessToken = job.secrets?.robotAccessToken;
if (robotAccessToken) {
Datadog.setup({
expoApiV2BaseUrl: config.wwwApiV2BaseUrl,
turtleBuildId: this.buildId,
robotAccessToken,
});
}
const ctx = createBuildContext({
job,
Generated
+36 -36
View File
@@ -2856,24 +2856,24 @@ __metadata:
languageName: node
linkType: hard
"@expo/build-tools@npm:18.13.1, @expo/build-tools@workspace:packages/build-tools":
"@expo/build-tools@npm:19.0.0, @expo/build-tools@workspace:packages/build-tools":
version: 0.0.0-use.local
resolution: "@expo/build-tools@workspace:packages/build-tools"
dependencies:
"@expo/config": "npm:55.0.10"
"@expo/config-plugins": "npm:55.0.7"
"@expo/downloader": "npm:18.5.0"
"@expo/eas-build-job": "npm:18.13.1"
"@expo/downloader": "npm:19.0.0"
"@expo/eas-build-job": "npm:19.0.0"
"@expo/env": "npm:^0.4.0"
"@expo/logger": "npm:18.5.0"
"@expo/logger": "npm:19.0.0"
"@expo/package-manager": "npm:1.9.10"
"@expo/plist": "npm:^0.2.0"
"@expo/repack-app": "npm:~0.4.1"
"@expo/results": "npm:^1.0.0"
"@expo/spawn-async": "npm:1.7.2"
"@expo/steps": "npm:18.13.1"
"@expo/template-file": "npm:18.5.0"
"@expo/turtle-spawn": "npm:18.5.0"
"@expo/steps": "npm:19.0.0"
"@expo/template-file": "npm:19.0.0"
"@expo/turtle-spawn": "npm:19.0.0"
"@expo/xcpretty": "npm:^4.3.1"
"@google-cloud/storage": "npm:^7.11.2"
"@sentry/node": "npm:7.77.0"
@@ -3096,7 +3096,7 @@ __metadata:
languageName: node
linkType: hard
"@expo/downloader@npm:18.5.0, @expo/downloader@workspace:packages/downloader":
"@expo/downloader@npm:19.0.0, @expo/downloader@workspace:packages/downloader":
version: 0.0.0-use.local
resolution: "@expo/downloader@workspace:packages/downloader"
dependencies:
@@ -3112,13 +3112,13 @@ __metadata:
languageName: unknown
linkType: soft
"@expo/eas-build-job@npm:18.13.1, @expo/eas-build-job@workspace:packages/eas-build-job":
"@expo/eas-build-job@npm:19.0.0, @expo/eas-build-job@workspace:packages/eas-build-job":
version: 0.0.0-use.local
resolution: "@expo/eas-build-job@workspace:packages/eas-build-job"
dependencies:
"@expo/logger": "npm:18.5.0"
"@expo/logger": "npm:19.0.0"
"@expo/results": "npm:1.0.0"
"@expo/turtle-spawn": "npm:18.5.0"
"@expo/turtle-spawn": "npm:19.0.0"
"@types/jest": "npm:^29.5.12"
"@types/node": "npm:20.14.2"
jest: "npm:^29.7.0"
@@ -3131,12 +3131,12 @@ __metadata:
languageName: unknown
linkType: soft
"@expo/eas-json@npm:18.13.1, @expo/eas-json@workspace:packages/eas-json":
"@expo/eas-json@npm:19.0.0, @expo/eas-json@workspace:packages/eas-json":
version: 0.0.0-use.local
resolution: "@expo/eas-json@workspace:packages/eas-json"
dependencies:
"@babel/code-frame": "npm:7.23.5"
"@expo/eas-build-job": "npm:18.13.1"
"@expo/eas-build-job": "npm:19.0.0"
"@tsconfig/node20": "npm:20.1.8"
"@types/babel__code-frame": "npm:7.0.3"
"@types/fs-extra": "npm:11.0.4"
@@ -3273,7 +3273,7 @@ __metadata:
languageName: node
linkType: hard
"@expo/logger@npm:18.5.0, @expo/logger@workspace:packages/logger":
"@expo/logger@npm:19.0.0, @expo/logger@workspace:packages/logger":
version: 0.0.0-use.local
resolution: "@expo/logger@workspace:packages/logger"
dependencies:
@@ -3484,12 +3484,12 @@ __metadata:
languageName: node
linkType: hard
"@expo/steps@npm:18.13.1, @expo/steps@workspace:packages/steps":
"@expo/steps@npm:19.0.0, @expo/steps@workspace:packages/steps":
version: 0.0.0-use.local
resolution: "@expo/steps@workspace:packages/steps"
dependencies:
"@expo/eas-build-job": "npm:18.13.1"
"@expo/logger": "npm:18.5.0"
"@expo/eas-build-job": "npm:19.0.0"
"@expo/logger": "npm:19.0.0"
"@expo/spawn-async": "npm:^1.7.2"
"@jest/globals": "npm:^29.7.0"
"@types/jest": "npm:^29.5.12"
@@ -3514,7 +3514,7 @@ __metadata:
languageName: unknown
linkType: soft
"@expo/template-file@npm:18.5.0, @expo/template-file@workspace:packages/template-file":
"@expo/template-file@npm:19.0.0, @expo/template-file@workspace:packages/template-file":
version: 0.0.0-use.local
resolution: "@expo/template-file@workspace:packages/template-file"
dependencies:
@@ -3537,11 +3537,11 @@ __metadata:
languageName: node
linkType: hard
"@expo/turtle-spawn@npm:18.5.0, @expo/turtle-spawn@workspace:packages/turtle-spawn":
"@expo/turtle-spawn@npm:19.0.0, @expo/turtle-spawn@workspace:packages/turtle-spawn":
version: 0.0.0-use.local
resolution: "@expo/turtle-spawn@workspace:packages/turtle-spawn"
dependencies:
"@expo/logger": "npm:18.5.0"
"@expo/logger": "npm:19.0.0"
"@expo/spawn-async": "npm:^1.7.2"
"@types/jest": "npm:^29.5.12"
"@types/node": "npm:20.14.2"
@@ -3557,18 +3557,18 @@ __metadata:
resolution: "@expo/worker@workspace:packages/worker"
dependencies:
"@babel/core": "npm:7.24.7"
"@expo/build-tools": "npm:18.13.1"
"@expo/build-tools": "npm:19.0.0"
"@expo/config-plugins": "npm:55.0.7"
"@expo/config-types": "npm:55.0.5"
"@expo/downloader": "npm:18.5.0"
"@expo/eas-build-job": "npm:18.13.1"
"@expo/logger": "npm:18.5.0"
"@expo/downloader": "npm:19.0.0"
"@expo/eas-build-job": "npm:19.0.0"
"@expo/logger": "npm:19.0.0"
"@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.13.1"
"@expo/template-file": "npm:18.5.0"
"@expo/turtle-spawn": "npm:18.5.0"
"@expo/steps": "npm:19.0.0"
"@expo/template-file": "npm:19.0.0"
"@expo/turtle-spawn": "npm:19.0.0"
"@hapi/boom": "npm:10.0.1"
"@types/babel__core": "npm:7.20.5"
"@types/fs-extra": "npm:11.0.4"
@@ -10348,7 +10348,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.13.1"
"@expo/steps": "npm:19.0.0"
"@jest/globals": "npm:^29.7.0"
"@types/jest": "npm:^29.5.12"
"@types/node": "npm:20.14.2"
@@ -10961,11 +10961,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
dependencies:
"@expo/build-tools": "npm:18.13.1"
"@expo/eas-build-job": "npm:18.13.1"
"@expo/logger": "npm:18.5.0"
"@expo/build-tools": "npm:19.0.0"
"@expo/eas-build-job": "npm:19.0.0"
"@expo/logger": "npm:19.0.0"
"@expo/spawn-async": "npm:^1.7.2"
"@expo/turtle-spawn": "npm:18.5.0"
"@expo/turtle-spawn": "npm:19.0.0"
"@types/bunyan": "npm:^1.8.11"
"@types/fs-extra": "npm:^11.0.4"
"@types/hapi__joi": "npm:^17.1.14"
@@ -11019,11 +11019,11 @@ __metadata:
"@expo/code-signing-certificates": "npm:0.0.5"
"@expo/config": "npm:55.0.10"
"@expo/config-plugins": "npm:55.0.7"
"@expo/eas-build-job": "npm:18.13.1"
"@expo/eas-json": "npm:18.13.1"
"@expo/eas-build-job": "npm:19.0.0"
"@expo/eas-json": "npm:19.0.0"
"@expo/env": "npm:^1.0.0"
"@expo/json-file": "npm:8.3.3"
"@expo/logger": "npm:18.5.0"
"@expo/logger": "npm:19.0.0"
"@expo/multipart-body-parser": "npm:2.0.0"
"@expo/osascript": "npm:2.1.4"
"@expo/package-manager": "npm:1.9.10"
@@ -11035,7 +11035,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.13.1"
"@expo/steps": "npm:19.0.0"
"@expo/timeago.js": "npm:1.0.0"
"@graphql-codegen/cli": "npm:5.0.0"
"@graphql-codegen/introspection": "npm:4.0.0"