mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
079aa5dc66 | ||
|
|
fa8980759f | ||
|
|
790085ea9e | ||
|
|
9c7ac49f3e | ||
|
|
fa244d6410 | ||
|
|
db8a3770f8 | ||
|
|
be16b0032f | ||
|
|
3ef14b5eb7 | ||
|
|
c777b25e92 | ||
|
|
15b690c857 | ||
|
|
4d04463714 |
No files matched your search
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
INPUT_STAGING_ONLY: ${{ github.event.inputs.staging_only }}
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
echo //registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
- name: Add latest-eas-build tag
|
||||
run: |
|
||||
@@ -60,5 +60,5 @@ jobs:
|
||||
echo "Only moving the staging tag, skipping the production tag"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
echo //registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
@@ -176,12 +176,18 @@ jobs:
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
- name: Add latest-eas-build-staging tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
|
||||
- name: Add latest-eas-build tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
|
||||
@@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Hidden beta version of new `eas account:usage` command for viewing account usage and billing estimates. ([#3334](https://github.com/expo/eas-cli/pull/3334) by [@douglowder](https://github.com/douglowder), [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `--no-bytecode` and `--source-maps [mode]` flags to `eas update`. ([#3339](https://github.com/expo/eas-cli/pull/3339) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add App Clip bundle identifier registration support for multi-target iOS builds. ([#3300](https://github.com/expo/eas-cli/pull/3300) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Add `--runtime-version` and `--platform` filters to `eas update:list`. ([#3261](https://github.com/expo/eas-cli/pull/3261) by [@HarelSultan](https://github.com/HarelSultan))
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "16.30.0",
|
||||
"version": "16.32.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+110
-87
@@ -57,6 +57,7 @@ eas --help COMMAND
|
||||
<!-- commands -->
|
||||
* [`eas account:login`](#eas-accountlogin)
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:usage [ACCOUNT_NAME]`](#eas-accountusage-account_name)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
|
||||
@@ -176,7 +177,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -193,7 +194,29 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
view account usage and billing for the current cycle
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas account:usage [ACCOUNT_NAME] [--json] [--non-interactive]
|
||||
|
||||
ARGUMENTS
|
||||
ACCOUNT_NAME Account name to view usage for. If not provided, the account will be selected interactively (or defaults
|
||||
to the only account if there is just one)
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
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/v16.32.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -210,7 +233,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -224,7 +247,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.32.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -276,7 +299,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -297,7 +320,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -317,7 +340,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -337,7 +360,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -360,7 +383,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -401,7 +424,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -420,7 +443,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -437,7 +460,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -456,7 +479,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -476,7 +499,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -497,7 +520,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -532,7 +555,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -584,7 +607,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -613,7 +636,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -639,7 +662,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -693,7 +716,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -712,7 +735,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -731,7 +754,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -748,7 +771,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -769,7 +792,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -790,7 +813,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -812,7 +835,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -832,7 +855,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -854,7 +877,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -876,7 +899,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -909,7 +932,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -932,7 +955,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -953,7 +976,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/v16.30.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.32.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -970,7 +993,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -988,7 +1011,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1016,7 +1039,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1041,7 +1064,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1062,7 +1085,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1083,7 +1106,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1120,7 +1143,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1140,7 +1163,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1161,7 +1184,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1182,7 +1205,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1196,7 +1219,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1210,7 +1233,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.32.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1240,7 +1263,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1265,7 +1288,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1287,7 +1310,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1312,7 +1335,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1338,7 +1361,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1360,7 +1383,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1382,7 +1405,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1414,7 +1437,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1458,7 +1481,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1488,7 +1511,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1596,7 +1619,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1613,7 +1636,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1630,7 +1653,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1681,7 +1704,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/v16.30.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.32.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1695,7 +1718,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1718,7 +1741,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1741,7 +1764,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1759,7 +1782,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1794,7 +1817,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.32.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1834,7 +1857,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1854,7 +1877,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1875,7 +1898,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1899,7 +1922,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1924,7 +1947,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1960,7 +1983,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -1987,7 +2010,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2015,7 +2038,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2036,7 +2059,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2056,7 +2079,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2077,7 +2100,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/v16.30.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v16.32.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2098,7 +2121,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2118,7 +2141,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2136,7 +2159,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2158,7 +2181,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2175,7 +2198,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2295,7 +2318,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2315,7 +2338,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2339,7 +2362,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run [FILE]`
|
||||
|
||||
@@ -2379,7 +2402,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2404,7 +2427,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2432,7 +2455,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2452,7 +2475,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2474,5 +2497,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/v16.30.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/v16.32.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+5436
-353
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "16.30.0",
|
||||
"version": "16.32.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { EasService, EasServiceMetric, UsageMetricType } from '../../graphql/generated';
|
||||
import { AccountFullUsageData } from '../../graphql/queries/AccountQuery';
|
||||
import {
|
||||
calculateBillingPeriodDays,
|
||||
calculateDaysElapsed,
|
||||
calculateDaysRemaining,
|
||||
extractUsageData,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
} from '../usageUtils';
|
||||
|
||||
function createMockFullUsageData(
|
||||
overrides: Partial<{
|
||||
name: string;
|
||||
subscriptionName: string;
|
||||
buildValue: number;
|
||||
buildLimit: number;
|
||||
mauValue: number;
|
||||
mauLimit: number;
|
||||
bandwidthValue: number;
|
||||
bandwidthLimit: number;
|
||||
buildOverageCost: number;
|
||||
updateOverageCost: number;
|
||||
}> = {}
|
||||
): AccountFullUsageData {
|
||||
const {
|
||||
name = 'test-account',
|
||||
subscriptionName = 'Starter',
|
||||
buildValue = 10,
|
||||
buildLimit = 50,
|
||||
mauValue = 500,
|
||||
mauLimit = 3000,
|
||||
bandwidthValue = 2.5,
|
||||
bandwidthLimit = 10,
|
||||
buildOverageCost = 0,
|
||||
updateOverageCost = 0,
|
||||
} = overrides;
|
||||
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
return {
|
||||
__typename: 'Account',
|
||||
id: 'account-id',
|
||||
name,
|
||||
subscription: {
|
||||
__typename: 'SubscriptionDetails',
|
||||
id: 'sub-id',
|
||||
name: subscriptionName,
|
||||
status: 'active',
|
||||
nextInvoice: endOfMonth.toISOString(),
|
||||
nextInvoiceAmountDueCents: 1900,
|
||||
recurringCents: 1900,
|
||||
price: 1900,
|
||||
addons: [],
|
||||
},
|
||||
billingPeriod: {
|
||||
__typename: 'BillingPeriod',
|
||||
id: 'billing-period-id',
|
||||
start: startOfMonth.toISOString(),
|
||||
end: endOfMonth.toISOString(),
|
||||
anchor: startOfMonth.toISOString(),
|
||||
},
|
||||
usageMetrics: {
|
||||
__typename: 'AccountUsageMetrics',
|
||||
MEDIUM_ANDROID_BUILDS: [],
|
||||
LARGE_ANDROID_BUILDS: [],
|
||||
MEDIUM_IOS_BUILDS: [],
|
||||
LARGE_IOS_BUILDS: [],
|
||||
EAS_BUILD: {
|
||||
__typename: 'UsageMetricTotal',
|
||||
id: 'build-metric-id',
|
||||
billingPeriod: {
|
||||
__typename: 'BillingPeriod',
|
||||
id: 'billing-period-id',
|
||||
start: startOfMonth.toISOString(),
|
||||
end: endOfMonth.toISOString(),
|
||||
anchor: startOfMonth.toISOString(),
|
||||
},
|
||||
planMetrics: [
|
||||
{
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'build-plan-metric-id',
|
||||
service: EasService.Builds,
|
||||
serviceMetric: EasServiceMetric.Builds,
|
||||
metricType: UsageMetricType.Build,
|
||||
value: buildValue,
|
||||
limit: buildLimit,
|
||||
platformBreakdown: {
|
||||
__typename: 'EstimatedUsagePlatformBreakdown',
|
||||
ios: {
|
||||
__typename: 'EstimatedUsagePlatformDetail',
|
||||
value: Math.floor(buildValue * 0.6),
|
||||
limit: Math.floor(buildLimit * 0.6),
|
||||
},
|
||||
android: {
|
||||
__typename: 'EstimatedUsagePlatformDetail',
|
||||
value: Math.floor(buildValue * 0.4),
|
||||
limit: Math.floor(buildLimit * 0.4),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
overageMetrics:
|
||||
buildOverageCost > 0
|
||||
? [
|
||||
{
|
||||
__typename: 'EstimatedOverageAndCost',
|
||||
id: 'build-overage-id',
|
||||
service: EasService.Builds,
|
||||
serviceMetric: EasServiceMetric.Builds,
|
||||
metricType: UsageMetricType.Build,
|
||||
value: buildValue - buildLimit,
|
||||
limit: buildLimit,
|
||||
totalCost: buildOverageCost,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
totalCost: buildOverageCost,
|
||||
},
|
||||
EAS_UPDATE: {
|
||||
__typename: 'UsageMetricTotal',
|
||||
id: 'update-metric-id',
|
||||
billingPeriod: {
|
||||
__typename: 'BillingPeriod',
|
||||
id: 'billing-period-id',
|
||||
start: startOfMonth.toISOString(),
|
||||
end: endOfMonth.toISOString(),
|
||||
anchor: startOfMonth.toISOString(),
|
||||
},
|
||||
planMetrics: [
|
||||
{
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'mau-plan-metric-id',
|
||||
service: EasService.Updates,
|
||||
serviceMetric: EasServiceMetric.UniqueUpdaters,
|
||||
metricType: UsageMetricType.Update,
|
||||
value: mauValue,
|
||||
limit: mauLimit,
|
||||
},
|
||||
{
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'bandwidth-plan-metric-id',
|
||||
service: EasService.Updates,
|
||||
serviceMetric: EasServiceMetric.BandwidthUsage,
|
||||
metricType: UsageMetricType.Bandwidth,
|
||||
value: bandwidthValue,
|
||||
limit: bandwidthLimit,
|
||||
},
|
||||
],
|
||||
overageMetrics:
|
||||
updateOverageCost > 0
|
||||
? [
|
||||
{
|
||||
__typename: 'EstimatedOverageAndCost',
|
||||
id: 'mau-overage-id',
|
||||
service: EasService.Updates,
|
||||
serviceMetric: EasServiceMetric.UniqueUpdaters,
|
||||
metricType: UsageMetricType.Update,
|
||||
value: mauValue - mauLimit,
|
||||
limit: mauLimit,
|
||||
totalCost: updateOverageCost,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
totalCost: updateOverageCost,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('formats date correctly', () => {
|
||||
const result = formatDate('2024-03-15T00:00:00.000Z');
|
||||
expect(result).toMatch(/Mar 15, 2024|Mar 14, 2024/); // Allow for timezone differences
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCurrency', () => {
|
||||
it('formats cents to dollars', () => {
|
||||
expect(formatCurrency(1900)).toBe('$19.00');
|
||||
expect(formatCurrency(0)).toBe('$0.00');
|
||||
expect(formatCurrency(150)).toBe('$1.50');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatNumber', () => {
|
||||
it('formats numbers with no decimals by default', () => {
|
||||
expect(formatNumber(1000)).toBe('1,000');
|
||||
expect(formatNumber(1234567)).toBe('1,234,567');
|
||||
});
|
||||
|
||||
it('formats numbers with specified decimals', () => {
|
||||
expect(formatNumber(1234.5678, 2)).toBe('1,234.57');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDaysRemaining', () => {
|
||||
it('calculates days remaining correctly', () => {
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + 10);
|
||||
const result = calculateDaysRemaining(futureDate.toISOString());
|
||||
expect(result).toBeGreaterThanOrEqual(9);
|
||||
expect(result).toBeLessThanOrEqual(11);
|
||||
});
|
||||
|
||||
it('returns 0 for past dates', () => {
|
||||
const pastDate = new Date();
|
||||
pastDate.setDate(pastDate.getDate() - 10);
|
||||
const result = calculateDaysRemaining(pastDate.toISOString());
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDaysElapsed', () => {
|
||||
it('calculates days elapsed correctly', () => {
|
||||
const pastDate = new Date();
|
||||
pastDate.setDate(pastDate.getDate() - 10);
|
||||
const result = calculateDaysElapsed(pastDate.toISOString());
|
||||
expect(result).toBeGreaterThanOrEqual(10);
|
||||
expect(result).toBeLessThanOrEqual(11);
|
||||
});
|
||||
|
||||
it('returns at least 1 for recent dates', () => {
|
||||
const today = new Date();
|
||||
const result = calculateDaysElapsed(today.toISOString());
|
||||
expect(result).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateBillingPeriodDays', () => {
|
||||
it('calculates billing period days correctly', () => {
|
||||
const start = '2024-03-01T00:00:00.000Z';
|
||||
const end = '2024-03-31T00:00:00.000Z';
|
||||
const result = calculateBillingPeriodDays(start, end);
|
||||
expect(result).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractUsageData', () => {
|
||||
it('correctly extracts usage data from API response', () => {
|
||||
const mockData = createMockFullUsageData({
|
||||
buildValue: 25,
|
||||
buildLimit: 50,
|
||||
mauValue: 1000,
|
||||
mauLimit: 3000,
|
||||
});
|
||||
|
||||
const result = extractUsageData(mockData);
|
||||
|
||||
expect(result.accountName).toBe('test-account');
|
||||
expect(result.subscriptionPlan).toBe('Starter');
|
||||
expect(result.builds.total.planValue).toBe(25);
|
||||
expect(result.builds.total.limit).toBe(50);
|
||||
expect(result.updates.mau.planValue).toBe(1000);
|
||||
expect(result.updates.mau.limit).toBe(3000);
|
||||
});
|
||||
|
||||
it('handles overage costs', () => {
|
||||
const mockData = createMockFullUsageData({
|
||||
buildValue: 60,
|
||||
buildLimit: 50,
|
||||
buildOverageCost: 2000, // $20 in cents
|
||||
});
|
||||
|
||||
const result = extractUsageData(mockData);
|
||||
|
||||
expect(result.builds.overageCostCents).toBe(2000);
|
||||
expect(result.totalOverageCostCents).toBe(2000);
|
||||
});
|
||||
|
||||
it('handles Free plan', () => {
|
||||
const mockData = createMockFullUsageData({
|
||||
subscriptionName: 'Free',
|
||||
});
|
||||
|
||||
const result = extractUsageData(mockData);
|
||||
|
||||
expect(result.subscriptionPlan).toBe('Free');
|
||||
});
|
||||
|
||||
it('calculates percent used correctly', () => {
|
||||
const mockData = createMockFullUsageData({
|
||||
buildValue: 25,
|
||||
buildLimit: 50,
|
||||
});
|
||||
|
||||
const result = extractUsageData(mockData);
|
||||
|
||||
expect(result.builds.total.percentUsed).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Billing period calculations', () => {
|
||||
it('correctly identifies billing period dates', () => {
|
||||
const mockData = createMockFullUsageData();
|
||||
const result = extractUsageData(mockData);
|
||||
|
||||
const startDate = new Date(result.billingPeriod.start);
|
||||
const endDate = new Date(result.billingPeriod.end);
|
||||
|
||||
expect(startDate).toBeInstanceOf(Date);
|
||||
expect(endDate).toBeInstanceOf(Date);
|
||||
expect(endDate.getTime()).toBeGreaterThan(startDate.getTime());
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,12 @@
|
||||
import {
|
||||
AnyVariables,
|
||||
Client,
|
||||
OperationContext,
|
||||
OperationResult,
|
||||
OperationResultSource,
|
||||
TypedDocumentNode,
|
||||
cacheExchange,
|
||||
createClient as createUrqlClient,
|
||||
fetchExchange,
|
||||
} from '@urql/core';
|
||||
import { Client, cacheExchange, createClient as createUrqlClient, fetchExchange } from '@urql/core';
|
||||
import { retryExchange } from '@urql/exchange-retry';
|
||||
import { DocumentNode } from 'graphql';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { getExpoApiBaseUrl } from '../../../api';
|
||||
import { httpsProxyAgent } from '../../../fetch';
|
||||
|
||||
export interface ExpoGraphqlClient extends Client {
|
||||
query<Data = any, Variables extends AnyVariables = AnyVariables>(
|
||||
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
|
||||
variables: Variables,
|
||||
context: Partial<OperationContext> & { additionalTypenames: string[] }
|
||||
): OperationResultSource<OperationResult<Data, Variables>>;
|
||||
}
|
||||
// Extend the urql Client to allow for future customizations or type additions ahead of upstream type support.
|
||||
export interface ExpoGraphqlClient extends Client {}
|
||||
|
||||
export function createGraphqlClient(authInfo: {
|
||||
accessToken: string | null;
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import {
|
||||
AppPlatform,
|
||||
EasBuildBillingResourceClass,
|
||||
EasServiceMetric,
|
||||
UsageMetricType,
|
||||
} from '../graphql/generated';
|
||||
import { AccountFullUsageData } from '../graphql/queries/AccountQuery';
|
||||
import { calculatePercentUsed } from '../utils/usage/checkForOverages';
|
||||
|
||||
export interface UsageMetricDisplay {
|
||||
name: string;
|
||||
planValue: number;
|
||||
limit: number;
|
||||
percentUsed: number;
|
||||
overageValue: number;
|
||||
overageCost: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface BuildOverageByWorkerSize {
|
||||
platform: AppPlatform;
|
||||
resourceClass: EasBuildBillingResourceClass;
|
||||
count: number;
|
||||
costCents: number;
|
||||
}
|
||||
|
||||
export interface BuildCountByPlatformAndSize {
|
||||
platform: 'ios' | 'android';
|
||||
resourceClass: 'medium' | 'large';
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UsageDisplayData {
|
||||
accountName: string;
|
||||
subscriptionPlan: string;
|
||||
billingPeriod: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
builds: {
|
||||
total: UsageMetricDisplay;
|
||||
ios?: UsageMetricDisplay;
|
||||
android?: UsageMetricDisplay;
|
||||
countsByPlatformAndSize: BuildCountByPlatformAndSize[];
|
||||
overagesByWorkerSize: BuildOverageByWorkerSize[];
|
||||
overageCostCents: number;
|
||||
};
|
||||
updates: {
|
||||
mau: UsageMetricDisplay;
|
||||
bandwidth: UsageMetricDisplay;
|
||||
overageCostCents: number;
|
||||
};
|
||||
totalOverageCostCents: number;
|
||||
estimatedBillCents: number;
|
||||
recurringCents: number | null;
|
||||
}
|
||||
|
||||
export function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCurrency(cents: number): string {
|
||||
return `$${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
export function formatNumber(value: number, decimals: number = 0): string {
|
||||
if (decimals === 0) {
|
||||
return Math.round(value).toLocaleString();
|
||||
}
|
||||
return value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
});
|
||||
}
|
||||
|
||||
export function calculateDaysRemaining(endDate: string): number {
|
||||
const end = new Date(endDate);
|
||||
const now = new Date();
|
||||
const diffTime = end.getTime() - now.getTime();
|
||||
return Math.max(0, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
export function calculateDaysElapsed(startDate: string): number {
|
||||
const start = new Date(startDate);
|
||||
const now = new Date();
|
||||
const diffTime = now.getTime() - start.getTime();
|
||||
return Math.max(1, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
export function calculateBillingPeriodDays(startDate: string, endDate: string): number {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const diffTime = end.getTime() - start.getTime();
|
||||
return Math.max(1, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
export function extractUsageData(data: AccountFullUsageData): UsageDisplayData {
|
||||
const { name, subscription, billingPeriod, usageMetrics } = data;
|
||||
const {
|
||||
EAS_BUILD,
|
||||
EAS_UPDATE,
|
||||
MEDIUM_ANDROID_BUILDS,
|
||||
LARGE_ANDROID_BUILDS,
|
||||
MEDIUM_IOS_BUILDS,
|
||||
LARGE_IOS_BUILDS,
|
||||
} = usageMetrics;
|
||||
|
||||
// Find build metrics
|
||||
const buildMetric = EAS_BUILD.planMetrics.find(
|
||||
m => m.serviceMetric === EasServiceMetric.Builds && m.metricType === UsageMetricType.Build
|
||||
);
|
||||
|
||||
// Extract build counts by platform and worker size
|
||||
const countsByPlatformAndSize: BuildCountByPlatformAndSize[] = [];
|
||||
const mediumAndroidCount = MEDIUM_ANDROID_BUILDS?.[0]?.value ?? 0;
|
||||
const largeAndroidCount = LARGE_ANDROID_BUILDS?.[0]?.value ?? 0;
|
||||
const mediumIosCount = MEDIUM_IOS_BUILDS?.[0]?.value ?? 0;
|
||||
const largeIosCount = LARGE_IOS_BUILDS?.[0]?.value ?? 0;
|
||||
|
||||
if (mediumAndroidCount > 0) {
|
||||
countsByPlatformAndSize.push({
|
||||
platform: 'android',
|
||||
resourceClass: 'medium',
|
||||
count: mediumAndroidCount,
|
||||
});
|
||||
}
|
||||
if (largeAndroidCount > 0) {
|
||||
countsByPlatformAndSize.push({
|
||||
platform: 'android',
|
||||
resourceClass: 'large',
|
||||
count: largeAndroidCount,
|
||||
});
|
||||
}
|
||||
if (mediumIosCount > 0) {
|
||||
countsByPlatformAndSize.push({
|
||||
platform: 'ios',
|
||||
resourceClass: 'medium',
|
||||
count: mediumIosCount,
|
||||
});
|
||||
}
|
||||
if (largeIosCount > 0) {
|
||||
countsByPlatformAndSize.push({ platform: 'ios', resourceClass: 'large', count: largeIosCount });
|
||||
}
|
||||
|
||||
// Extract build overages by worker size from overage metrics with metadata
|
||||
const overagesByWorkerSize: BuildOverageByWorkerSize[] = [];
|
||||
for (const overage of EAS_BUILD.overageMetrics) {
|
||||
if (
|
||||
overage.serviceMetric === EasServiceMetric.Builds &&
|
||||
overage.metadata?.billingResourceClass &&
|
||||
overage.metadata?.platform
|
||||
) {
|
||||
overagesByWorkerSize.push({
|
||||
platform: overage.metadata.platform,
|
||||
resourceClass: overage.metadata.billingResourceClass,
|
||||
count: overage.value,
|
||||
costCents: overage.totalCost,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find update metrics
|
||||
const mauMetric = EAS_UPDATE.planMetrics.find(
|
||||
m =>
|
||||
m.serviceMetric === EasServiceMetric.UniqueUpdaters && m.metricType === UsageMetricType.Update
|
||||
);
|
||||
const bandwidthMetric = EAS_UPDATE.planMetrics.find(
|
||||
m =>
|
||||
m.serviceMetric === EasServiceMetric.BandwidthUsage &&
|
||||
m.metricType === UsageMetricType.Bandwidth
|
||||
);
|
||||
const mauOverage = EAS_UPDATE.overageMetrics.find(
|
||||
m =>
|
||||
m.serviceMetric === EasServiceMetric.UniqueUpdaters && m.metricType === UsageMetricType.Update
|
||||
);
|
||||
const bandwidthOverage = EAS_UPDATE.overageMetrics.find(
|
||||
m =>
|
||||
m.serviceMetric === EasServiceMetric.BandwidthUsage &&
|
||||
m.metricType === UsageMetricType.Bandwidth
|
||||
);
|
||||
|
||||
// Build metrics - plan values from planMetrics, overage from overageMetrics
|
||||
const buildPlanValue = buildMetric?.value ?? 0;
|
||||
const buildLimit = buildMetric?.limit ?? 0;
|
||||
const iosBuildPlanValue = buildMetric?.platformBreakdown?.ios.value ?? 0;
|
||||
const iosBuildLimit = buildMetric?.platformBreakdown?.ios.limit ?? 0;
|
||||
const androidBuildPlanValue = buildMetric?.platformBreakdown?.android.value ?? 0;
|
||||
const androidBuildLimit = buildMetric?.platformBreakdown?.android.limit ?? 0;
|
||||
|
||||
// Update metrics - plan values from planMetrics, overage from overageMetrics
|
||||
const mauPlanValue = mauMetric?.value ?? 0;
|
||||
const mauOverageValue = mauOverage?.value ?? 0;
|
||||
const mauLimit = mauMetric?.limit ?? 0;
|
||||
|
||||
const bandwidthPlanValue = bandwidthMetric?.value ?? 0;
|
||||
const bandwidthOverageValue = bandwidthOverage?.value ?? 0;
|
||||
const bandwidthLimit = bandwidthMetric?.limit ?? 0;
|
||||
|
||||
const buildOverageCostCents = EAS_BUILD.totalCost;
|
||||
const updateOverageCostCents = EAS_UPDATE.totalCost;
|
||||
const totalOverageCostCents = buildOverageCostCents + updateOverageCostCents;
|
||||
|
||||
// Calculate total overage count for display
|
||||
const totalOverageBuilds = overagesByWorkerSize.reduce((sum, o) => sum + o.count, 0);
|
||||
|
||||
return {
|
||||
accountName: name,
|
||||
subscriptionPlan: subscription?.name ?? 'Free',
|
||||
billingPeriod: {
|
||||
start: billingPeriod.start,
|
||||
end: billingPeriod.end,
|
||||
},
|
||||
builds: {
|
||||
total: {
|
||||
name: 'Builds',
|
||||
planValue: buildPlanValue,
|
||||
limit: buildLimit,
|
||||
percentUsed: calculatePercentUsed(buildPlanValue, buildLimit),
|
||||
overageValue: totalOverageBuilds,
|
||||
overageCost: buildOverageCostCents,
|
||||
unit: 'builds',
|
||||
},
|
||||
ios: buildMetric?.platformBreakdown
|
||||
? {
|
||||
name: 'iOS Builds',
|
||||
planValue: iosBuildPlanValue,
|
||||
limit: iosBuildLimit,
|
||||
percentUsed: calculatePercentUsed(iosBuildPlanValue, iosBuildLimit),
|
||||
overageValue: 0,
|
||||
overageCost: 0,
|
||||
unit: 'builds',
|
||||
}
|
||||
: undefined,
|
||||
android: buildMetric?.platformBreakdown
|
||||
? {
|
||||
name: 'Android Builds',
|
||||
planValue: androidBuildPlanValue,
|
||||
limit: androidBuildLimit,
|
||||
percentUsed: calculatePercentUsed(androidBuildPlanValue, androidBuildLimit),
|
||||
overageValue: 0,
|
||||
overageCost: 0,
|
||||
unit: 'builds',
|
||||
}
|
||||
: undefined,
|
||||
countsByPlatformAndSize,
|
||||
overagesByWorkerSize,
|
||||
overageCostCents: buildOverageCostCents,
|
||||
},
|
||||
updates: {
|
||||
mau: {
|
||||
name: 'Unique Updaters',
|
||||
planValue: mauPlanValue,
|
||||
limit: mauLimit,
|
||||
percentUsed: calculatePercentUsed(mauPlanValue, mauLimit),
|
||||
overageValue: mauOverageValue,
|
||||
overageCost: mauOverage?.totalCost ?? 0,
|
||||
unit: 'users',
|
||||
},
|
||||
bandwidth: {
|
||||
name: 'Bandwidth',
|
||||
planValue: bandwidthPlanValue,
|
||||
limit: bandwidthLimit,
|
||||
percentUsed: calculatePercentUsed(bandwidthPlanValue, bandwidthLimit),
|
||||
overageValue: bandwidthOverageValue,
|
||||
overageCost: bandwidthOverage?.totalCost ?? 0,
|
||||
unit: 'bytes',
|
||||
},
|
||||
overageCostCents: updateOverageCostCents,
|
||||
},
|
||||
totalOverageCostCents,
|
||||
estimatedBillCents: (subscription?.recurringCents ?? 0) + totalOverageCostCents,
|
||||
recurringCents: subscription?.recurringCents ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateBillingPeriodInfo(data: UsageDisplayData): {
|
||||
daysRemaining: number;
|
||||
daysElapsed: number;
|
||||
totalDays: number;
|
||||
} {
|
||||
const daysElapsed = calculateDaysElapsed(data.billingPeriod.start);
|
||||
const totalDays = calculateBillingPeriodDays(data.billingPeriod.start, data.billingPeriod.end);
|
||||
const daysRemaining = calculateDaysRemaining(data.billingPeriod.end);
|
||||
|
||||
return {
|
||||
daysRemaining,
|
||||
daysElapsed,
|
||||
totalDays,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AccountFullUsageQuery as AccountFullUsageQueryType,
|
||||
EasService,
|
||||
EasServiceMetric,
|
||||
UsageMetricType,
|
||||
} from '../../../graphql/generated';
|
||||
import { AccountQuery } from '../../../graphql/queries/AccountQuery';
|
||||
import { calculatePercentUsed, createProgressBar } from '../../../utils/usage/checkForOverages';
|
||||
|
||||
jest.mock('../../../graphql/queries/AccountQuery');
|
||||
|
||||
function createMockFullUsageData(
|
||||
overrides: Partial<{
|
||||
name: string;
|
||||
subscriptionName: string;
|
||||
buildValue: number;
|
||||
buildLimit: number;
|
||||
mauValue: number;
|
||||
mauLimit: number;
|
||||
bandwidthValue: number;
|
||||
bandwidthLimit: number;
|
||||
buildOverageCost: number;
|
||||
updateOverageCost: number;
|
||||
}> = {}
|
||||
): AccountFullUsageQueryType['account']['byId'] {
|
||||
const {
|
||||
name = 'test-account',
|
||||
subscriptionName = 'Starter',
|
||||
buildValue = 10,
|
||||
buildLimit = 50,
|
||||
mauValue = 500,
|
||||
mauLimit = 3000,
|
||||
bandwidthValue = 2.5,
|
||||
bandwidthLimit = 10,
|
||||
buildOverageCost = 0,
|
||||
updateOverageCost = 0,
|
||||
} = overrides;
|
||||
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
return {
|
||||
__typename: 'Account',
|
||||
id: 'account-id',
|
||||
name,
|
||||
subscription: {
|
||||
__typename: 'SubscriptionDetails',
|
||||
id: 'sub-id',
|
||||
name: subscriptionName,
|
||||
status: 'active',
|
||||
nextInvoice: endOfMonth.toISOString(),
|
||||
nextInvoiceAmountDueCents: 1900,
|
||||
recurringCents: 1900,
|
||||
price: 1900,
|
||||
addons: [],
|
||||
},
|
||||
billingPeriod: {
|
||||
__typename: 'BillingPeriod',
|
||||
id: 'billing-period-id',
|
||||
start: startOfMonth.toISOString(),
|
||||
end: endOfMonth.toISOString(),
|
||||
anchor: startOfMonth.toISOString(),
|
||||
},
|
||||
usageMetrics: {
|
||||
__typename: 'AccountUsageMetrics',
|
||||
MEDIUM_ANDROID_BUILDS: [],
|
||||
LARGE_ANDROID_BUILDS: [],
|
||||
MEDIUM_IOS_BUILDS: [],
|
||||
LARGE_IOS_BUILDS: [],
|
||||
EAS_BUILD: {
|
||||
__typename: 'UsageMetricTotal',
|
||||
id: 'build-metric-id',
|
||||
billingPeriod: {
|
||||
__typename: 'BillingPeriod',
|
||||
id: 'billing-period-id',
|
||||
start: startOfMonth.toISOString(),
|
||||
end: endOfMonth.toISOString(),
|
||||
anchor: startOfMonth.toISOString(),
|
||||
},
|
||||
planMetrics: [
|
||||
{
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'build-plan-metric-id',
|
||||
service: EasService.Builds,
|
||||
serviceMetric: EasServiceMetric.Builds,
|
||||
metricType: UsageMetricType.Build,
|
||||
value: buildValue,
|
||||
limit: buildLimit,
|
||||
platformBreakdown: {
|
||||
__typename: 'EstimatedUsagePlatformBreakdown',
|
||||
ios: {
|
||||
__typename: 'EstimatedUsagePlatformDetail',
|
||||
value: Math.floor(buildValue * 0.6),
|
||||
limit: Math.floor(buildLimit * 0.6),
|
||||
},
|
||||
android: {
|
||||
__typename: 'EstimatedUsagePlatformDetail',
|
||||
value: Math.floor(buildValue * 0.4),
|
||||
limit: Math.floor(buildLimit * 0.4),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
overageMetrics:
|
||||
buildOverageCost > 0
|
||||
? [
|
||||
{
|
||||
__typename: 'EstimatedOverageAndCost',
|
||||
id: 'build-overage-id',
|
||||
service: EasService.Builds,
|
||||
serviceMetric: EasServiceMetric.Builds,
|
||||
metricType: UsageMetricType.Build,
|
||||
value: buildValue - buildLimit,
|
||||
limit: buildLimit,
|
||||
totalCost: buildOverageCost,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
totalCost: buildOverageCost,
|
||||
},
|
||||
EAS_UPDATE: {
|
||||
__typename: 'UsageMetricTotal',
|
||||
id: 'update-metric-id',
|
||||
billingPeriod: {
|
||||
__typename: 'BillingPeriod',
|
||||
id: 'billing-period-id',
|
||||
start: startOfMonth.toISOString(),
|
||||
end: endOfMonth.toISOString(),
|
||||
anchor: startOfMonth.toISOString(),
|
||||
},
|
||||
planMetrics: [
|
||||
{
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'mau-plan-metric-id',
|
||||
service: EasService.Updates,
|
||||
serviceMetric: EasServiceMetric.UniqueUpdaters,
|
||||
metricType: UsageMetricType.Update,
|
||||
value: mauValue,
|
||||
limit: mauLimit,
|
||||
},
|
||||
{
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'bandwidth-plan-metric-id',
|
||||
service: EasService.Updates,
|
||||
serviceMetric: EasServiceMetric.BandwidthUsage,
|
||||
metricType: UsageMetricType.Bandwidth,
|
||||
value: bandwidthValue,
|
||||
limit: bandwidthLimit,
|
||||
},
|
||||
],
|
||||
overageMetrics:
|
||||
updateOverageCost > 0
|
||||
? [
|
||||
{
|
||||
__typename: 'EstimatedOverageAndCost',
|
||||
id: 'mau-overage-id',
|
||||
service: EasService.Updates,
|
||||
serviceMetric: EasServiceMetric.UniqueUpdaters,
|
||||
metricType: UsageMetricType.Update,
|
||||
value: mauValue - mauLimit,
|
||||
limit: mauLimit,
|
||||
totalCost: updateOverageCost,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
totalCost: updateOverageCost,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('AccountQuery', () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockGetFullUsageAsync = jest.mocked(AccountQuery.getFullUsageAsync);
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetFullUsageAsync.mockClear();
|
||||
});
|
||||
|
||||
it('fetches usage data for an account', async () => {
|
||||
const mockData = createMockFullUsageData();
|
||||
mockGetFullUsageAsync.mockResolvedValue(mockData as any);
|
||||
|
||||
const currentDate = new Date();
|
||||
const startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
||||
const endDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
|
||||
|
||||
const result = await AccountQuery.getFullUsageAsync(
|
||||
mockGraphqlClient,
|
||||
'account-id',
|
||||
currentDate,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
|
||||
expect(mockGetFullUsageAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'account-id',
|
||||
expect.any(Date),
|
||||
expect.any(Date),
|
||||
expect.any(Date)
|
||||
);
|
||||
expect(result.name).toBe('test-account');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculatePercentUsed', () => {
|
||||
it('calculates correct percentage', () => {
|
||||
expect(calculatePercentUsed(50, 100)).toBe(50);
|
||||
expect(calculatePercentUsed(85, 100)).toBe(85);
|
||||
expect(calculatePercentUsed(100, 100)).toBe(100);
|
||||
});
|
||||
|
||||
it('caps at 100%', () => {
|
||||
expect(calculatePercentUsed(150, 100)).toBe(100);
|
||||
});
|
||||
|
||||
it('returns 0 when limit is 0', () => {
|
||||
expect(calculatePercentUsed(50, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProgressBar', () => {
|
||||
it('creates correct progress bar for 50%', () => {
|
||||
const bar = createProgressBar(50, 20);
|
||||
expect(bar).toBe('██████████░░░░░░░░░░');
|
||||
});
|
||||
|
||||
it('creates correct progress bar for 0%', () => {
|
||||
const bar = createProgressBar(0, 20);
|
||||
expect(bar).toBe('░░░░░░░░░░░░░░░░░░░░');
|
||||
});
|
||||
|
||||
it('creates correct progress bar for 100%', () => {
|
||||
const bar = createProgressBar(100, 20);
|
||||
expect(bar).toBe('████████████████████');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,409 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
UsageDisplayData,
|
||||
UsageMetricDisplay,
|
||||
calculateBillingPeriodInfo,
|
||||
extractUsageData,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
} from '../../commandUtils/usageUtils';
|
||||
import { AppPlatform, EasBuildBillingResourceClass } from '../../graphql/generated';
|
||||
import { AccountFullUsageData, AccountQuery } from '../../graphql/queries/AccountQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { createProgressBar } from '../../utils/usage/checkForOverages';
|
||||
|
||||
const MiB = 1024 * 1024;
|
||||
const GiB = 1024 * MiB;
|
||||
const TiB = 1024 * GiB;
|
||||
|
||||
function formatBytes(bytes: number): { value: number; unit: string } {
|
||||
if (bytes >= TiB) {
|
||||
return { value: bytes / TiB, unit: 'TiB' };
|
||||
} else if (bytes >= GiB) {
|
||||
return { value: bytes / GiB, unit: 'GiB' };
|
||||
} else {
|
||||
return { value: bytes / MiB, unit: 'MiB' };
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytesDisplay(bytes: number): string {
|
||||
const { value, unit } = formatBytes(bytes);
|
||||
return `${formatNumber(value, value < 10 ? 2 : value < 100 ? 1 : 0)} ${unit}`;
|
||||
}
|
||||
|
||||
function displayBandwidthMetric(metric: UsageMetricDisplay, indent: string = ' '): void {
|
||||
const progressBar = createProgressBar(metric.percentUsed, 20);
|
||||
const percentStr = `${metric.percentUsed}%`;
|
||||
const planUsageStr = `${formatBytesDisplay(metric.planValue)}/${formatBytesDisplay(
|
||||
metric.limit
|
||||
)}`;
|
||||
|
||||
let color = chalk.green;
|
||||
if (metric.percentUsed >= 100) {
|
||||
color = chalk.red;
|
||||
} else if (metric.percentUsed >= 85) {
|
||||
color = chalk.yellow;
|
||||
}
|
||||
|
||||
Log.log(`${indent}${metric.name} (plan): ${color(planUsageStr)}`);
|
||||
Log.log(`${indent}${progressBar} ${color(percentStr)}`);
|
||||
|
||||
if (metric.overageValue > 0) {
|
||||
Log.log(
|
||||
`${indent}${metric.name} (overage): ${chalk.red(
|
||||
formatBytesDisplay(metric.overageValue)
|
||||
)} (${formatCurrency(metric.overageCost)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function displayMetric(metric: UsageMetricDisplay, indent: string = ' '): void {
|
||||
const progressBar = createProgressBar(metric.percentUsed, 20);
|
||||
const percentStr = `${metric.percentUsed}%`;
|
||||
const planUsageStr = `${formatNumber(metric.planValue)}/${formatNumber(metric.limit)} ${
|
||||
metric.unit ?? ''
|
||||
}`;
|
||||
|
||||
let color = chalk.green;
|
||||
if (metric.percentUsed >= 100) {
|
||||
color = chalk.red;
|
||||
} else if (metric.percentUsed >= 85) {
|
||||
color = chalk.yellow;
|
||||
}
|
||||
|
||||
Log.log(`${indent}${metric.name} (plan): ${color(planUsageStr)}`);
|
||||
Log.log(`${indent}${progressBar} ${color(percentStr)}`);
|
||||
|
||||
if (metric.overageValue > 0) {
|
||||
Log.log(
|
||||
`${indent}${metric.name} (overage): ${chalk.red(
|
||||
`${formatNumber(metric.overageValue)} ${metric.unit ?? ''}`
|
||||
)} (${formatCurrency(metric.overageCost)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function billingUrl(accountName: string): string {
|
||||
return `https://expo.dev/accounts/${accountName}/settings/billing`;
|
||||
}
|
||||
|
||||
function displayUsage(data: UsageDisplayData, usageData: AccountFullUsageData): void {
|
||||
const subscription = usageData.subscription;
|
||||
|
||||
Log.newLine();
|
||||
Log.log(chalk.bold(`Account: ${data.accountName}`));
|
||||
Log.log(`Plan: ${data.subscriptionPlan}`);
|
||||
if (subscription?.concurrencies) {
|
||||
Log.log(
|
||||
`Concurrencies: ${subscription.concurrencies.total} total (iOS: ${subscription.concurrencies.ios}, Android: ${subscription.concurrencies.android})`
|
||||
);
|
||||
}
|
||||
Log.log(
|
||||
`Billing Period: ${formatDate(data.billingPeriod.start)} - ${formatDate(
|
||||
data.billingPeriod.end
|
||||
)}`
|
||||
);
|
||||
|
||||
const periodInfo = calculateBillingPeriodInfo(data);
|
||||
Log.log(
|
||||
`Days: ${periodInfo.daysElapsed} elapsed / ${periodInfo.totalDays} total (${periodInfo.daysRemaining} remaining)`
|
||||
);
|
||||
|
||||
Log.newLine();
|
||||
Log.log(chalk.bold.underline('EAS Build'));
|
||||
displayMetric(data.builds.total);
|
||||
if (data.builds.ios) {
|
||||
displayMetric(data.builds.ios, ' ');
|
||||
}
|
||||
if (data.builds.android) {
|
||||
displayMetric(data.builds.android, ' ');
|
||||
}
|
||||
|
||||
// Show build counts by platform and worker size
|
||||
if (data.builds.countsByPlatformAndSize.length > 0) {
|
||||
Log.log(' Breakdown by platform/worker:');
|
||||
for (const item of data.builds.countsByPlatformAndSize) {
|
||||
const platformName = item.platform === 'ios' ? 'iOS' : 'Android';
|
||||
Log.log(` ${platformName} ${item.resourceClass}: ${formatNumber(item.count)} builds`);
|
||||
}
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.log(chalk.bold.underline('EAS Update'));
|
||||
displayMetric(data.updates.mau);
|
||||
displayBandwidthMetric(data.updates.bandwidth);
|
||||
|
||||
Log.newLine();
|
||||
Log.log(chalk.bold.underline('Billing'));
|
||||
|
||||
const upcomingInvoice = subscription?.upcomingInvoice;
|
||||
|
||||
// Show subscription addons
|
||||
if (subscription?.addons && subscription.addons.length > 0) {
|
||||
Log.log(' Addons:');
|
||||
for (const addon of subscription.addons) {
|
||||
Log.log(` ${addon.name}: ${addon.quantity}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show upcoming invoice line items
|
||||
if (upcomingInvoice?.lineItems && upcomingInvoice.lineItems.length > 0) {
|
||||
Log.newLine();
|
||||
Log.log(' Upcoming invoice:');
|
||||
for (const lineItem of upcomingInvoice.lineItems) {
|
||||
const amount = lineItem.amount;
|
||||
const amountStr = amount < 0 ? chalk.green(formatCurrency(amount)) : formatCurrency(amount);
|
||||
Log.log(` ${lineItem.description}: ${amountStr}`);
|
||||
}
|
||||
Log.newLine();
|
||||
Log.log(` Invoice total: ${chalk.bold(formatCurrency(upcomingInvoice.total))}`);
|
||||
} else {
|
||||
// Fallback to the calculated estimate if no upcoming invoice
|
||||
if (data.recurringCents !== null && data.recurringCents > 0) {
|
||||
Log.log(` Base subscription: ${formatCurrency(data.recurringCents)}`);
|
||||
}
|
||||
|
||||
if (data.totalOverageCostCents > 0) {
|
||||
Log.log(` Current overages: ${chalk.yellow(formatCurrency(data.totalOverageCostCents))}`);
|
||||
if (data.builds.overageCostCents > 0) {
|
||||
Log.log(` Build overages: ${formatCurrency(data.builds.overageCostCents)}`);
|
||||
// Show breakdown by worker size
|
||||
for (const overage of data.builds.overagesByWorkerSize) {
|
||||
const platformName = overage.platform === AppPlatform.Ios ? 'iOS' : 'Android';
|
||||
const sizeName =
|
||||
overage.resourceClass === EasBuildBillingResourceClass.Large ? 'large' : 'medium';
|
||||
Log.log(
|
||||
` ${platformName} ${sizeName}: ${formatNumber(
|
||||
overage.count
|
||||
)} builds (${formatCurrency(overage.costCents)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.updates.overageCostCents > 0) {
|
||||
Log.log(` Update overages: ${formatCurrency(data.updates.overageCostCents)}`);
|
||||
}
|
||||
}
|
||||
|
||||
Log.log(` Estimated bill: ${chalk.bold(formatCurrency(data.estimatedBillCents))}`);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.log(chalk.dim(`View detailed billing: ${link(billingUrl(data.accountName))}`));
|
||||
}
|
||||
|
||||
export default class AccountUsage extends EasCommand {
|
||||
static override description = 'view account usage and billing for the current cycle';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'ACCOUNT_NAME',
|
||||
description:
|
||||
'Account name to view usage for. If not provided, the account will be selected interactively (or defaults to the only account if there is just one)',
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EasJsonOnlyFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { ACCOUNT_NAME: accountName },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(AccountUsage);
|
||||
|
||||
// Enable JSON output if either --json or --non-interactive is passed
|
||||
const json = jsonFlag || nonInteractive;
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient, actor },
|
||||
} = await this.getContextAsync(AccountUsage, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
// Find the target account
|
||||
const defaultAccount = actor.accounts[0];
|
||||
let targetAccount: { id: string; name: string };
|
||||
const availableAccounts = actor.accounts.map(a => a.name).join(', ');
|
||||
|
||||
if (accountName) {
|
||||
// First check if it's one of the user's accounts
|
||||
const found = actor.accounts.find(a => a.name === accountName);
|
||||
if (found) {
|
||||
targetAccount = found;
|
||||
} else {
|
||||
// Try to look up the account by name (user may have access via organization)
|
||||
try {
|
||||
const account = await AccountQuery.getByNameAsync(graphqlClient, accountName);
|
||||
if (!account) {
|
||||
throw new Error(
|
||||
`Account "${accountName}" not found. Available accounts: ${availableAccounts}`
|
||||
);
|
||||
}
|
||||
targetAccount = account;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Account "${accountName}" not found or you don't have access. Available accounts: ${availableAccounts}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (nonInteractive) {
|
||||
throw new Error(
|
||||
'ACCOUNT_NAME argument must be provided when running in `--non-interactive` mode.'
|
||||
);
|
||||
} else if (actor.accounts.length === 1) {
|
||||
// Only one account, use it directly
|
||||
targetAccount = defaultAccount;
|
||||
} else {
|
||||
// Prompt user to select an account
|
||||
targetAccount = await selectAsync(
|
||||
'Select account to view usage for:',
|
||||
actor.accounts.map(account => ({
|
||||
title: account.name,
|
||||
value: account,
|
||||
})),
|
||||
{ initial: defaultAccount }
|
||||
);
|
||||
}
|
||||
|
||||
const spinner = ora(`Fetching usage data for account ${targetAccount.name}`).start();
|
||||
|
||||
try {
|
||||
const currentDate = new Date();
|
||||
const { start, end } = await AccountQuery.getBillingPeriodAsync(
|
||||
graphqlClient,
|
||||
targetAccount.id,
|
||||
currentDate
|
||||
);
|
||||
const usageData = await AccountQuery.getFullUsageAsync(
|
||||
graphqlClient,
|
||||
targetAccount.id,
|
||||
currentDate,
|
||||
start,
|
||||
end
|
||||
);
|
||||
Log.debug(JSON.stringify(usageData, null, 2));
|
||||
|
||||
spinner.succeed(`Usage data loaded for ${targetAccount.name}`);
|
||||
|
||||
const displayData = extractUsageData(usageData);
|
||||
const periodInfo = calculateBillingPeriodInfo(displayData);
|
||||
|
||||
if (json) {
|
||||
const subscription = usageData.subscription;
|
||||
printJsonOnlyOutput({
|
||||
account: {
|
||||
name: displayData.accountName,
|
||||
plan: displayData.subscriptionPlan,
|
||||
concurrencies: subscription?.concurrencies ?? null,
|
||||
billingPeriod: {
|
||||
start: displayData.billingPeriod.start,
|
||||
end: displayData.billingPeriod.end,
|
||||
daysElapsed: periodInfo.daysElapsed,
|
||||
daysRemaining: periodInfo.daysRemaining,
|
||||
totalDays: periodInfo.totalDays,
|
||||
},
|
||||
},
|
||||
builds: {
|
||||
plan: {
|
||||
used: displayData.builds.total.planValue,
|
||||
limit: displayData.builds.total.limit,
|
||||
percentUsed: displayData.builds.total.percentUsed,
|
||||
},
|
||||
overage: {
|
||||
count: displayData.builds.total.overageValue,
|
||||
costCents: displayData.builds.total.overageCost,
|
||||
byWorkerSize: displayData.builds.overagesByWorkerSize.map(o => ({
|
||||
platform: o.platform.toLowerCase(),
|
||||
resourceClass: o.resourceClass.toLowerCase(),
|
||||
count: o.count,
|
||||
costCents: o.costCents,
|
||||
})),
|
||||
},
|
||||
byPlatformAndSize: displayData.builds.countsByPlatformAndSize.map(item => ({
|
||||
platform: item.platform,
|
||||
resourceClass: item.resourceClass,
|
||||
count: item.count,
|
||||
})),
|
||||
ios: displayData.builds.ios
|
||||
? {
|
||||
plan: {
|
||||
used: displayData.builds.ios.planValue,
|
||||
limit: displayData.builds.ios.limit,
|
||||
percentUsed: displayData.builds.ios.percentUsed,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
android: displayData.builds.android
|
||||
? {
|
||||
plan: {
|
||||
used: displayData.builds.android.planValue,
|
||||
limit: displayData.builds.android.limit,
|
||||
percentUsed: displayData.builds.android.percentUsed,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
},
|
||||
updates: {
|
||||
uniqueUpdaters: {
|
||||
plan: {
|
||||
used: displayData.updates.mau.planValue,
|
||||
limit: displayData.updates.mau.limit,
|
||||
percentUsed: displayData.updates.mau.percentUsed,
|
||||
},
|
||||
overage: {
|
||||
count: displayData.updates.mau.overageValue,
|
||||
costCents: displayData.updates.mau.overageCost,
|
||||
},
|
||||
},
|
||||
bandwidth: {
|
||||
plan: {
|
||||
usedBytes: displayData.updates.bandwidth.planValue,
|
||||
usedFormatted: formatBytesDisplay(displayData.updates.bandwidth.planValue),
|
||||
limitBytes: displayData.updates.bandwidth.limit,
|
||||
limitFormatted: formatBytesDisplay(displayData.updates.bandwidth.limit),
|
||||
percentUsed: displayData.updates.bandwidth.percentUsed,
|
||||
},
|
||||
overage: {
|
||||
usedBytes: displayData.updates.bandwidth.overageValue,
|
||||
usedFormatted: formatBytesDisplay(displayData.updates.bandwidth.overageValue),
|
||||
costCents: displayData.updates.bandwidth.overageCost,
|
||||
},
|
||||
},
|
||||
overageCostCents: displayData.updates.overageCostCents,
|
||||
},
|
||||
billing: {
|
||||
addons: subscription?.addons ?? [],
|
||||
upcomingInvoice: subscription?.upcomingInvoice ?? null,
|
||||
estimated: {
|
||||
recurringCents: displayData.recurringCents,
|
||||
overageCents: displayData.totalOverageCostCents,
|
||||
totalCents: displayData.estimatedBillCents,
|
||||
},
|
||||
},
|
||||
billingUrl: billingUrl(displayData.accountName),
|
||||
});
|
||||
} else {
|
||||
displayUsage(displayData, usageData);
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to fetch usage data');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import fs from 'fs-extra';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fetch from 'node-fetch';
|
||||
import path from 'path';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { AscApiClient, AscApiClientPostApi } from '../../submit/ios/AscApiClient';
|
||||
|
||||
export default class SubmitUploadToAsc extends EasCommand {
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
path: Flags.string({
|
||||
description: 'Path to the IPA file',
|
||||
required: true,
|
||||
}),
|
||||
key: Flags.string({
|
||||
description: 'Path to the ASC API Key JSON file',
|
||||
required: true,
|
||||
}),
|
||||
'app-id': Flags.string({
|
||||
description: 'App Store Connect App ID (e.g. 1491144534)',
|
||||
required: true,
|
||||
}),
|
||||
'bundle-version': Flags.string({
|
||||
description: 'CFBundleVersion (Build Version, e.g. 13)',
|
||||
required: true,
|
||||
}),
|
||||
'bundle-short-version': Flags.string({
|
||||
description: 'CFBundleShortVersionString (Marketing Version, e.g. 1.0.0)',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SubmitUploadToAsc);
|
||||
|
||||
const ipaPath = path.resolve(flags.path);
|
||||
if (!(await fs.pathExists(ipaPath))) {
|
||||
throw new Error(`IPA file not found: ${ipaPath}`);
|
||||
}
|
||||
const fileSize = (await fs.stat(ipaPath)).size;
|
||||
const fileName = path.basename(ipaPath);
|
||||
|
||||
const keyPath = path.resolve(flags.key);
|
||||
if (!(await fs.pathExists(keyPath))) {
|
||||
throw new Error(`Key file not found: ${keyPath}`);
|
||||
}
|
||||
|
||||
const keyJson = await fs.readJson(keyPath);
|
||||
const apiKey = z
|
||||
.object({
|
||||
issuer_id: z.string(),
|
||||
key_id: z.string(),
|
||||
key: z.string(),
|
||||
})
|
||||
.parse(keyJson);
|
||||
|
||||
Log.log('Generating JWT...');
|
||||
const token = jwt.sign({}, apiKey.key, {
|
||||
algorithm: 'ES256',
|
||||
issuer: apiKey.issuer_id,
|
||||
expiresIn: '20m',
|
||||
audience: 'appstoreconnect-v1',
|
||||
header: {
|
||||
kid: apiKey.key_id,
|
||||
},
|
||||
});
|
||||
|
||||
const client = new AscApiClient({ token });
|
||||
|
||||
Log.log('Reading App information...');
|
||||
const appResponse = await client.getAsync(
|
||||
'/v1/apps/:id',
|
||||
{ 'fields[apps]': ['bundleId', 'name'] as const },
|
||||
{ id: flags['app-id'] }
|
||||
);
|
||||
Log.log(
|
||||
`Uploading Build to "${appResponse.data.attributes.name}" (${appResponse.data.attributes.bundleId})...`
|
||||
);
|
||||
|
||||
Log.log('Creating Build Upload...');
|
||||
const buildUploadResponse = await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: flags['bundle-short-version'],
|
||||
cfBundleVersion: flags['bundle-version'],
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: flags['app-id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const buildUploadId = buildUploadResponse.data.id;
|
||||
|
||||
Log.log(`Build Upload initialized (ID: ${buildUploadId}). Preparing IPA upload...`);
|
||||
const buildFileResponse = await client.postAsync('/v1/buildUploadFiles', {
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
attributes: {
|
||||
assetType: 'ASSET',
|
||||
fileName,
|
||||
fileSize,
|
||||
uti: 'com.apple.ipa',
|
||||
},
|
||||
relationships: {
|
||||
buildUpload: {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: buildUploadId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Log.log(`IPA Upload initialized (ID: ${buildFileResponse.data.id}). Uploading IPA...`);
|
||||
await uploadChunksAsync({
|
||||
uploadOperations: buildFileResponse.data.attributes.uploadOperations,
|
||||
ipaPath,
|
||||
});
|
||||
|
||||
Log.log('Committing upload...');
|
||||
await client.patchAsync(
|
||||
`/v1/buildUploadFiles/:id`,
|
||||
{
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
id: buildFileResponse.data.id,
|
||||
attributes: {
|
||||
uploaded: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: buildFileResponse.data.id,
|
||||
}
|
||||
);
|
||||
|
||||
Log.log('Checking upload file status...');
|
||||
const waitingForFileStartedAt = performance.now();
|
||||
while (performance.now() - waitingForFileStartedAt < 60 * 1000 /* 60 seconds */) {
|
||||
const buildFileStatusResponse = await client.getAsync(
|
||||
`/v1/buildUploadFiles/:id`,
|
||||
{ 'fields[buildUploadFiles]': ['assetDeliveryState'] },
|
||||
{ id: buildFileResponse.data.id }
|
||||
);
|
||||
|
||||
if (buildFileStatusResponse.data.attributes.assetDeliveryState.state === 'AWAITING_UPLOAD') {
|
||||
Log.log(
|
||||
`Waiting for file upload to finish processing... (state = ${buildFileStatusResponse.data.attributes.assetDeliveryState.state})`
|
||||
);
|
||||
await setTimeout(2000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { errors = [], warnings = [] } =
|
||||
buildFileStatusResponse.data.attributes.assetDeliveryState;
|
||||
if (warnings.length > 0) {
|
||||
Log.warn(
|
||||
`Warnings:\n- ${warnings.map(w => `${w.description} (${w.code})`).join('\n- ')}\n`
|
||||
);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
Log.error(`Errors:\n- ${errors.map(e => `${e.description} (${e.code})`).join('\n- ')}\n`);
|
||||
}
|
||||
|
||||
if (buildFileStatusResponse.data.attributes.assetDeliveryState.state === 'FAILED') {
|
||||
throw new Error(`File upload (ID: ${buildFileResponse.data.id}) failed.`);
|
||||
} else if (buildFileStatusResponse.data.attributes.assetDeliveryState.state === 'COMPLETE') {
|
||||
Log.log(`File upload (ID: ${buildFileResponse.data.id}) complete!`);
|
||||
} else if (
|
||||
buildFileStatusResponse.data.attributes.assetDeliveryState.state === 'UPLOAD_COMPLETE'
|
||||
) {
|
||||
Log.log(`File upload (ID: ${buildFileResponse.data.id}) finished!`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Log.log('Checking build upload status...');
|
||||
const waitingForBuildStartedAt = performance.now();
|
||||
while (performance.now() - waitingForBuildStartedAt < 60 * 1000 /* 60 seconds */) {
|
||||
const buildUploadStatusResponse = await client.getAsync(
|
||||
`/v1/buildUploads/:id`,
|
||||
{ 'fields[buildUploads]': ['state', 'build'], include: ['build'] },
|
||||
{ id: buildUploadId }
|
||||
);
|
||||
|
||||
if (
|
||||
buildUploadStatusResponse.data.attributes.state.state === 'AWAITING_UPLOAD' ||
|
||||
buildUploadStatusResponse.data.attributes.state.state === 'PROCESSING'
|
||||
) {
|
||||
Log.log(
|
||||
`Waiting for build upload to finish... (status = ${buildUploadStatusResponse.data.attributes.state.state})`
|
||||
);
|
||||
await setTimeout(2000);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.log('\n');
|
||||
|
||||
const {
|
||||
errors = [],
|
||||
warnings = [],
|
||||
infos = [],
|
||||
} = buildUploadStatusResponse.data.attributes.state;
|
||||
if (infos.length > 0) {
|
||||
Log.log(`Infos:\n- ${infos.map(i => `${i.description} (${i.code})`).join('\n- ')}\n`);
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
Log.warn(
|
||||
`Warnings:\n- ${warnings.map(w => `${w.description} (${w.code})`).join('\n- ')}\n`
|
||||
);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
Log.error(`Errors:\n- ${errors.map(e => `${e.description} (${e.code})`).join('\n- ')}\n`);
|
||||
}
|
||||
|
||||
if (buildUploadStatusResponse.data.attributes.state.state === 'FAILED') {
|
||||
throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
|
||||
} else if (buildUploadStatusResponse.data.attributes.state.state === 'COMPLETE') {
|
||||
Log.log(`Build upload (ID: ${buildUploadId}) complete!`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadChunksAsync({
|
||||
uploadOperations,
|
||||
ipaPath,
|
||||
}: {
|
||||
uploadOperations: AscApiClientPostApi['/v1/buildUploadFiles']['response']['data']['attributes']['uploadOperations'];
|
||||
ipaPath: string;
|
||||
}): Promise<void> {
|
||||
const fd = await fs.open(ipaPath, 'r');
|
||||
try {
|
||||
const promises = uploadOperations.map(async (op, index) => {
|
||||
Log.log(
|
||||
` Uploading chunk ${index + 1}/${uploadOperations.length} (offset: ${op.offset}, length: ${
|
||||
op.length
|
||||
})...`
|
||||
);
|
||||
|
||||
const buffer = new Uint8Array(op.length);
|
||||
await fs.read(fd, buffer, 0, op.length, op.offset);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
};
|
||||
|
||||
for (const { name, value } of op.requestHeaders) {
|
||||
headers[name] = value;
|
||||
}
|
||||
|
||||
const response = await fetch(op.url, {
|
||||
method: op.method,
|
||||
headers,
|
||||
body: buffer,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`Failed to upload chunk ${index + 1}: ${response.status} ${response.statusText}\n${text}`
|
||||
);
|
||||
}
|
||||
Log.log(` Chunk ${index + 1}/${uploadOperations.length} uploaded.`);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
await fs.close(fd);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
// import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
UpdateRolloutInfoGroup,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
// import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
@@ -35,7 +33,6 @@ import {
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
filterCollectedAssetsByRequestedPlatforms,
|
||||
// findCompatibleBuildsAsync,
|
||||
generateEasMetadataAsync,
|
||||
getBranchNameForCommandAsync,
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
|
||||
@@ -639,12 +636,6 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
// const runtimeToCompatibleBuilds = await Promise.all(
|
||||
// runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
|
||||
// findCompatibleBuildsAsync(graphqlClient, projectId, obj)
|
||||
// )
|
||||
// );
|
||||
|
||||
for (const runtime of uniqBy(
|
||||
runtimeToPlatformsAndFingerprintInfoMapping,
|
||||
version => version.runtimeVersion
|
||||
@@ -716,58 +707,9 @@ export default class UpdatePublish extends EasCommand {
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
|
||||
// NOTE(brentvatne): temporarily disable logging this until we can revisit the formatting
|
||||
// and the logic for it - it's a bit too aggressive right now, and warns even if you're
|
||||
// not using EAS Build
|
||||
//
|
||||
// const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
|
||||
// ({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
|
||||
// )?.fingerprintInfoGroupWithCompatibleBuilds;
|
||||
// if (fingerprintsWithoutCompatibleBuilds) {
|
||||
// const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
|
||||
// ([_platform, fingerprintInfo]) => !fingerprintInfo.build
|
||||
// );
|
||||
// if (missingBuilds.length > 0) {
|
||||
// const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
// Log.warn('No compatible builds found for the following fingerprints:');
|
||||
// for (const [platform, fingerprintInfo] of missingBuilds) {
|
||||
// const fingerprintUrl = new URL(
|
||||
// `/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
|
||||
// getExpoWebsiteBaseUrl()
|
||||
// );
|
||||
// Log.warn(
|
||||
// formatFields(
|
||||
// [
|
||||
// {
|
||||
// label: `${this.prettyPlatform(platform)} fingerprint`,
|
||||
// value: fingerprintInfo.fingerprintHash,
|
||||
// },
|
||||
// { label: 'URL', value: fingerprintUrl.toString() },
|
||||
// ],
|
||||
// {
|
||||
// labelFormat: label => ` ${chalk.dim(label)}:`,
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
// Log.addNewLineIfNone();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// private prettyPlatform(updatePlatform: string): string {
|
||||
// switch (updatePlatform) {
|
||||
// case 'android':
|
||||
// return 'Android';
|
||||
// case 'ios':
|
||||
// return 'iOS';
|
||||
// default:
|
||||
// return updatePlatform;
|
||||
// }
|
||||
// }
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
|
||||
+805
-45
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,268 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AccountFullUsageQuery as AccountFullUsageQueryType,
|
||||
AccountFullUsageQueryVariables,
|
||||
AccountUsageForOverageWarningQuery,
|
||||
AccountUsageForOverageWarningQueryVariables,
|
||||
} from '../generated';
|
||||
import {
|
||||
AccountUsageMetricFragmentNode,
|
||||
BillingPeriodFragmentNode,
|
||||
SubscriptionDetailsFragmentNode,
|
||||
UsageMetricTotalFragmentNode,
|
||||
} from '../types/Account';
|
||||
|
||||
export type AccountFullUsageData = NonNullable<AccountFullUsageQueryType['account']['byId']>;
|
||||
|
||||
export const AccountQuery = {
|
||||
async getByNameAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountName: string
|
||||
): Promise<{ id: string; name: string } | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ account: { byName: { id: string; name: string } | null } }>(
|
||||
gql`
|
||||
query AccountByName($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountName },
|
||||
{ additionalTypenames: ['Account'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byName;
|
||||
},
|
||||
|
||||
async getFullUsageAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
currentDate: Date,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<AccountFullUsageData> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AccountFullUsageQueryType, AccountFullUsageQueryVariables>(
|
||||
gql`
|
||||
query AccountFullUsage(
|
||||
$accountId: String!
|
||||
$currentDate: DateTime!
|
||||
$startDate: DateTime!
|
||||
$endDate: DateTime!
|
||||
) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
name
|
||||
subscription {
|
||||
id
|
||||
...SubscriptionDetailsFragment
|
||||
}
|
||||
billingPeriod(date: $currentDate) {
|
||||
id
|
||||
...BillingPeriodFragment
|
||||
}
|
||||
usageMetrics {
|
||||
MEDIUM_ANDROID_BUILDS: metricsForServiceMetric(
|
||||
serviceMetric: BUILDS
|
||||
granularity: TOTAL
|
||||
timespan: { start: $startDate, end: $endDate }
|
||||
filterParams: {
|
||||
platform: "android"
|
||||
billingResourceClass: ["medium"]
|
||||
status: ["finished", "errored"]
|
||||
}
|
||||
) {
|
||||
id
|
||||
...AccountUsageMetricFragment
|
||||
}
|
||||
LARGE_ANDROID_BUILDS: metricsForServiceMetric(
|
||||
serviceMetric: BUILDS
|
||||
granularity: TOTAL
|
||||
timespan: { start: $startDate, end: $endDate }
|
||||
filterParams: {
|
||||
platform: "android"
|
||||
billingResourceClass: ["large"]
|
||||
status: ["finished", "errored"]
|
||||
}
|
||||
) {
|
||||
id
|
||||
...AccountUsageMetricFragment
|
||||
}
|
||||
MEDIUM_IOS_BUILDS: metricsForServiceMetric(
|
||||
serviceMetric: BUILDS
|
||||
granularity: TOTAL
|
||||
timespan: { start: $startDate, end: $endDate }
|
||||
filterParams: {
|
||||
platform: "ios"
|
||||
billingResourceClass: ["medium"]
|
||||
status: ["finished", "errored"]
|
||||
}
|
||||
) {
|
||||
id
|
||||
...AccountUsageMetricFragment
|
||||
}
|
||||
LARGE_IOS_BUILDS: metricsForServiceMetric(
|
||||
serviceMetric: BUILDS
|
||||
granularity: TOTAL
|
||||
timespan: { start: $startDate, end: $endDate }
|
||||
filterParams: {
|
||||
platform: "ios"
|
||||
billingResourceClass: ["large"]
|
||||
status: ["finished", "errored"]
|
||||
}
|
||||
) {
|
||||
id
|
||||
...AccountUsageMetricFragment
|
||||
}
|
||||
EAS_BUILD: byBillingPeriod(date: $currentDate, service: BUILDS) {
|
||||
id
|
||||
...UsageMetricTotalFragment
|
||||
}
|
||||
EAS_UPDATE: byBillingPeriod(date: $currentDate, service: UPDATES) {
|
||||
id
|
||||
...UsageMetricTotalFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(SubscriptionDetailsFragmentNode)}
|
||||
${print(BillingPeriodFragmentNode)}
|
||||
${print(AccountUsageMetricFragmentNode)}
|
||||
${print(UsageMetricTotalFragmentNode)}
|
||||
`,
|
||||
{
|
||||
accountId,
|
||||
currentDate: currentDate.toISOString(),
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
},
|
||||
{
|
||||
additionalTypenames: [
|
||||
'Account',
|
||||
'AccountUsageMetrics',
|
||||
'UsageMetricTotal',
|
||||
'BillingPeriod',
|
||||
'EstimatedUsage',
|
||||
'EstimatedOverageAndCost',
|
||||
],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
if (!data.account.byId) {
|
||||
throw new Error(`Account with ID ${accountId} not found`);
|
||||
}
|
||||
|
||||
return data.account.byId;
|
||||
},
|
||||
|
||||
async getUsageForOverageWarningAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
currentDate: Date
|
||||
): Promise<AccountUsageForOverageWarningQuery['account']['byId']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AccountUsageForOverageWarningQuery, AccountUsageForOverageWarningQueryVariables>(
|
||||
gql`
|
||||
query AccountUsageForOverageWarning($accountId: String!, $currentDate: DateTime!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
name
|
||||
subscription {
|
||||
id
|
||||
name
|
||||
}
|
||||
usageMetrics {
|
||||
EAS_BUILD: byBillingPeriod(date: $currentDate, service: BUILDS) {
|
||||
id
|
||||
planMetrics {
|
||||
id
|
||||
serviceMetric
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountId, currentDate: currentDate.toISOString() },
|
||||
{
|
||||
additionalTypenames: ['Account', 'AccountUsageMetrics', 'UsageMetricTotal'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byId;
|
||||
},
|
||||
|
||||
async getBillingPeriodAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
currentDate: Date
|
||||
): Promise<{
|
||||
start: Date;
|
||||
end: Date;
|
||||
}> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{
|
||||
account: {
|
||||
byId: {
|
||||
id: string;
|
||||
name: string;
|
||||
billingPeriod: { id: string; start: string; end: string; anchor: string };
|
||||
} | null;
|
||||
};
|
||||
}>(
|
||||
gql`
|
||||
query AccountBillingPeriod($accountId: String!, $currentDate: DateTime!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
name
|
||||
billingPeriod(date: $currentDate) {
|
||||
id
|
||||
...BillingPeriodFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(BillingPeriodFragmentNode)}
|
||||
`,
|
||||
{ accountId, currentDate: currentDate.toISOString() },
|
||||
{
|
||||
additionalTypenames: ['Account', 'BillingPeriod'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const { start, end } = data.account.byId?.billingPeriod ?? {};
|
||||
if (!start || !end) {
|
||||
throw new Error('Billing period data not found');
|
||||
}
|
||||
return {
|
||||
start: new Date(start),
|
||||
end: new Date(end),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AccountUsageForOverageWarningQuery,
|
||||
AccountUsageForOverageWarningQueryVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const AccountUsageQuery = {
|
||||
async getUsageForOverageWarningAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
currentDate: Date
|
||||
): Promise<AccountUsageForOverageWarningQuery['account']['byId']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AccountUsageForOverageWarningQuery, AccountUsageForOverageWarningQueryVariables>(
|
||||
gql`
|
||||
query AccountUsageForOverageWarning($accountId: String!, $currentDate: DateTime!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
name
|
||||
subscription {
|
||||
id
|
||||
name
|
||||
}
|
||||
usageMetrics {
|
||||
EAS_BUILD: byBillingPeriod(date: $currentDate, service: BUILDS) {
|
||||
id
|
||||
planMetrics {
|
||||
id
|
||||
serviceMetric
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountId, currentDate: currentDate.toISOString() },
|
||||
{
|
||||
additionalTypenames: ['Account', 'AccountUsageMetrics', 'UsageMetricTotal'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byId;
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable graphql/required-fields */
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AccountFragmentNode = gql`
|
||||
@@ -16,3 +18,153 @@ export const AccountFragmentNode = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const InvoiceLineItemFragmentNode = gql`
|
||||
fragment InvoiceLineItemFragment on InvoiceLineItem {
|
||||
id
|
||||
description
|
||||
amount
|
||||
period {
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const InvoiceFragmentNode = gql`
|
||||
${print(InvoiceLineItemFragmentNode)}
|
||||
|
||||
fragment InvoiceFragment on Invoice {
|
||||
id
|
||||
total
|
||||
lineItems {
|
||||
...InvoiceLineItemFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ConcurrenciesFragmentNode = gql`
|
||||
fragment ConcurrenciesFragment on Concurrencies {
|
||||
total
|
||||
android
|
||||
ios
|
||||
}
|
||||
`;
|
||||
|
||||
export const AddonDetailsFragmentNode = gql`
|
||||
fragment AddonDetailsFragment on AddonDetails {
|
||||
id
|
||||
name
|
||||
quantity
|
||||
}
|
||||
`;
|
||||
|
||||
export const SubscriptionDetailsFragmentNode = gql`
|
||||
${print(ConcurrenciesFragmentNode)}
|
||||
${print(AddonDetailsFragmentNode)}
|
||||
${print(InvoiceFragmentNode)}
|
||||
|
||||
fragment SubscriptionDetailsFragment on SubscriptionDetails {
|
||||
id
|
||||
name
|
||||
status
|
||||
nextInvoice
|
||||
nextInvoiceAmountDueCents
|
||||
recurringCents
|
||||
price
|
||||
concurrencies {
|
||||
...ConcurrenciesFragment
|
||||
}
|
||||
addons {
|
||||
...AddonDetailsFragment
|
||||
}
|
||||
upcomingInvoice {
|
||||
...InvoiceFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const BillingPeriodFragmentNode = gql`
|
||||
fragment BillingPeriodFragment on BillingPeriod {
|
||||
id
|
||||
start
|
||||
end
|
||||
anchor
|
||||
}
|
||||
`;
|
||||
|
||||
export const EstimatedUsagePlatformBreakdownFragmentNode = gql`
|
||||
fragment EstimatedUsagePlatformBreakdownFragment on EstimatedUsagePlatformBreakdown {
|
||||
ios {
|
||||
value
|
||||
limit
|
||||
}
|
||||
android {
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EstimatedUsageFragmentNode = gql`
|
||||
${print(EstimatedUsagePlatformBreakdownFragmentNode)}
|
||||
|
||||
fragment EstimatedUsageFragment on EstimatedUsage {
|
||||
id
|
||||
service
|
||||
serviceMetric
|
||||
metricType
|
||||
value
|
||||
limit
|
||||
platformBreakdown {
|
||||
...EstimatedUsagePlatformBreakdownFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EstimatedOverageAndCostFragmentNode = gql`
|
||||
fragment EstimatedOverageAndCostFragment on EstimatedOverageAndCost {
|
||||
id
|
||||
service
|
||||
serviceMetric
|
||||
metricType
|
||||
value
|
||||
limit
|
||||
totalCost
|
||||
metadata {
|
||||
... on AccountUsageEASBuildMetadata {
|
||||
billingResourceClass
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UsageMetricTotalFragmentNode = gql`
|
||||
${print(BillingPeriodFragmentNode)}
|
||||
${print(EstimatedUsageFragmentNode)}
|
||||
${print(EstimatedOverageAndCostFragmentNode)}
|
||||
|
||||
fragment UsageMetricTotalFragment on UsageMetricTotal {
|
||||
id
|
||||
billingPeriod {
|
||||
...BillingPeriodFragment
|
||||
}
|
||||
planMetrics {
|
||||
...EstimatedUsageFragment
|
||||
}
|
||||
overageMetrics {
|
||||
...EstimatedOverageAndCostFragment
|
||||
}
|
||||
totalCost
|
||||
}
|
||||
`;
|
||||
|
||||
export const AccountUsageMetricFragmentNode = gql`
|
||||
fragment AccountUsageMetricFragment on AccountUsageMetric {
|
||||
id
|
||||
serviceMetric
|
||||
metricType
|
||||
value
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,358 @@
|
||||
import { asyncResult } from '@expo/results';
|
||||
import fetch from 'node-fetch';
|
||||
import { ZodError, z } from 'zod';
|
||||
|
||||
import Log from '../../log';
|
||||
|
||||
type ApiSchema = {
|
||||
[Path in string]: {
|
||||
path?: z.ZodType<Record<string, string>>;
|
||||
request: z.ZodType<unknown>;
|
||||
response: z.ZodType<unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
const GetApi = {
|
||||
'/v1/apps/:id': {
|
||||
path: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
request: z.object({
|
||||
'fields[apps]': z.array(z.enum(['bundleId', 'name'])).refine(opts => {
|
||||
// Let's say we currently require fetching these two and nothing else.
|
||||
return opts.includes('bundleId') && opts.includes('name');
|
||||
}),
|
||||
}),
|
||||
response: z.object({
|
||||
data: z.object({
|
||||
type: z.literal('apps'),
|
||||
id: z.string(),
|
||||
attributes: z.object({
|
||||
bundleId: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'/v1/buildUploadFiles/:id': {
|
||||
path: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
request: z.object({
|
||||
'fields[buildUploadFiles]': z.array(z.enum(['assetDeliveryState'])).refine(opts => {
|
||||
return opts.includes('assetDeliveryState');
|
||||
}),
|
||||
}),
|
||||
response: z.object({
|
||||
data: z.object({
|
||||
type: z.literal('buildUploadFiles'),
|
||||
id: z.string(),
|
||||
attributes: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/appmediaassetstate
|
||||
assetDeliveryState: z.object({
|
||||
state: z.enum(['AWAITING_UPLOAD', 'UPLOAD_COMPLETE', 'COMPLETE', 'FAILED']),
|
||||
errors: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
warnings: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'/v1/buildUploads/:id': {
|
||||
path: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
request: z.object({
|
||||
'fields[buildUploads]': z.array(z.enum(['build', 'state'])).refine(opts => {
|
||||
return opts.includes('build') && opts.includes('state');
|
||||
}),
|
||||
include: z.array(z.enum(['build'])).refine(opts => {
|
||||
return opts.includes('build');
|
||||
}),
|
||||
}),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadresponse
|
||||
response: z.object({
|
||||
data: z.object({
|
||||
type: z.literal('buildUploads'),
|
||||
id: z.string(),
|
||||
attributes: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/buildupload/attributes-data.dictionary/state-data.dictionary
|
||||
state: z.object({
|
||||
state: z.enum(['AWAITING_UPLOAD', 'PROCESSING', 'COMPLETE', 'FAILED']),
|
||||
infos: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
errors: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
warnings: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
} satisfies ApiSchema;
|
||||
|
||||
const PostApi = {
|
||||
'/v1/buildUploads': {
|
||||
request: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadcreaterequest/data-data.dictionary
|
||||
data: z.object({
|
||||
type: z.literal('buildUploads'),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadcreaterequest/data-data.dictionary/attributes-data.dictionary
|
||||
attributes: z.object({
|
||||
cfBundleShortVersionString: z.string(),
|
||||
cfBundleVersion: z.string(),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/platform
|
||||
platform: z.enum(['IOS', 'MAC_OS', 'TV_OS', 'VISION_OS']),
|
||||
}),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadcreaterequest/data-data.dictionary/relationships-data.dictionary
|
||||
relationships: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadcreaterequest/data-data.dictionary/relationships-data.dictionary/app-data.dictionary
|
||||
app: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadcreaterequest/data-data.dictionary/relationships-data.dictionary/app-data.dictionary/data-data.dictionary
|
||||
data: z.object({
|
||||
type: z.literal('apps'),
|
||||
id: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadresponse
|
||||
response: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/buildupload
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('buildUploads'),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'/v1/buildUploadFiles': {
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfilecreaterequest
|
||||
request: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfilecreaterequest/data-data.dictionary
|
||||
data: z.object({
|
||||
type: z.literal('buildUploadFiles'),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfilecreaterequest/data-data.dictionary/attributes-data.dictionary
|
||||
attributes: z.object({
|
||||
assetType: z.enum(['ASSET', 'ASSET_DESCRIPTION', 'ASSET_SPI']),
|
||||
fileName: z.string(),
|
||||
fileSize: z.number().min(1).max(9007199254740991),
|
||||
uti: z.enum([
|
||||
'com.apple.binary-property-list',
|
||||
'com.apple.ipa',
|
||||
'com.apple.pkg',
|
||||
'com.apple.xml-property-list',
|
||||
'com.pkware.zip-archive',
|
||||
]),
|
||||
}),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfilecreaterequest/data-data.dictionary/relationships-data.dictionary
|
||||
relationships: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfilecreaterequest/data-data.dictionary/relationships-data.dictionary/buildupload-data.dictionary
|
||||
buildUpload: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfilecreaterequest/data-data.dictionary/relationships-data.dictionary/buildupload-data.dictionary/data-data.dictionary
|
||||
data: z.object({
|
||||
type: z.literal('buildUploads'),
|
||||
id: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfileresponse
|
||||
response: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfile
|
||||
data: z.object({
|
||||
type: z.literal('buildUploadFiles'),
|
||||
id: z.string(),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfile/attributes-data.dictionary
|
||||
attributes: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/appmediaassetstate
|
||||
uploadOperations: z.array(
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/deliveryfileuploadoperation
|
||||
z.object({
|
||||
length: z.number().min(1).max(9007199254740991),
|
||||
method: z.string(),
|
||||
offset: z.number().min(0).max(9007199254740991),
|
||||
partNumber: z.number().min(1).max(9007199254740991),
|
||||
requestHeaders: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
url: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
} satisfies ApiSchema;
|
||||
|
||||
const PatchApi = {
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/patch-v1-builduploadfiles-_id_
|
||||
'/v1/buildUploadFiles/:id': {
|
||||
path: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
request: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfileupdaterequest/data-data.dictionary
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('buildUploadFiles'),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfileupdaterequest/data-data.dictionary/attributes-data.dictionary
|
||||
attributes: z.object({
|
||||
uploaded: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
response: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfile
|
||||
data: z.object({
|
||||
type: z.literal('buildUploadFiles'),
|
||||
id: z.string(),
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/builduploadfile/attributes-data.dictionary
|
||||
attributes: z.object({
|
||||
// https://developer.apple.com/documentation/appstoreconnectapi/appmediaassetstate
|
||||
assetDeliveryState: z.object({
|
||||
state: z.enum(['AWAITING_UPLOAD', 'UPLOAD_COMPLETE', 'COMPLETE', 'FAILED']),
|
||||
errors: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
warnings: z.array(z.object({ code: z.string(), description: z.string() })).optional(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
} satisfies ApiSchema;
|
||||
|
||||
export type AscApiClientPostApi = {
|
||||
[Path in keyof typeof PostApi]: {
|
||||
request: z.input<(typeof PostApi)[Path]['request']>;
|
||||
response: z.output<(typeof PostApi)[Path]['response']>;
|
||||
};
|
||||
};
|
||||
|
||||
export type AscApiClientPatchApi = {
|
||||
[Path in keyof typeof PatchApi]: {
|
||||
request: z.input<(typeof PatchApi)[Path]['request']>;
|
||||
response: z.output<(typeof PatchApi)[Path]['response']>;
|
||||
};
|
||||
};
|
||||
|
||||
export class AscApiClient {
|
||||
private readonly baseUrl = 'https://api.appstoreconnect.apple.com';
|
||||
private readonly token: string;
|
||||
|
||||
constructor({ token }: { token: string }) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public async getAsync<TPath extends keyof typeof GetApi>(
|
||||
path: TPath,
|
||||
body: z.input<(typeof GetApi)[TPath]['request']>,
|
||||
params?: z.input<(typeof GetApi)[TPath]['path']>
|
||||
): Promise<z.output<(typeof GetApi)[TPath]['response']>> {
|
||||
const schema = GetApi[path];
|
||||
|
||||
let effectivePath: string = path;
|
||||
for (const [key, value] of Object.entries(params ?? {})) {
|
||||
effectivePath = effectivePath.replace(`:${key}`, String(value));
|
||||
}
|
||||
|
||||
return await this.sendRequestAsync({
|
||||
method: 'GET',
|
||||
path: `${effectivePath}?${new URLSearchParams(body).toString()}`,
|
||||
body,
|
||||
requestSchema: schema.request,
|
||||
responseSchema: schema.response,
|
||||
});
|
||||
}
|
||||
|
||||
public async postAsync<TPath extends keyof typeof PostApi>(
|
||||
path: TPath,
|
||||
body: z.input<(typeof PostApi)[TPath]['request']>
|
||||
): Promise<z.output<(typeof PostApi)[TPath]['response']>> {
|
||||
const schema = PostApi[path];
|
||||
return await this.sendRequestAsync({
|
||||
method: 'POST',
|
||||
path,
|
||||
body,
|
||||
requestSchema: schema.request,
|
||||
responseSchema: schema.response,
|
||||
});
|
||||
}
|
||||
|
||||
public async patchAsync<TPath extends keyof typeof PatchApi>(
|
||||
path: TPath,
|
||||
body: z.input<(typeof PatchApi)[TPath]['request']>,
|
||||
params: z.input<(typeof PatchApi)[TPath]['path']>
|
||||
): Promise<z.output<(typeof PatchApi)[TPath]['response']>> {
|
||||
const schema = PatchApi[path];
|
||||
|
||||
let effectivePath: string = path;
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
effectivePath = effectivePath.replace(`:${key}`, String(value));
|
||||
}
|
||||
|
||||
return await this.sendRequestAsync({
|
||||
method: 'PATCH',
|
||||
path: effectivePath,
|
||||
body,
|
||||
requestSchema: schema.request,
|
||||
responseSchema: schema.response,
|
||||
});
|
||||
}
|
||||
|
||||
private async sendRequestAsync({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
requestSchema,
|
||||
responseSchema,
|
||||
}: {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
path: string;
|
||||
body: unknown;
|
||||
requestSchema: z.ZodType<any>;
|
||||
responseSchema: z.ZodType<any>;
|
||||
}): Promise<any> {
|
||||
const url = new URL(path, this.baseUrl).toString();
|
||||
|
||||
const parsedBody = await asyncResult((async () => requestSchema.parse(body))());
|
||||
if (!parsedBody.ok) {
|
||||
throw new Error(
|
||||
`Malformed request to App Store Connect: ${z.prettifyError(
|
||||
parsedBody.enforceError() as ZodError
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
body: method === 'GET' ? undefined : JSON.stringify(parsedBody.value),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Unexpected response (${response.status}) from App Store Connect: ${text}`, {
|
||||
cause: response,
|
||||
});
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
Log.debug(`Response from App Store Connect: ${JSON.stringify(json, null, 2)}`);
|
||||
|
||||
const parsedResponse = await asyncResult((async () => responseSchema.parse(json))());
|
||||
if (!parsedResponse.ok) {
|
||||
throw new Error(
|
||||
`Malformed response from App Store Connect: ${z.prettifyError(
|
||||
parsedResponse.enforceError() as ZodError
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return parsedResponse.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import { AscApiClient } from '../AscApiClient';
|
||||
|
||||
describe(AscApiClient, () => {
|
||||
const token = 'test-token';
|
||||
const client = new AscApiClient({ token });
|
||||
|
||||
beforeAll(() => {
|
||||
nock.disableNetConnect();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('fetches app info', async () => {
|
||||
const appId = '1491144534';
|
||||
const responseFixture = require('./fixtures/apps/get-apps-200.json');
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(200, responseFixture);
|
||||
|
||||
const result = await client.getAsync(
|
||||
'/v1/apps/:id',
|
||||
{ 'fields[apps]': ['bundleId', 'name'] },
|
||||
{ id: appId }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: appId,
|
||||
attributes: {
|
||||
name: 'turtle-cli-example',
|
||||
bundleId: 'com.expo.turtle.cli.example',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('creates build upload', async () => {
|
||||
const buildUploadId = 'fdf9c476-aaa4-4ead-b91c-6e3cc3a47805';
|
||||
const responseFixture = require('./fixtures/buildUploads/post-buildUploads-200.json');
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.post('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: '1.0',
|
||||
cfBundleVersion: '13',
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: '1491144534',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.reply(200, responseFixture);
|
||||
|
||||
const result = await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: '1.0',
|
||||
cfBundleVersion: '13',
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: '1491144534',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: buildUploadId,
|
||||
},
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('creates build upload file', async () => {
|
||||
const buildUploadId = 'fdf9c476-aaa4-4ead-b91c-6e3cc3a47805';
|
||||
const fileId = '5b110930-f947-4998-a129-5926ffcedde5';
|
||||
const responseFixture = require('./fixtures/buildUploadFiles/post-buildUploadFiles-201.json');
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.post('/v1/buildUploadFiles', {
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
attributes: {
|
||||
assetType: 'ASSET',
|
||||
fileName: 'app.ipa',
|
||||
fileSize: 359,
|
||||
uti: 'com.apple.ipa',
|
||||
},
|
||||
relationships: {
|
||||
buildUpload: {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: buildUploadId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.reply(201, responseFixture);
|
||||
|
||||
const result = await client.postAsync('/v1/buildUploadFiles', {
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
attributes: {
|
||||
assetType: 'ASSET',
|
||||
fileName: 'app.ipa',
|
||||
fileSize: 359,
|
||||
uti: 'com.apple.ipa',
|
||||
},
|
||||
relationships: {
|
||||
buildUpload: {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: buildUploadId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
id: fileId,
|
||||
attributes: {
|
||||
uploadOperations: [
|
||||
{
|
||||
method: 'PUT',
|
||||
url: 'https://storage/upload',
|
||||
length: 359,
|
||||
offset: 0,
|
||||
requestHeaders: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
},
|
||||
],
|
||||
partNumber: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('commits upload', async () => {
|
||||
const fileId = '5b110930-f947-4998-a129-5926ffcedde5';
|
||||
const responseFixture = require('./fixtures/buildUploadFiles/patch-buildUploadFiles-200.json');
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/buildUploadFiles/${fileId}`, {
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
id: fileId,
|
||||
attributes: {
|
||||
uploaded: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.reply(200, responseFixture);
|
||||
|
||||
const result = await client.patchAsync(
|
||||
'/v1/buildUploadFiles/:id',
|
||||
{
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
id: fileId,
|
||||
attributes: {
|
||||
uploaded: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ id: fileId }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
id: fileId,
|
||||
attributes: {
|
||||
assetDeliveryState: responseFixture.data.attributes.assetDeliveryState,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fetches build upload info', async () => {
|
||||
const buildUploadId = 'fdf9c476-aaa4-4ead-b91c-6e3cc3a47805';
|
||||
const responseFixture = require('./fixtures/buildUploads/get-buildUploads-200.json');
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/buildUploads/${buildUploadId}`)
|
||||
.query({
|
||||
'fields[buildUploads]': 'build,state',
|
||||
include: 'build',
|
||||
})
|
||||
.reply(200, responseFixture);
|
||||
|
||||
const result = await client.getAsync(
|
||||
'/v1/buildUploads/:id',
|
||||
{
|
||||
'fields[buildUploads]': ['build', 'state'],
|
||||
include: ['build'],
|
||||
},
|
||||
{ id: buildUploadId }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: buildUploadId,
|
||||
attributes: {
|
||||
state: responseFixture.data.attributes.state,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fetches build upload file info', async () => {
|
||||
const fileId = '5b110930-f947-4998-a129-5926ffcedde5';
|
||||
const responseFixture = require('./fixtures/buildUploadFiles/get-buildUploadFiles-200.json');
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/buildUploadFiles/${fileId}`)
|
||||
.query({
|
||||
'fields[buildUploadFiles]': 'assetDeliveryState',
|
||||
})
|
||||
.reply(200, responseFixture);
|
||||
|
||||
const result = await client.getAsync(
|
||||
'/v1/buildUploadFiles/:id',
|
||||
{
|
||||
'fields[buildUploadFiles]': ['assetDeliveryState'],
|
||||
},
|
||||
{ id: fileId }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
type: 'buildUploadFiles',
|
||||
id: fileId,
|
||||
attributes: {
|
||||
assetDeliveryState: responseFixture.data.attributes.assetDeliveryState,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "apps",
|
||||
"id": "1491144534",
|
||||
"attributes": {
|
||||
"name": "turtle-cli-example",
|
||||
"bundleId": "com.expo.turtle.cli.example"
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/apps/1491144534"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/apps/1491144534?fields%5Bapps%5D=bundleId%2Cname"
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "buildUploadFiles",
|
||||
"id": "5b110930-f947-4998-a129-5926ffcedde5",
|
||||
"attributes": {
|
||||
"assetDeliveryState": {
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"state": "COMPLETE"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploadFiles/5b110930-f947-4998-a129-5926ffcedde5"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploadFiles/5b110930-f947-4998-a129-5926ffcedde5?fields%5BbuildUploadFiles%5D=assetDeliveryState"
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "buildUploadFiles",
|
||||
"id": "5b110930-f947-4998-a129-5926ffcedde5",
|
||||
"attributes": {
|
||||
"assetDeliveryState": {
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"state": "COMPLETE"
|
||||
},
|
||||
"assetToken": "token.json",
|
||||
"assetType": "ASSET",
|
||||
"fileName": "app.ipa",
|
||||
"fileSize": 359,
|
||||
"sourceFileChecksums": {
|
||||
"file": null,
|
||||
"composite": {
|
||||
"hash": "hash",
|
||||
"algorithm": "MD5"
|
||||
}
|
||||
},
|
||||
"uploadOperations": null,
|
||||
"uti": "com.apple.ipa"
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploadFiles/5b110930-f947-4998-a129-5926ffcedde5"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploadFiles/5b110930-f947-4998-a129-5926ffcedde5"
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "buildUploadFiles",
|
||||
"id": "5b110930-f947-4998-a129-5926ffcedde5",
|
||||
"attributes": {
|
||||
"assetDeliveryState": {
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"state": "AWAITING_UPLOAD"
|
||||
},
|
||||
"assetToken": "token.json",
|
||||
"assetType": "ASSET",
|
||||
"fileName": "app.ipa",
|
||||
"fileSize": 359,
|
||||
"sourceFileChecksums": {
|
||||
"file": null,
|
||||
"composite": null
|
||||
},
|
||||
"uploadOperations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"url": "https://storage/upload",
|
||||
"length": 359,
|
||||
"offset": 0,
|
||||
"requestHeaders": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"expiration": "2026-02-06T01:31:03.779-08:00",
|
||||
"partNumber": 1,
|
||||
"entityTag": "tag"
|
||||
}
|
||||
],
|
||||
"uti": "com.apple.ipa"
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploadFiles/5b110930-f947-4998-a129-5926ffcedde5"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploadFiles"
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "buildUploads",
|
||||
"id": "fdf9c476-aaa4-4ead-b91c-6e3cc3a47805",
|
||||
"attributes": {
|
||||
"state": {
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"infos": [],
|
||||
"state": "PROCESSING"
|
||||
}
|
||||
},
|
||||
"relationships": {
|
||||
"build": {
|
||||
"data": null
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploads/fdf9c476-aaa4-4ead-b91c-6e3cc3a47805"
|
||||
}
|
||||
},
|
||||
"included": [],
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploads/fdf9c476-aaa4-4ead-b91c-6e3cc3a47805?fields%5BbuildUploads%5D=build%2Cstate&include=build"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "buildUploads",
|
||||
"id": "fdf9c476-aaa4-4ead-b91c-6e3cc3a47805",
|
||||
"attributes": {
|
||||
"cfBundleShortVersionString": "1.0",
|
||||
"cfBundleVersion": "13",
|
||||
"createdDate": "2026-01-30T01:31:03.007-08:00",
|
||||
"state": {
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"infos": [],
|
||||
"state": "AWAITING_UPLOAD"
|
||||
},
|
||||
"platform": "IOS",
|
||||
"uploadedDate": null
|
||||
},
|
||||
"relationships": {
|
||||
"buildUploadFiles": {
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploads/fdf9c476-aaa4-4ead-b91c-6e3cc3a47805/relationships/buildUploadFiles",
|
||||
"related": "https://api.appstoreconnect.apple.com/v1/buildUploads/fdf9c476-aaa4-4ead-b91c-6e3cc3a47805/buildUploadFiles"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploads/fdf9c476-aaa4-4ead-b91c-6e3cc3a47805"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://api.appstoreconnect.apple.com/v1/buildUploads"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
EstimatedUsage,
|
||||
UsageMetricType,
|
||||
} from '../../../graphql/generated';
|
||||
import { AccountUsageQuery } from '../../../graphql/queries/AccountUsageQuery';
|
||||
import { AccountQuery } from '../../../graphql/queries/AccountQuery';
|
||||
import Log, { link } from '../../../log';
|
||||
import {
|
||||
createProgressBar,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
maybeWarnAboutUsageOveragesAsync,
|
||||
} from '../checkForOverages';
|
||||
|
||||
jest.mock('../../../graphql/queries/AccountUsageQuery');
|
||||
jest.mock('../../../graphql/queries/AccountQuery');
|
||||
jest.mock('../../../log', () => ({
|
||||
...jest.requireActual('../../../log'),
|
||||
warn: jest.fn(),
|
||||
@@ -72,7 +72,7 @@ function createMockAccountUsage({
|
||||
describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockGetUsageForOverageWarningAsync = jest.mocked(
|
||||
AccountUsageQuery.getUsageForOverageWarningAsync
|
||||
AccountQuery.getUsageForOverageWarningAsync
|
||||
);
|
||||
const mockWarn = jest.mocked(Log.warn);
|
||||
const mockDebug = jest.mocked(Log.debug);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AccountUsageQuery } from '../../graphql/queries/AccountUsageQuery';
|
||||
import { AccountQuery } from '../../graphql/queries/AccountQuery';
|
||||
import Log, { link } from '../../log';
|
||||
|
||||
const THRESHOLD_PERCENT = 85;
|
||||
@@ -19,11 +19,7 @@ export async function maybeWarnAboutUsageOveragesAsync({
|
||||
name,
|
||||
subscription,
|
||||
usageMetrics: { EAS_BUILD },
|
||||
} = await AccountUsageQuery.getUsageForOverageWarningAsync(
|
||||
graphqlClient,
|
||||
accountId,
|
||||
currentDate
|
||||
);
|
||||
} = await AccountQuery.getUsageForOverageWarningAsync(graphqlClient, accountId, currentDate);
|
||||
|
||||
const planMetric = EAS_BUILD?.planMetrics?.[0];
|
||||
if (!planMetric || !subscription) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "16.30.0",
|
||||
"version": "16.32.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "1.0.271",
|
||||
"@expo/build-tools": "1.0.272",
|
||||
"@expo/config-plugins": "8.0.8",
|
||||
"@expo/config-types": "52.0.1",
|
||||
"@expo/downloader": "1.0.260",
|
||||
|
||||
@@ -51,6 +51,7 @@ export function getBuildEnv({
|
||||
job.platform ?? (config.resourceClass && ResourceClassToPlatform[config.resourceClass]);
|
||||
if (runnerPlatform === Platform.IOS) {
|
||||
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', config.cocoapodsCacheUrl);
|
||||
setEnv(env, 'COMPILER_INDEX_STORE_ENABLE', 'NO');
|
||||
|
||||
if (job.builderEnvironment?.env?.EAS_USE_CACHE === '1') {
|
||||
setEnv(env, 'USE_CCACHE', '1');
|
||||
@@ -131,6 +132,11 @@ export function getBuildEnv({
|
||||
setEnv(env, 'EXPO_STAGING', '1');
|
||||
}
|
||||
|
||||
const xcodeVersionResult = spawnSync('xcodebuild', ['-version'], { env });
|
||||
if (xcodeVersionResult.stdout?.includes('Xcode 26.0')) {
|
||||
setEnv(env, 'DELIVER_ALTOOL_ADDITIONAL_UPLOAD_PARAMETERS', '--use-old-altool');
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
|
||||
@@ -1546,10 +1546,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-2.1.13.tgz#b4950a68a54befa8f1581ae0066477b068dd4757"
|
||||
integrity sha512-nt3efiJhAWTHl9ikKYrHEuv3dhqCdicsHFRE9LmvtcVsPhXl9bAsm0gbACoLPr7ClP8664H/S6SdVJOD/tw0jg==
|
||||
|
||||
"@expo/[email protected]1":
|
||||
version "1.0.271"
|
||||
resolved "https://registry.yarnpkg.com/@expo/build-tools/-/build-tools-1.0.271.tgz#07924aafc90a956be2fd465f5be07ced571f4d13"
|
||||
integrity sha512-rakZ3X4ldXf/RmXQIsZDzWxsnoRHaESzOg+qRhUgkPMBIvDmemPtXEzhBtBEPOgdOmWSzAMAG5O0VMtlUxLlug==
|
||||
"@expo/[email protected]2":
|
||||
version "1.0.272"
|
||||
resolved "https://registry.yarnpkg.com/@expo/build-tools/-/build-tools-1.0.272.tgz#d1a8fca48ca18da3a80fb5f1990df03f20c29c58"
|
||||
integrity sha512-lN7ZQ+0qmvxJNYPaDQQoDo8OXzDj5xB8jgcpsKYlje58moPmWMGgRMYDXoBDOiR97H6vUn2LQ+F/HmNf0gDskQ==
|
||||
dependencies:
|
||||
"@expo/config" "10.0.6"
|
||||
"@expo/config-plugins" "9.0.12"
|
||||
|
||||
Reference in new issue
Block a user