Compare commits

..
7 Commits
Author SHA1 Message Date
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
25 changed files with 3593 additions and 316 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 }}
+11 -2
View File
@@ -8,14 +8,23 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 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))
### 🐛 Bug fixes
### 🧹 Chores
## [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.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/packages/eas-cli/src/commands/workflow/view.ts)_
<!-- commandsstop -->
+2413 -1
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli",
"version": "20.4.0",
"version": "20.5.0",
"description": "EAS command line tool",
"keywords": [
"cli",
+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;
}
+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'
@@ -9,6 +9,7 @@ import {
import { AccountQuery } from '../../../graphql/queries/AccountQuery';
import Log, { link } from '../../../log';
import {
classifyUsageTier,
createProgressBar,
displayOverageWarning,
maybeWarnAboutUsageOveragesAsync,
@@ -44,11 +45,15 @@ function createMockAccountUsage({
name = 'test-account',
subscriptionName = 'Free',
buildPlanMetrics = [],
overageMetrics = [],
totalCost = 0,
}: {
id?: string;
name?: string;
subscriptionName?: string;
buildPlanMetrics?: EstimatedUsage[];
overageMetrics?: { __typename: 'EstimatedOverageAndCost'; id: string; value: number }[];
totalCost?: number;
} = {}): AccountUsageForOverageWarningQuery['account']['byId'] {
return {
__typename: 'Account',
@@ -64,6 +69,8 @@ function createMockAccountUsage({
__typename: 'UsageMetricTotal',
id: 'metric-id',
planMetrics: buildPlanMetrics,
overageMetrics,
totalCost,
},
},
};
@@ -83,7 +90,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
mockDebug.mockClear();
});
it('displays a warning for a Free plan with high build usage', async () => {
it('displays an approaching-tier warning for a Free plan at 80%+ usage', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
buildPlanMetrics: [createMockPlanMetric({ value: 85 })],
@@ -102,14 +109,14 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 85% of your included build credits for this month.")
expect.stringContaining("You've used 85% of your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
);
});
it('displays a warning for a Starter plan with high build usage', async () => {
it('displays an approaching-tier warning for a paid plan at 80%+ usage', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
subscriptionName: 'Starter',
@@ -123,7 +130,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 90% of your included build credits for this month.")
expect.stringContaining("You've used 90% of your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
@@ -132,6 +139,94 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
);
});
it('displays an at-tier warning for a Free plan that has hit the limit', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
buildPlanMetrics: [createMockPlanMetric({ value: 100 })],
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've reached your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('New builds are blocked until your billing period resets.')
);
});
it('displays an at-tier warning for a paid plan that has hit the limit with no overage', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
subscriptionName: 'Starter',
buildPlanMetrics: [createMockPlanMetric({ value: 100 })],
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've reached your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('Additional builds will be charged at pay-as-you-go rates.')
);
});
it('displays the at-tier message for a Free plan even if overage is reported (defensive fallback)', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
buildPlanMetrics: [createMockPlanMetric({ value: 105 })],
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 5 }],
totalCost: 0,
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've reached your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('New builds are blocked until your billing period resets.')
);
});
it('displays an over-tier warning for a paid plan with overage and cost', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
subscriptionName: 'Starter',
buildPlanMetrics: [createMockPlanMetric({ value: 110 })],
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 10 }],
totalCost: 1500,
})
);
await maybeWarnAboutUsageOveragesAsync({
graphqlClient: mockGraphqlClient,
accountId: 'account-id',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
"You've used 10 builds beyond your included credits this billing period ($15.00 in overages so far)."
)
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('Additional builds continue at pay-as-you-go rates.')
);
});
it('does not display a warning when usage is below threshold', async () => {
mockGetUsageForOverageWarningAsync.mockResolvedValue(
createMockAccountUsage({
@@ -171,6 +266,30 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
});
});
describe('classifyUsageTier', () => {
it('returns null when usage is below threshold', () => {
expect(classifyUsageTier({ planValue: 50, limit: 100, overageCount: 0 })).toBeNull();
expect(classifyUsageTier({ planValue: 79, limit: 100, overageCount: 0 })).toBeNull();
});
it('returns "approaching" at >= 80% but below limit', () => {
expect(classifyUsageTier({ planValue: 80, limit: 100, overageCount: 0 })).toBe('approaching');
expect(classifyUsageTier({ planValue: 99, limit: 100, overageCount: 0 })).toBe('approaching');
});
it('returns "at" when planValue >= limit and no overage', () => {
expect(classifyUsageTier({ planValue: 100, limit: 100, overageCount: 0 })).toBe('at');
});
it('returns "over" when any overage has been counted', () => {
expect(classifyUsageTier({ planValue: 105, limit: 100, overageCount: 5 })).toBe('over');
});
it('prefers "over" if overage exists, regardless of planValue', () => {
expect(classifyUsageTier({ planValue: 50, limit: 100, overageCount: 1 })).toBe('over');
});
});
describe('createProgressBar', () => {
it('creates a progress bar with correct fill for a given percentage', () => {
const bar = createProgressBar(85, 20);
@@ -202,16 +321,20 @@ describe('displayOverageWarning', () => {
mockLink.mockClear();
});
it('displays a warning for a Free plan', () => {
it('approaching tier: displays a warning for a Free plan with progress bar', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: true,
tier: 'approaching',
name: 'test-account',
hasFreePlan: true,
planValue: 85,
limit: 100,
overageCount: 0,
overageCostCents: 0,
});
expect(mockWarn).toHaveBeenCalledTimes(2);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 85% of your included build credits for this month.")
expect.stringContaining("You've used 85% of your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
@@ -219,18 +342,23 @@ describe('displayOverageWarning', () => {
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('Upgrade your plan to continue service.')
);
expect(mockWarn).toHaveBeenCalledWith(expect.stringMatching(/█+░+/));
});
it('displays a warning for a paid plan', () => {
it('approaching tier: displays a warning for a paid plan with progress bar', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: false,
tier: 'approaching',
name: 'test-account',
hasFreePlan: false,
planValue: 85,
limit: 100,
overageCount: 0,
overageCostCents: 0,
});
expect(mockWarn).toHaveBeenCalledTimes(2);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 85% of your included build credits for this month.")
expect.stringContaining("You've used 85% of your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
@@ -238,16 +366,113 @@ describe('displayOverageWarning', () => {
)
);
expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('See usage in billing.'));
});
it('at tier: displays a Free-plan limit-reached message with present-tense block wording', () => {
displayOverageWarning({
tier: 'at',
name: 'test-account',
hasFreePlan: true,
planValue: 100,
limit: 100,
overageCount: 0,
overageCostCents: 0,
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('██████████████████████████░░░░')
expect.stringContaining("You've reached your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('New builds are blocked until your billing period resets.')
);
});
it('at tier: displays a paid-plan limit-reached message with pay-as-you-go warning', () => {
displayOverageWarning({
tier: 'at',
name: 'test-account',
hasFreePlan: false,
planValue: 100,
limit: 100,
overageCount: 0,
overageCostCents: 0,
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've reached your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('Additional builds will be charged at pay-as-you-go rates.')
);
});
it('over tier on Free plan: defensively falls back to the at-tier blocked message', () => {
displayOverageWarning({
tier: 'over',
name: 'test-account',
hasFreePlan: true,
planValue: 105,
limit: 100,
overageCount: 5,
overageCostCents: 0,
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've reached your included build credits this billing period.")
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('New builds are blocked until your billing period resets.')
);
});
it('over tier: displays a paid-plan overage count and accrued cost', () => {
displayOverageWarning({
tier: 'over',
name: 'test-account',
hasFreePlan: false,
planValue: 110,
limit: 100,
overageCount: 10,
overageCostCents: 1500,
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
"You've used 10 builds beyond your included credits this billing period ($15.00 in overages so far)."
)
);
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining('Additional builds continue at pay-as-you-go rates.')
);
});
it('over tier: paid plan with a single overage build uses singular wording', () => {
displayOverageWarning({
tier: 'over',
name: 'test-account',
hasFreePlan: false,
planValue: 101,
limit: 100,
overageCount: 1,
overageCostCents: 150,
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining(
"You've used 1 build beyond your included credits this billing period ($1.50 in overages so far)."
)
);
});
it('includes correct account name in billing URL', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: true,
tier: 'approaching',
name: 'my-custom-account',
hasFreePlan: true,
planValue: 85,
limit: 100,
overageCount: 0,
overageCostCents: 0,
});
expect(mockLink).toHaveBeenCalledWith(
@@ -255,27 +480,4 @@ describe('displayOverageWarning', () => {
expect.objectContaining({ text: 'Upgrade your plan to continue service.' })
);
});
it('shows progress bar when usage is under 100%', () => {
displayOverageWarning({
percentUsed: 85,
hasFreePlan: true,
name: 'test-account',
});
expect(mockWarn).toHaveBeenCalledWith(expect.stringMatching(/█+░+/));
});
it('does not show progress bar when usage is at 100%', () => {
displayOverageWarning({
percentUsed: 100,
hasFreePlan: true,
name: 'test-account',
});
expect(mockWarn).toHaveBeenCalledWith(
expect.stringContaining("You've used 100% of your included build credits for this month.")
);
expect(mockWarn).not.toHaveBeenCalledWith(expect.stringMatching(/[█░]/));
});
});
@@ -4,7 +4,9 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import { AccountQuery } from '../../graphql/queries/AccountQuery';
import Log, { link } from '../../log';
const THRESHOLD_PERCENT = 85;
const APPROACHING_THRESHOLD_PERCENT = 80;
export type UsageTier = 'approaching' | 'at' | 'over';
export async function maybeWarnAboutUsageOveragesAsync({
graphqlClient,
@@ -15,28 +17,71 @@ export async function maybeWarnAboutUsageOveragesAsync({
}): Promise<void> {
try {
const currentDate = new Date();
const {
name,
subscription,
usageMetrics: { EAS_BUILD },
} = await AccountQuery.getUsageForOverageWarningAsync(graphqlClient, accountId, currentDate);
const account = await AccountQuery.getUsageForOverageWarningAsync(
graphqlClient,
accountId,
currentDate
);
if (!account) {
return;
}
const planMetric = EAS_BUILD?.planMetrics?.[0];
const { name, subscription, usageMetrics } = account;
const buildMetrics = usageMetrics.EAS_BUILD;
const planMetric = buildMetrics?.planMetrics?.[0];
if (!planMetric || !subscription) {
return;
}
const percentUsed = calculatePercentUsed(planMetric.value, planMetric.limit);
if (percentUsed >= THRESHOLD_PERCENT) {
const hasFreePlan = subscription.name === 'Free';
displayOverageWarning({ percentUsed, hasFreePlan, name });
const overageCount = buildMetrics.overageMetrics.reduce((sum, o) => sum + o.value, 0);
const overageCostCents = buildMetrics.totalCost;
const tier = classifyUsageTier({
planValue: planMetric.value,
limit: planMetric.limit,
overageCount,
});
if (!tier) {
return;
}
const hasFreePlan = subscription.name === 'Free';
displayOverageWarning({
tier,
name,
hasFreePlan,
planValue: planMetric.value,
limit: planMetric.limit,
overageCount,
overageCostCents,
});
} catch (error) {
// Silently fail if we can't fetch usage data - we don't want to block the user's workflow
Log.debug(`Failed to fetch usage data: ${error}`);
}
}
export function classifyUsageTier({
planValue,
limit,
overageCount,
}: {
planValue: number;
limit: number;
overageCount: number;
}): UsageTier | null {
if (overageCount > 0) {
return 'over';
}
if (limit > 0 && planValue >= limit) {
return 'at';
}
const percentUsed = calculatePercentUsed(planValue, limit);
if (percentUsed >= APPROACHING_THRESHOLD_PERCENT) {
return 'approaching';
}
return null;
}
export function calculatePercentUsed(value: number, limit: number): number {
if (limit === 0) {
return 0;
@@ -52,31 +97,70 @@ export function createProgressBar(percentUsed: number, width: number = 30): stri
return `${filled}${empty}`;
}
function formatCents(cents: number): string {
return `$${(cents / 100).toFixed(2)}`;
}
export function displayOverageWarning({
percentUsed,
tier,
name,
hasFreePlan,
name,
planValue,
limit,
overageCount,
overageCostCents,
}: {
percentUsed: number;
tier: UsageTier;
name: string;
hasFreePlan: boolean;
name: string;
planValue: number;
limit: number;
overageCount: number;
overageCostCents: number;
}): void {
const message = chalk.bold(
`You've used ${percentUsed}% of your included build credits for this month.`
);
// Don't show progress bar at 100% - it's redundant when the limit is reached
const progressBar = percentUsed < 100 ? ' ' + createProgressBar(percentUsed) : '';
Log.warn(message + progressBar);
const billingUrl = `https://expo.dev/accounts/${name}/settings/billing`;
if (tier === 'approaching') {
const percentUsed = calculatePercentUsed(planValue, limit);
Log.warn(
chalk.bold(
`You've used ${percentUsed}% of your included build credits this billing period.`
) +
' ' +
createProgressBar(percentUsed)
);
Log.warn(
hasFreePlan
? "You won't be able to start new builds once you reach the limit. " +
link(billingUrl, { text: 'Upgrade your plan to continue service.', dim: false })
: 'Additional usage beyond your limit will be charged at pay-as-you-go rates. ' +
link(billingUrl, { text: 'See usage in billing.', dim: false })
);
return;
}
const billingUrl = `https://expo.dev/accounts/${name}/settings/billing`;
const warning = hasFreePlan
? "You won't be able to start new builds once you reach the limit. " +
link(billingUrl, { text: 'Upgrade your plan to continue service.', dim: false })
: 'Additional usage beyond your limit will be charged at pay-as-you-go rates. ' +
link(billingUrl, {
text: 'See usage in billing.',
dim: false,
});
// Free users are blocked at the limit, so they can't reach an "over" state.
// If we ever see Free + over (data inconsistency, mid-flight plan change), treat it as "at".
if (tier === 'at' || (tier === 'over' && hasFreePlan)) {
Log.warn(chalk.bold("You've reached your included build credits this billing period."));
Log.warn(
hasFreePlan
? 'New builds are blocked until your billing period resets. ' +
link(billingUrl, { text: 'Upgrade your plan to continue building.', dim: false })
: 'Additional builds will be charged at pay-as-you-go rates. ' +
link(billingUrl, { text: 'See usage in billing.', dim: false })
);
return;
}
Log.warn(warning);
// tier === 'over' && paid plan
Log.warn(
chalk.bold(
`You've used ${overageCount} build${overageCount === 1 ? '' : 's'} beyond your included credits this billing period (${formatCents(overageCostCents)} in overages so far).`
)
);
Log.warn(
'Additional builds continue at pay-as-you-go rates. ' +
link(billingUrl, { text: 'See usage in billing.', dim: false })
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/worker",
"version": "20.4.0",
"version": "20.5.0",
"private": true,
"scripts": {
"start": "yarn watch",
+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);