Compare commits

...
16 Commits
Author SHA1 Message Date
Expo CI cd47185f29 v20.5.1 2026-07-01 16:34:31 +00:00
88c51c3ccc [eas-cli] Fix Free plan users being falsely warned they hit their build limit (#3927)
The "over" usage tier fired whenever the summed value of any overageMetrics
row was greater than zero, independent of the plan limit or whether a charge
was actually incurred. For Free plans that message collapsed into the
"you've reached your limit, upgrade to keep building" warning, so free users
well below their limit (e.g. 6 builds) were told they were blocked.

Free plans can never be in an "over" state (they are blocked at the limit
and never charged pay-as-you-go), and even paid plans now require a real
billable overage cost (totalCost > 0) before showing the overage warning.

Co-authored-by: Sarah Lane <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-01 09:38:27 -06:00
Stanisław Chmiela 9247f3cf78 [eas-cli] Upgrade apple-utils to 2.1.22 (#3913)
## Summary

- [eas-cli] Upgrade `@expo/apple-utils` from `2.1.19` to `2.1.22`.
- Refresh the Yarn lockfile entry for the new package tarball.
- Add a changelog entry under the unreleased `main` section.

## Why

This picks up the latest published Apple API helpers, including recent App Store Connect pagination, SIWA, and review-related fixes from `@expo/apple-utils`.

## Validation

CI should pass.
2026-06-30 18:33:59 +02:00
Stanisław Chmiela bde331da63 [eas-cli] Fix managed tvOS env typecheck (#3920)
## Summary
- fix main typecheck after `Env` was widened to allow undefined values
- align `getManagedTvBuildSettings` with the shared `Env` type instead of requiring `Record<string, string>`

## Test plan

CI should pass.
2026-06-30 12:13:25 +00:00
Stanisław Chmiela fd055f08b3 [eas-build-job] Allow undefined env values (#3900)
## Summary

Widens the shared build `Env` type to allow `undefined` values, matching Node/process env semantics where undefined entries can represent absent values.

## Details

- Changes `Env` to `Record<string, string | undefined>` in `@expo/eas-build-job`.
- Allows `ctx.reportError` extras to contain undefined values for diagnostic metadata.
- Updates strict string boundaries to explicitly require values where needed, such as GraphQL IDs and static context API URLs.
- Filters undefined values out of `displayRuntimeInfo` string-only display maps.

## Validation

CI should pass.
2026-06-30 14:03:53 +02:00
Stanisław Chmiela 785d690e65 [build-tools] Enable simulator accessibility prefs before boot (#3914)
## Summary

- Add an opt-in `enable_accessibility_settings` boolean input to `eas/start_ios_simulator`.
- When enabled, call `IosSimulatorUtils.enableAccessibilitySettingsAsync` inline immediately before each `IosSimulatorUtils.startAsync` call, including cloned simulators.
- Reuse the existing `xcrun simctl list devices --json --no-escape-slashes available` wrapper so the `dataPath` from simctl JSON is used directly, then do an exact UDID/name equality check in JS. This avoids `simctl` search-term substring matches like `iPhone 17` matching `iPhone 17 Pro`.
- Treat invalid requested simulator state as `UserError`; wrap command/filesystem setup failures as `SystemError`.

## Why

These accessibility preferences help Argent and other AX-tree based tools inspect Simulator state more reliably. They can influence app/runtime accessibility behavior, so the setting is explicit and default-off rather than applied to every simulator startup.

## Maestro compatibility

`enable_accessibility_settings` defaults to `false`, so existing Maestro test runs and other `eas/start_ios_simulator` users keep the previous startup behavior unless they opt in. When enabled, the setup only writes preferences for an exact, available, shutdown simulator before `simctl bootstatus -b`. If the requested simulator is not available or not shutdown, the step fails with `UserError`; if the setup command/filesystem work fails, it fails with `SystemError`.

## Notes

- `plutil` is a standard macOS utility (`/usr/bin/plutil` on the local validation machine), so this does not add an external tool installation requirement.

## Validation

CI should pass.
2026-06-30 11:02:21 +02:00
Kudo Chien 47335f02f0 [build-tools] Fix repack AAB output path (#3892) 2026-06-30 14:38:06 +08:00
Douglas Lowder 75d9be813f [eas-cli] Resolve ApplePlatform.TV_OS for managed tvOS targets via EXPO_TV (#3907)
# Why

Non interactive builds were attempting to incorrectly verify tvOS provisioning profiles with settings for the iOS platform.

# How

Recognize when EXPO_TV env variable is set, and in that case, set the target platform to tvOS.

Fixes #3905

# Test Plan

- Tested with a real SDK 56 TV project
- New unit tests added
2026-06-29 18:32:47 -07:00
Expo CI cdaa011ce3 update CHANGELOG.md 2026-06-29 21:45:52 +00:00
Expo CI ee6e197587 v20.5.0 2026-06-29 21:43:22 +00:00
Gabe Debes 63baff4daf [eas-cli] Connect existing PostHog accounts via browser handoff (#3895) 2026-06-29 14:33:53 -07:00
Stanisław Chmiela 85a4054b17 [eas-cli] Use simulator session dashboard URLs (#3904)
## Why

The Expo dashboard route for device run sessions moved to the simulator sessions surface, so EAS CLI should send users to that page and use the simulator-session terminology in user-facing output.

## How

- Add a dashboard URL helper that points device run sessions at `/simulator-sessions/{id}`.
- Update `eas simulator:{start,get,list}` output and JSON to expose `deviceRunSessionUrl` using the simulator sessions route.
- Update user-facing simulator command copy from "device run session" to "simulator session".

## Test Plan

CI should pass.
2026-06-29 16:03:04 +02:00
Stanisław Chmiela 15cd0c5ae2 [worker] Disable Homebrew auto-update in EAS (#3912)
# Why

I don't think auto-updating Homebrew in CI makes sense. It creates a harder-to-reproduce / inconsistent environment.

# How

Added `HOMEBREW_NO_AUTO_UPDATE=1` to worker env.

# Test Plan

Next time https://expo.dev/accounts/gwdp/projects/expo-go-maestro/workflows/019f12db-23cd-78af-866b-3af410ccc10f runs I'll confirm `xcodes` does not update other formulas.
2026-06-29 13:39:00 +02:00
Stanisław Chmiela efc78f43d0 [ci] Update codemention action pin (#3903)
## Summary

Updates the codemention GitHub Action pin from `v1.4.0` to a reviewed post-`v1.5.2` upstream commit:

- `tobyhs/codemention@ebac5877393ab8693fd73b42708c8552043e9a3b`
- keeps the action pinned to an immutable commit SHA because this workflow runs on `pull_request_target` with `pull-requests: write`
- avoids pinning to moving `main` or a mutable tag

## Security review

I reviewed upstream `tobyhs/codemention` changes from `v1.4.0` through `v1.5.2` and found that `v1.5.2` introduced `[email protected]`, which currently has critical/high production dependency advisories. I therefore did not pin to `v1.5.2`.

The selected post-release commit includes follow-up dependency fixes on upstream `main`, including `[email protected]`, `[email protected]`, and `[email protected].0`. `npm audit --omit=dev` on that upstream commit reported 0 vulnerabilities at review time.

## Validation

- `yarn fmt:check .github/workflows/codemention.yml`
- YAML parse check
- `git diff --check`
2026-06-26 14:21:30 +02:00
a905a50f65 [eas-cli] Add tiered build-credit usage warnings (approaching/at/over) (#3882)
* [eas-cli] Add tiered build-credit usage warnings (approaching/at/over)

Lowers the warning threshold from 85% to 80% to match email
notifications. Adds two new tiers beyond the existing "approaching":
one when the user has reached their included credits, and one when
they've exceeded them on a paid plan. Free plans collapse the "over"
case into the "at" message since Free is blocked at the limit.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* [eas-cli] Add CHANGELOG entry for tiered usage warnings (#3882)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Sarah Lane <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-25 15:29:20 -06:00
Expo CI b36b5b36d7 update CHANGELOG.md 2026-06-25 21:14:47 +00:00
59 changed files with 4255 additions and 413 deletions

No files matched your search

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