Compare commits

..
Author SHA1 Message Date
Quinlan Jung 439abcb784 [eas-cli] paginated progress bar 2024-04-12 16:21:25 -07:00
101 changed files with 2110 additions and 4369 deletions

No files matched your search

+1 -29
View File
@@ -69,22 +69,16 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 20
- run: brew install python-setuptools
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64,darwin-arm64
yarn oclif pack:tarballs --no-xz --targets darwin-x64
- id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm64
run: echo "filename=$(ls eas-*-arm64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
env:
@@ -94,15 +88,6 @@ jobs:
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload darwin-arm64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm64.outputs.filename }}
asset_name: ${{ steps.arm64.outputs.filename }}
asset_content_type: application/gzip
build-windows:
name: Build for Windows
runs-on: ubuntu-latest
@@ -226,16 +211,3 @@ jobs:
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
open-pr-in-universe:
name: Open a PR with `@expo/eas-json` bump in universe
needs: publish-to-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Open a PR with `@expo/eas-json` version bump in universe
uses: benc-uk/workflow-dispatch@v1
with:
repo: expo/universe
ref: main
workflow: update-eas-json.yml
token: ${{ secrets.EXPO_BOT_PAT }}
-87
View File
@@ -12,94 +12,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
## [9.0.1](https://github.com/expo/eas-cli/releases/tag/v9.0.1) - 2024-05-09
### 🧹 Chores
- Update image tags in `eas.schema.json`. ([#2363](https://github.com/expo/eas-cli/pull/2363) by [@szdziedzic](https://github.com/szdziedzic))
## [9.0.0](https://github.com/expo/eas-cli/releases/tag/v9.0.0) - 2024-05-08
### 🛠 Breaking changes
- Allow modification of provisioning profile in CI, add --freeze-credentials flag. ([#2347](https://github.com/expo/eas-cli/pull/2347) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Pass env from process.env and build profile to expo-updates CLI calls where applicable. ([#2359](https://github.com/expo/eas-cli/pull/2359) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove more classic updates code. ([#2357](https://github.com/expo/eas-cli/pull/2357) by [@wschurman](https://github.com/wschurman))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2360](https://github.com/expo/eas-cli/pull/2360) by [@expo-bot](https://github.com/expo-bot))
- Don't pass custom global `"expoCli"` version set in `eas.json` to EAS Build process and warn users when setting it. ([#2361](https://github.com/expo/eas-cli/pull/2361) by [@szdziedzic](https://github.com/szdziedzic))
## [8.0.0](https://github.com/expo/eas-cli/releases/tag/v8.0.0) - 2024-05-01
### 🛠 Breaking changes
- Stop creating a channel on `eas update` and `eas update:roll-back-to-embedded` unless the `--channel` flag is specified. ([#2346](https://github.com/expo/eas-cli/pull/2346) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Improve login info message for other login options. ([#2352](https://github.com/expo/eas-cli/pull/2352) by [@wschurman](https://github.com/wschurman))
- Show the -s, --sso option in the login command help. ([#2353](https://github.com/expo/eas-cli/pull/2353) by [@lzkb](https://github.com/lzkb))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2351](https://github.com/expo/eas-cli/pull/2351) by [@expo-bot](https://github.com/expo-bot))
- Don't prompt users to set push notifications by default if they don't have the `expo-notifications` installed. ([#2343](https://github.com/expo/eas-cli/pull/2343) by [@szdziedzic](https://github.com/szdziedzic))
## [7.8.5](https://github.com/expo/eas-cli/releases/tag/v7.8.5) - 2024-04-26
### 🐛 Bug fixes
- Add explicit workflow arg to expo-update CLI calls. ([#2340](https://github.com/expo/eas-cli/pull/2340) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Improve error message if the server returns `UNAUTHORIZED_ERROR`. ([#2345](https://github.com/expo/eas-cli/pull/2345) by [@szdziedzic](https://github.com/szdziedzic))
- Fill in min expo-updates version for expo-updates CLI. ([#2344](https://github.com/expo/eas-cli/pull/2344) by [@wschurman](https://github.com/wschurman))
## [7.8.4](https://github.com/expo/eas-cli/releases/tag/v7.8.4) - 2024-04-24
### 🧹 Chores
- Update the list of available Android images. ([#2337](https://github.com/expo/eas-cli/pull/2337) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Make multi-select for revoking distribution certificates more readable. ([#2342](https://github.com/expo/eas-cli/pull/2342) by [@szdziedzic](https://github.com/szdziedzic))
- Improve error message displayed when EAS CLI version doesn't satisfy minimal version required specified in eas.json. ([#2341](https://github.com/expo/eas-cli/pull/2341) by [@szdziedzic](https://github.com/szdziedzic))
## [7.8.3](https://github.com/expo/eas-cli/releases/tag/v7.8.3) - 2024-04-23
### 🐛 Bug fixes
- Don't prompt to set `android.package` and `ios.bundleIdentifier` values when running in non-interactive mode. ([#2336](https://github.com/expo/eas-cli/pull/2336) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Amend credential removal wording. ([#2334](https://github.com/expo/eas-cli/pull/2334) by [@quinlanj](https://github.com/quinlanj))
## [7.8.2](https://github.com/expo/eas-cli/releases/tag/v7.8.2) - 2024-04-15
### 🐛 Bug fixes
- Fix display of errors when expo-updates CLI command fails. ([#2324](https://github.com/expo/eas-cli/pull/2324) by [@wschurman](https://github.com/wschurman))
- Move credentials endpoints to paginated counterparts. ([#2327](https://github.com/expo/eas-cli/pull/2327) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Add progress bar for fetching paginated datasets. ([#2326](https://github.com/expo/eas-cli/pull/2326) by [@quinlanj](https://github.com/quinlanj))
## [7.8.1](https://github.com/expo/eas-cli/releases/tag/v7.8.1) - 2024-04-11
### 🐛 Bug fixes
- Fix command source files URLs in autogenerated `README`. ([#2318](https://github.com/expo/eas-cli/pull/2318) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2316](https://github.com/expo/eas-cli/pull/2316) by [@expo-bot](https://github.com/expo-bot))
- Stop querying `Build.resourceClass` field. ([#2320](https://github.com/expo/eas-cli/pull/2320) by [@szdziedzic](https://github.com/szdziedzic))
## [7.8.0](https://github.com/expo/eas-cli/releases/tag/v7.8.0) - 2024-04-08
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "9.0.2",
"version": "7.8.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+64 -72
View File
@@ -128,10 +128,7 @@ log in with your Expo account
```
USAGE
$ eas account:login [-s]
FLAGS
-s, --sso Login with SSO
$ eas account:login
DESCRIPTION
log in with your Expo account
@@ -140,7 +137,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/account/login.ts)_
## `eas account:logout`
@@ -157,7 +154,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/account/logout.ts)_
## `eas account:view`
@@ -174,7 +171,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -188,7 +185,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -219,7 +216,7 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v2.3.10/packages/eas-cli/src/commands/autocomplete/index.ts)_
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v2.3.10/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
@@ -240,7 +237,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -261,7 +258,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -281,7 +278,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -301,7 +298,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -324,7 +321,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/branch/view.ts)_
## `eas build`
@@ -334,7 +331,7 @@ start a build
USAGE
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache]
[--auto-submit | --auto-submit-with-profile <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--json --non-interactive]
trace|debug|info|warn|error|fatal] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to
@@ -348,8 +345,6 @@ FLAGS
--build-logger-level=(trace|debug|info|warn|error|fatal) The level of logs to output during the build process.
Defaults to "info".
--clear-cache Clear cache before the build
--freeze-credentials Prevent the build from updating credentials in
non-interactive mode
--json Enable JSON output, non-JSON messages will be printed to
stderr.
--local Run build locally [experimental]
@@ -361,7 +356,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -380,7 +375,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -397,7 +392,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/v9.0.2/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -416,7 +411,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/delete.ts)_
## `eas build:inspect`
@@ -451,7 +446,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/v9.0.2/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -492,7 +487,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/v9.0.2/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/list.ts)_
## `eas build:resign`
@@ -518,7 +513,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/resign.ts)_
## `eas build:run`
@@ -544,7 +539,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/v9.0.2/packages/eas-cli/src/commands/build/run.ts)_
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/run.ts)_
## `eas build:submit`
@@ -594,7 +589,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/v9.0.2/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -613,7 +608,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/v9.0.2/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -632,7 +627,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/v9.0.2/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -649,7 +644,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/v9.0.2/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -670,7 +665,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -692,7 +687,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/v9.0.2/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -712,7 +707,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -744,7 +739,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/v9.0.2/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -767,7 +762,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/channel/view.ts)_
## `eas config`
@@ -788,7 +783,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/v9.0.2/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/config.ts)_
## `eas credentials`
@@ -805,7 +800,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -823,7 +818,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/v9.0.2/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/credentials/configure-build.ts)_
## `eas device:create`
@@ -837,7 +832,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/v9.0.2/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/device/create.ts)_
## `eas device:delete`
@@ -857,7 +852,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/v9.0.2/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/device/delete.ts)_
## `eas device:list`
@@ -878,7 +873,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/v9.0.2/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/device/list.ts)_
## `eas device:rename`
@@ -899,7 +894,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -913,7 +908,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/v9.0.2/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -927,7 +922,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -947,7 +942,7 @@ DESCRIPTION
Display help for eas.
```
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.23/packages/eas-cli/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.23/src/commands/help.ts)_
## `eas init`
@@ -975,10 +970,7 @@ log in with your Expo account
```
USAGE
$ eas login [-s]
FLAGS
-s, --sso Login with SSO
$ eas login
DESCRIPTION
log in with your Expo account
@@ -1018,7 +1010,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1035,7 +1027,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/v9.0.2/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1052,7 +1044,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/v9.0.2/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/metadata/push.ts)_
## `eas open`
@@ -1066,7 +1058,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/v9.0.2/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/open.ts)_
## `eas project:info`
@@ -1080,7 +1072,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/project/info.ts)_
## `eas project:init`
@@ -1102,7 +1094,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/project/init.ts)_
## `eas secret:create`
@@ -1125,7 +1117,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [packages/eas-cli/src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1143,7 +1135,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [packages/eas-cli/src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1157,7 +1149,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [packages/eas-cli/src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1177,7 +1169,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [packages/eas-cli/src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/secret/push.ts)_
## `eas submit`
@@ -1208,7 +1200,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/submit.ts)_
## `eas update`
@@ -1241,7 +1233,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1259,7 +1251,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/v9.0.2/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1280,7 +1272,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/v9.0.2/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1302,7 +1294,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1329,7 +1321,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/v9.0.2/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1356,7 +1348,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/v9.0.2/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1375,7 +1367,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1395,7 +1387,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1416,7 +1408,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1436,7 +1428,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1454,7 +1446,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1476,7 +1468,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1493,7 +1485,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v9.0.2/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v7.8.0/src/commands/webhook/view.ts)_
## `eas whoami`
+1126 -2004
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "9.0.2",
"version": "7.8.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -13,8 +13,8 @@
"@expo/config": "8.5.4",
"@expo/config-plugins": "7.8.4",
"@expo/config-types": "50.0.0",
"@expo/eas-build-job": "1.0.107",
"@expo/eas-json": "9.0.1",
"@expo/eas-build-job": "1.0.95",
"@expo/eas-json": "7.8.0",
"@expo/json-file": "8.2.37",
"@expo/logger": "1.0.57",
"@expo/multipart-body-parser": "1.1.0",
@@ -28,7 +28,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.107",
"@expo/steps": "1.0.95",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -77,7 +77,7 @@
"resolve-from": "5.0.0",
"semver": "7.5.4",
"slash": "3.0.0",
"tar": "6.2.1",
"tar": "6.2.0",
"terminal-link": "2.1.1",
"tslib": "2.6.2",
"turndown": "7.1.2",
+2 -1
View File
@@ -3,7 +3,8 @@ const fs = require('fs/promises');
(async () => {
// Patch `oclif readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/src\/commands/g, 'packages/eas-cli/src/commands');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent);
// eslint-disable-next-line no-console
@@ -3,7 +3,13 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -85,6 +91,7 @@ function createMockBuildFragment({
},
},
status: BuildStatus.InQueue,
resourceClass: BuildResourceClass.AndroidMedium,
isForIosSimulator: false,
};
}
@@ -3,7 +3,13 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToDeleteAsync } from '../../commands/build/delete';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -85,6 +91,7 @@ function createMockBuildFragment({
},
},
status: BuildStatus.InQueue,
resourceClass: BuildResourceClass.AndroidMedium,
isForIosSimulator: false,
};
}
@@ -107,7 +107,6 @@ export async function prepareAndroidBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
@@ -15,6 +15,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
@@ -68,6 +68,7 @@ export async function prepareJobAsync(
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
@@ -78,6 +79,7 @@ export async function prepareJobAsync(
secrets: {
...buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -18,13 +18,11 @@ export async function syncProjectConfigurationAsync({
exp,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
@@ -32,7 +30,7 @@ export async function syncProjectConfigurationAsync({
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env });
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
@@ -41,7 +41,6 @@ export async function createBuildContextAsync<T extends Platform>({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -61,7 +60,6 @@ export async function createBuildContextAsync<T extends Platform>({
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
@@ -88,7 +86,6 @@ export async function createBuildContextAsync<T extends Platform>({
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
});
const devClientProperties = getDevClientEventProperties({
-1
View File
@@ -89,7 +89,6 @@ export async function prepareIosBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
@@ -16,6 +16,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
builderEnvironment: job.builderEnvironment,
@@ -62,6 +62,7 @@ export async function prepareJobAsync(
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
@@ -72,6 +73,7 @@ export async function prepareJobAsync(
secrets: {
buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator,
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosVersionAutoIncrement } from '@expo/eas-json';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
@@ -15,21 +15,19 @@ export async function syncProjectConfigurationAsync({
targets,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ vcsClient, projectDir, exp, workflow, env });
await syncUpdatesConfigurationAsync(vcsClient, projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else {
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.107';
const PLUGIN_PACKAGE_VERSION = '1.0.96';
export enum LocalBuildMode {
/**
+43 -16
View File
@@ -9,16 +9,26 @@ import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios
import { LocalBuildMode } from './local';
import { BuildDistributionType } from './types';
import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import {
getUsername,
isClassicUpdatesSupportedAsync,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule';
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from '../update/android/UpdatesModule';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from '../update/ios/UpdatesModule';
import { easCliVersion } from '../utils/easCli';
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<Metadata> {
const channelObject = await resolveChannelAsync(ctx);
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution = ctx.buildProfile.distribution ?? BuildDistributionType.STORE;
const metadata: Metadata = {
trackingContext: ctx.analyticsEventProperties,
@@ -27,16 +37,9 @@ export async function collectMetadataAsync<T extends Platform>(
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion:
(await resolveRuntimeVersionAsync({
exp: ctx.exp,
platform: ctx.platform,
workflow: ctx.workflow,
projectDir: ctx.projectDir,
env: ctx.buildProfile.env,
})) ?? undefined,
runtimeVersion: (await resolveRuntimeVersionAsync(ctx)) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelObject,
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
@@ -114,9 +117,9 @@ function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string
}
}
async function resolveChannelAsync<T extends Platform>(
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | null> {
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
@@ -128,7 +131,31 @@ async function resolveChannelAsync<T extends Platform>(
return { channel };
}
return null;
if (!(await isClassicUpdatesSupportedAsync(ctx.projectDir))) {
return null;
}
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
return { releaseChannel };
}
async function getNativeReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
}
}
async function getNativeChannelAsync<T extends Platform>(
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config-types';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
BuildProfile,
@@ -71,6 +71,7 @@ import {
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
@@ -94,7 +95,6 @@ export interface BuildFlags {
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
}
export async function runBuildAndSubmitAsync(
@@ -363,7 +363,6 @@ async function prepareAndStartBuildAsync({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel,
freezeCredentials: flags.freezeCredentials,
});
if (moreBuilds) {
@@ -376,6 +375,12 @@ async function prepareAndStartBuildAsync({
);
}
await validateBuildProfileConfigMatchesProjectConfigAsync(
buildCtx.exp,
buildProfile,
buildCtx.projectId,
flags.nonInteractive
);
if (buildProfile.profile.channel) {
await validateExpoUpdatesInstalledAsProjectDependencyAsync({
exp: buildCtx.exp,
@@ -385,7 +390,6 @@ async function prepareAndStartBuildAsync({
sdkVersion: buildCtx.exp.sdkVersion,
nonInteractive: flags.nonInteractive,
buildProfile,
env: buildProfile.profile.env,
});
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
@@ -530,7 +534,6 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
buildProfile,
nonInteractive,
sdkVersion,
env,
}: {
exp: ExpoConfig;
projectId: string;
@@ -539,7 +542,6 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
buildProfile: ProfileData<'build'>;
nonInteractive: boolean;
sdkVersion?: string;
env: Env | undefined;
}): Promise<void> {
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
return;
@@ -567,7 +569,6 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
projectDir,
platform: RequestedPlatform.All,
vcsClient,
env,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
@@ -51,6 +51,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
value: build.releaseChannel,
},
{
label: 'Channel',
value: build.channel,
@@ -1,6 +1,5 @@
import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
@@ -211,13 +210,7 @@ export default abstract class EasCommand extends Command {
const defaultMsg = `${messageLine}${requestIdLine}`;
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nThis can sometimes happen if you are logged in as incorrect user.\nRun ${chalk.bold(
'eas whoami'
)} to check the username you are logged in as.\nRun ${chalk.bold(
'eas login'
)} to change the account.\n\nOriginal error message: ${defaultMsg}`;
return `You don't have the required permissions to perform this operation.\n\n${defaultMsg}`;
}
return defaultMsg;
@@ -55,7 +55,6 @@ describe(getProjectIdAsync, () => {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
@@ -15,11 +15,7 @@ async function applyCliConfigAsync(projectDir: string): Promise<void> {
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json (${
config.version
}).\n\nThis error probably means that you need update your eas-cli to a newer version.\nRun ${chalk.bold(
'npm install -g eas-cli'
)} to update the eas-cli to the latest version.`
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
);
}
}
@@ -3,7 +3,7 @@ import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
import { createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
@@ -87,21 +87,6 @@ export async function getProjectIdAsync(
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const projectId = await validateOrSetProjectIdAsync({ exp, graphqlClient, actor, options });
return projectId;
}
export async function validateOrSetProjectIdAsync({
exp,
graphqlClient,
actor,
options,
}: {
exp: ExpoConfig;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options: { env?: Env; nonInteractive: boolean };
}): Promise<string> {
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
if (typeof localProjectId !== 'string') {
@@ -13,6 +13,7 @@ export default class AccountLogin extends EasCommand {
description: 'Login with SSO',
char: 's',
default: false,
hidden: true,
}),
};
@@ -74,20 +74,14 @@ export default class BuildConfigure extends EasCommand {
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync({ projectDir, exp, workflow, env: undefined });
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
}
}
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync({
vcsClient,
projectDir,
exp,
workflow,
env: undefined,
});
await syncIosUpdatesConfigurationAsync(vcsClient, projectDir, exp);
}
}
}
@@ -35,7 +35,6 @@ interface RawBuildFlags {
'resource-class'?: ResourceClass;
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
}
export default class Build extends EasCommand {
@@ -105,10 +104,6 @@ export default class Build extends EasCommand {
description: 'The level of logs to output during the build process. Defaults to "info".',
options: Object.values(LoggerLevel),
}),
'freeze-credentials': Flags.boolean({
default: false,
description: 'Prevent the build from updating credentials in non-interactive mode',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -222,7 +217,6 @@ export default class Build extends EasCommand {
resourceClass: flags['resource-class'],
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
};
}
@@ -105,7 +105,6 @@ export default class BuildInspect extends EasCommand {
projectDir,
{
nonInteractive: false,
freezeCredentials: false,
wait: true,
clearCache: false,
json: false,
@@ -77,7 +77,6 @@ export default class BuildInternal extends EasCommand {
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
freezeCredentials: false,
wait: false,
clearCache: false,
json: true,
@@ -88,7 +88,6 @@ export default class BuildVersionGetView extends EasCommand {
buildProfile: profile,
platform,
vcsClient,
nonInteractive: flags['non-interactive'],
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -77,7 +77,6 @@ export default class BuildVersionSetView extends EasCommand {
buildProfile: profile,
platform,
vcsClient,
nonInteractive: false,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -102,7 +102,6 @@ export default class BuildVersionSyncView extends EasCommand {
buildProfile: profileInfo.profile,
platform: profileInfo.platform,
vcsClient,
nonInteractive: false,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -125,7 +125,6 @@ export default class ChannelRollout extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.Vcs,
...this.ContextOptions.LoggedIn,
};
@@ -134,7 +133,6 @@ export default class ChannelRollout extends EasCommand {
const argsAndFlags = this.sanitizeArgsAndFlags({ ...flags, ...args });
const {
privateProjectConfig: { exp, projectId, projectDir },
vcsClient,
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelRollout, {
nonInteractive: argsAndFlags.nonInteractive,
@@ -148,7 +146,6 @@ export default class ChannelRollout extends EasCommand {
nonInteractive: argsAndFlags.nonInteractive,
graphqlClient,
app,
vcsClient,
};
if (argsAndFlags.nonInteractive) {
await new NonInteractiveRollout(argsAndFlags).runAsync(ctx);
@@ -58,8 +58,7 @@ export default class InitializeBuildCredentials extends EasCommand {
privateProjectConfig ?? null,
getDynamicPrivateProjectConfigAsync,
platform,
buildProfile,
process.cwd()
buildProfile
).runAsync();
}
}
@@ -1,255 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { getExpoWebsiteBaseUrl } from '../../api';
import { reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { validateOrSetProjectIdAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import { CredentialsContextProjectInfo } from '../../credentials/context';
import { SetUpBuildCredentialsCommandAction } from '../../credentials/manager/SetUpBuildCredentialsCommandAction';
import { AppPlatform, OnboardingDeviceType, OnboardingEnvironment } from '../../graphql/generated';
import { UserPreferencesMutation } from '../../graphql/mutations/UserPreferencesMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { link } from '../../log';
import { runGitCloneAsync, runGitPushAsync } from '../../onboarding/git';
import { installDependenciesAsync } from '../../onboarding/installDependencies';
import { ExpoConfigOptions, getPrivateExpoConfig } from '../../project/expoConfig';
import { confirmAsync, promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import GitClient from '../../vcs/clients/git';
export default class Onboarding extends EasCommand {
static override hidden = true;
static override aliases = ['init:onboarding', 'onboarding'];
static override description = 'continue onboarding process started on the expo.dev website';
static override flags = {};
static override args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
const {
args: { TARGET_PROJECT_DIRECTORY: targetProjectDirInput },
} = await this.parse(Onboarding);
const {
loggedIn: { actor, graphqlClient },
analytics,
} = await this.getContextAsync(Onboarding, {
nonInteractive: false,
});
if (actor.__typename === 'Robot') {
throw new Error(
'This command is not available for robot users. Make sure you are not using a robot token and try again.'
);
}
if (!actor.preferences.onboarding) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. Visit https://expo.new to start a new project.'
);
}
if (!actor.preferences.onboarding.platform) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected platform). Continue the onboarding process on the Expo website.'
);
}
if (!actor.preferences.onboarding.environment) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected environment). Continue the onboarding process on the Expo website.'
);
}
if (!actor.preferences.onboarding.deviceType) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected device type). Continue the onboarding process on the Expo website.'
);
}
if (
new Date(actor.preferences.onboarding.lastUsed) < new Date(Date.now() - 1000 * 60 * 60 * 24)
) {
Log.warn(
'It seems like you started an onboarding process, but it has been a while since you last used it. If you want to start a new onboarding process, visit https://expo.new.'
);
Log.log();
return;
}
const platform =
actor.preferences.onboarding.platform === AppPlatform.Android
? Platform.ANDROID
: Platform.IOS;
const app = await AppQuery.byIdAsync(graphqlClient, actor.preferences.onboarding.appId);
const githubUsername = app.githubRepository
? app.githubRepository.metadata.githubRepoOwnerName
: 'expo';
const githubRepositoryName = app.githubRepository
? app.githubRepository.metadata.githubRepoName
: 'expo-default-template';
const initialTargetProjectDir = targetProjectDirInput ?? `./${githubRepositoryName}`;
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
Log.log();
Log.log('✨ We will continue your onboarding process in EAS CLI');
Log.log();
Log.log(
`🚚 Let's start by cloning ${
app.githubRepository
? `your project (${githubUsername}/${githubRepositoryName})`
: `default Expo template project (${githubUsername}/${githubRepositoryName})`
} from GitHub and installing dependencies.`
);
Log.log();
const shouldContinueWithDefaultTargetDirectory = await confirmAsync({
message: `Do you want to clone your project into ${initialTargetProjectDir}?`,
});
let targetProjectDir = initialTargetProjectDir;
if (!shouldContinueWithDefaultTargetDirectory) {
const { newTargetProjectDir } = await promptAsync({
type: 'text',
name: 'newTargetProjectDir',
message: 'Provide a new target directory path:',
validate: (input: string) => input !== '',
});
targetProjectDir = newTargetProjectDir;
}
Log.log();
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
githubUsername,
githubRepositoryName,
targetProjectDir,
});
const vcsClient = new GitClient(finalTargetProjectDirectory);
await installDependenciesAsync({
projectDir: finalTargetProjectDirectory,
});
await vcsClient.trackFileAsync('package-lock.json');
const shouldSetupCredentials =
actor.preferences.onboarding.deviceType === OnboardingDeviceType.Device &&
actor.preferences.onboarding.environment === OnboardingEnvironment.DevBuild;
if (shouldSetupCredentials) {
Log.log('🔑 Now we need to set up build credentials for your project:');
await new SetUpBuildCredentialsCommandAction(
actor,
graphqlClient,
vcsClient,
analytics,
await getPrivateExpoConfigWithProjectIdAsync({
projectDir: finalTargetProjectDirectory,
graphqlClient,
actor,
}),
getDynamicPrivateProjectConfigGetter({
projectDir: finalTargetProjectDirectory,
graphqlClient,
actor,
}),
platform,
'development',
finalTargetProjectDirectory
).runAsync();
}
if (await vcsClient.hasUncommittedChangesAsync()) {
Log.log(
'📦 We will now commit the changes made by the configuration process and push them to GitHub:'
);
Log.log();
Log.log('🔍 Checking for changes in the repository...');
await vcsClient.showChangedFilesAsync();
await reviewAndCommitChangesAsync(
vcsClient,
`[eas-onboarding] Install dependencies${
shouldSetupCredentials ? 'and set up build credentials' : ''
}`,
{ nonInteractive: false }
);
Log.log('📤 Pushing changes to GitHub...');
await runGitPushAsync({
targetProjectDir: finalTargetProjectDirectory,
});
}
const { __typename, ...previousPreferences } = actor.preferences.onboarding;
await UserPreferencesMutation.markCliDoneInOnboardingUserPreferencesAsync(graphqlClient, {
...previousPreferences,
appId: app.id,
});
Log.log();
Log.log('🎉 We finished configuring your project.');
Log.log();
Log.log('🚀 You can now go back to the website to continue:');
const url = new URL(
`/onboarding/develop/set-up-project-on-your-machine?project=${app.slug}&accountId=${app.ownerAccount.id}`,
getExpoWebsiteBaseUrl()
).toString();
Log.log(`👉 ${link(url)}`);
}
}
// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
async function getPrivateExpoConfigWithProjectIdAsync({
projectDir,
graphqlClient,
actor,
options,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options?: ExpoConfigOptions;
}): Promise<CredentialsContextProjectInfo> {
const expBefore = getPrivateExpoConfig(projectDir, options);
const projectId = await validateOrSetProjectIdAsync({
exp: expBefore,
graphqlClient,
actor,
options: {
nonInteractive: false,
},
});
const exp = getPrivateExpoConfig(projectDir, options);
return {
exp,
projectId,
};
}
// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
function getDynamicPrivateProjectConfigGetter({
projectDir,
graphqlClient,
actor,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
}): DynamicConfigContextFn {
return async (options?: ExpoConfigOptions) => {
return {
...(await getPrivateExpoConfigWithProjectIdAsync({
projectDir,
graphqlClient,
actor,
options,
})),
projectDir,
};
};
}
@@ -52,7 +52,6 @@ export default class UpdateConfigure extends EasCommand {
projectDir,
platform,
vcsClient,
env: undefined,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
+13 -10
View File
@@ -1,5 +1,4 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -7,6 +6,7 @@ import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
@@ -40,7 +40,6 @@ import {
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
@@ -200,7 +199,6 @@ export default class UpdatePublish extends EasCommand {
projectDir,
projectId,
vcsClient,
env: undefined,
});
const { exp } = await getDynamicPublicProjectConfigAsync();
@@ -361,24 +359,29 @@ export default class UpdatePublish extends EasCommand {
throw e;
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersions = await getRuntimeVersionObjectAsync({
exp,
platforms: realizedPlatforms,
projectDir,
workflows: {
...workflows,
web: Workflow.UNKNOWN,
},
env: undefined,
vcsClient,
});
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
Log.withTick(
`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`
);
}
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
@@ -1,10 +1,11 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
@@ -32,7 +33,6 @@ import {
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
@@ -154,7 +154,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
projectDir,
projectId,
vcsClient,
env: undefined,
});
// check that the expo-updates package version supports roll back to embedded
@@ -185,24 +184,28 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const realizedPlatforms: PublishPlatform[] =
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersions = await getRuntimeVersionObjectAsync({
exp,
platforms: realizedPlatforms,
projectDir,
workflows: {
...workflows,
web: Workflow.UNKNOWN,
},
env: undefined,
vcsClient,
});
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
@@ -1,5 +1,5 @@
import AppStoreApi from '../ios/appstore/AppStoreApi';
import { AuthCtx, AuthenticationMode } from '../ios/appstore/authenticateTypes';
import { AuthCtx } from '../ios/appstore/authenticateTypes';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
@@ -9,7 +9,6 @@ export const testAuthCtx: AuthCtx = {
export function getAppstoreMock(): AppStoreApi {
return {
defaultAuthenticationMode: AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(),
ensureBundleIdExistsAsync: jest.fn(),
listDistributionCertificatesAsync: jest.fn(),
@@ -1,5 +1,3 @@
import assert from 'node:assert';
import { AppFragment, Role } from '../../graphql/generated';
import { Actor } from '../../user/User';
@@ -26,7 +24,6 @@ export const jester: Actor = {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};
export const jester2 = {
@@ -44,7 +41,6 @@ export const jester2 = {
featureGates: {},
};
assert(jester.__typename === 'User');
export const testUsername = jester.username;
export const testSlug = 'testApp';
export const testProjectId = '7ef93448-3bc7-4b57-be32-99326dcf24f0';
@@ -4,76 +4,27 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
GoogleServiceAccountKeyByAccountQuery,
GoogleServiceAccountKeyFragment,
GoogleServiceAccountKeysPaginatedByAccountQuery,
} from '../../../../../graphql/generated';
import { GoogleServiceAccountKeyFragmentNode } from '../../../../../graphql/types/credentials/GoogleServiceAccountKey';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const GoogleServiceAccountKeyQuery = {
async getAllForAccountAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<GoogleServiceAccountKeyFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<GoogleServiceAccountKeyFragment>> => {
return await GoogleServiceAccountKeyQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching Google Service Account Keys...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<
GoogleServiceAccountKeysPaginatedByAccountQuery['account']['byName']['googleServiceAccountKeysPaginated']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GoogleServiceAccountKeysPaginatedByAccountQuery>(
.query<GoogleServiceAccountKeyByAccountQuery>(
gql`
query GoogleServiceAccountKeysPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
query GoogleServiceAccountKeyByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
googleServiceAccountKeysPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...GoogleServiceAccountKeyFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
googleServiceAccountKeys {
id
...GoogleServiceAccountKeyFragment
}
}
}
@@ -82,10 +33,6 @@ export const GoogleServiceAccountKeyQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['GoogleServiceAccountKey'],
@@ -93,6 +40,6 @@ export const GoogleServiceAccountKeyQuery = {
)
.toPromise()
);
return data.account.byName.googleServiceAccountKeysPaginated;
return data.account.byName.googleServiceAccountKeys;
},
};
@@ -25,7 +25,6 @@ export class CredentialsContext {
public readonly appStore = new AppStoreApi();
public readonly ios = IosGraphqlClient;
public readonly nonInteractive: boolean;
public readonly freezeCredentials: boolean = false;
public readonly projectDir: string;
public readonly user: Actor;
public readonly graphqlClient: ExpoGraphqlClient;
@@ -48,7 +47,6 @@ export class CredentialsContext {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
freezeCredentials?: boolean;
env?: Env;
}
) {
@@ -60,7 +58,6 @@ export class CredentialsContext {
this.vcsClient = options.vcsClient;
this.nonInteractive = options.nonInteractive ?? false;
this.projectInfo = options.projectInfo;
this.freezeCredentials = options.freezeCredentials ?? false;
}
get hasProjectContext(): boolean {
@@ -1,31 +1,9 @@
import { learnMore } from '../log';
export class MissingCredentialsNonInteractiveError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up. Run this command again in interactive mode.');
}
}
export class InsufficientAuthenticationNonInteractiveError extends Error {
constructor(message?: string) {
super(
message ??
`Authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
}
export class ForbidCredentialModificationError extends Error {
constructor(message?: string) {
super(
message ??
'Credentials cannot be modified. Run this command again without the --freeze-credentials flag.'
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
@@ -14,7 +14,6 @@ import { isAdHocProfile, isEnterpriseUniversalProfile } from './utils/provisioni
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
import { findApplicationTarget } from '../../project/ios/target';
import { isExpoNotificationsInstalled } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
@@ -106,11 +105,6 @@ export default class IosCredentialsProvider {
if (ctx.easJsonCliConfig?.promptToConfigurePushNotifications === false) {
return null;
} else if (
ctx.easJsonCliConfig?.promptToConfigurePushNotifications === undefined &&
!(await isExpoNotificationsInstalled(ctx.projectDir))
) {
return null;
}
const setupOption = await selectAsync(
@@ -5,18 +5,15 @@ import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
} from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { CredentialsContext } from '../../context';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
import { AuthCtx } from '../appstore/authenticateTypes';
import { Target } from '../types';
export class ConfigureProvisioningProfile {
@@ -30,18 +27,9 @@ export class ConfigureProvisioningProfile {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleProvisioningProfileMutationResult | null> {
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Remove the --freeze-credentials flag to configure a Provisioning Profile.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Configuring Provisioning Profiles is only supported in interactive mode.'
);
}
const { developerPortalIdentifier, provisioningProfile } = this.originalProvisioningProfile;
@@ -58,7 +46,7 @@ export class ConfigureProvisioningProfile {
return null;
}
const applePlatform = getApplePlatformFromTarget(this.target);
const applePlatform = await getApplePlatformFromTarget(this.target);
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier,
applePlatform
@@ -4,17 +4,14 @@ import nullthrows from 'nullthrows';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { generateProvisioningProfileAsync } from './ProvisioningProfileUtils';
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfile } from '../appstore/Credentials.types';
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
import { AuthCtx } from '../appstore/authenticateTypes';
import { provisioningProfileSchema } from '../credentials';
import { Target } from '../types';
@@ -26,18 +23,9 @@ export class CreateProvisioningProfile {
) {}
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Run this command again without the --freeze-credentials flag in order to generate a new Provisioning Profile.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to generate a new Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Creating Provisioning Profiles is only supported in interactive mode.'
);
}
const appleAuthCtx = await ctx.appStore.ensureAuthenticatedAsync();
@@ -61,31 +49,16 @@ export class CreateProvisioningProfile {
return provisioningProfileMutationResult;
}
private async maybeGetUserProvidedAsync(
ctx: CredentialsContext
): Promise<ProvisioningProfile | null> {
if (ctx.nonInteractive) {
return null;
}
private async provideOrGenerateAsync(
ctx: CredentialsContext,
appleAuthCtx: AuthCtx
): Promise<ProvisioningProfile> {
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
if (userProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
Log.warn('Provisioning profile: Unable to validate specified profile.');
return userProvided;
}
return null;
}
private async provideOrGenerateAsync(
ctx: CredentialsContext,
appleAuthCtx: AuthCtx
): Promise<ProvisioningProfile> {
const maybeUserProvided = await this.maybeGetUserProvidedAsync(ctx);
if (maybeUserProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
Log.warn('Provisioning profile: Unable to validate specified profile.');
return maybeUserProvided;
}
assert(
this.distributionCertificate.certificateP12,
'Distribution Certificate must have p12 certificate'
@@ -48,13 +48,8 @@ export function formatDistributionCertificate(
buildCredentials => buildCredentials.iosAppCredentials.app
);
if (apps.length) {
// iosAppBuildCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
line += chalk.gray(`\n 📲 Used by: ${appFullNames}${andMaybeMore}`);
const appFullNames = apps.map(app => app.fullName).join(',');
line += chalk.gray(`\n 📲 Used by: ${appFullNames}`);
}
if (validSerialNumbers?.includes(serialNumber)) {
@@ -259,10 +254,6 @@ function formatDistributionCertificateFromApple(
const { name, status, id, expires, created, ownerName, serialNumber } = appleInfo;
const expiresDate = new Date(expires * 1000).toDateString();
const createdDate = new Date(created * 1000).toDateString();
return [
`🍏 ${chalk.bold(name)} (${status})`,
`${chalk.bold('ID:')} ${id} ${chalk.bold('Serial Number:')} ${serialNumber}`,
`${chalk.bold('Apple Team:')} ${appleInfo.ownerId} (${ownerName})`,
`${chalk.bold('Expires:')} ${expiresDate} ${chalk.bold('Created:')} ${createdDate}`,
].join('\n\t');
return `${name} (${status}) - Cert ID: ${id}, Serial number: ${serialNumber}, Team ID: ${appleInfo.ownerId}, Team name: ${ownerName}
expires: ${expiresDate}, created: ${createdDate}`;
}
@@ -185,13 +185,8 @@ export function formatPushKey(
const apps = pushKey.iosAppCredentialsList.map(appCredentials => appCredentials.app);
if (apps.length) {
// iosAppCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
line += chalk.gray(`\n 📲 Used by: ${appFullNames}${andMaybeMore}`);
const appFullNames = apps.map(app => app.fullName).join(',');
line += chalk.gray(`\n 📲 Used by: ${appFullNames}`);
}
if (validPushKeyIdentifiers?.includes(keyIdentifier)) {
@@ -34,20 +34,14 @@ export class RemoveDistributionCertificate {
buildCredentials => buildCredentials.iosAppCredentials.app
);
if (apps.length !== 0) {
// iosAppBuildCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
const appFullNames = apps.map(app => app.fullName).join(',');
if (ctx.nonInteractive) {
throw new Error(
`Certificate is currently used by ${appFullNames}${andMaybeMore} and cannot be deleted in non-interactive mode.`
`Certificate is currently used by ${appFullNames} and cannot be deleted in non-interactive mode.`
);
}
const confirm = await confirmAsync({
message: `You are removing certificate used by ${appFullNames}${andMaybeMore}. Do you want to continue?`,
message: `You are removing certificate used by ${appFullNames}. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
@@ -27,14 +27,9 @@ export class RemovePushKey {
const apps = this.pushKey.iosAppCredentialsList.map(appCredentials => appCredentials.app);
if (apps.length !== 0) {
// iosAppCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
const appFullNames = apps.map(app => app.fullName).join(',');
const confirm = await confirmAsync({
message: `Removing this push key will disable push notifications for ${appFullNames}${andMaybeMore}. Do you want to continue?`,
message: `Removing this push key will disable push notifications for ${appFullNames}. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
@@ -15,17 +15,12 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../graphql/generated';
import { learnMore } from '../../../log';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthenticationMode } from '../appstore/authenticateTypes';
import { Target } from '../types';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
@@ -107,18 +102,9 @@ export class SetUpProvisioningProfile {
if (areBuildCredentialsSetup) {
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, this.distributionType));
}
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Provisioning profile is not configured correctly. Remove the --freeze-credentials flag to configure it.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Provisioning profile is not configured correctly. Run this command again in interactive mode.'
);
}
@@ -141,18 +127,11 @@ export class SetUpProvisioningProfile {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
const isNonInteractiveOrUserDidConfirmAsync = async (): Promise<boolean> => {
if (ctx.nonInteractive) {
return true;
}
return await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
};
const confirm = await isNonInteractiveOrUserDidConfirmAsync();
const confirm = await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
if (!confirm) {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
@@ -11,11 +11,7 @@ import {
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../../errors';
import { AuthenticationMode } from '../../appstore/authenticateTypes';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
@@ -28,18 +24,15 @@ describe('ConfigureProvisioningProfile', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
const testCases = ['NON_INTERACTIVE', 'INTERACTIVE'];
test.each(testCases)('configures a Provisioning Profile in %s Mode', async mode => {
it('configures a Provisioning Profile in Interactive Mode', async () => {
const mockProvisioningProfileFromApple = {
provisioningProfileId: testProvisioningProfileFragment.developerPortalIdentifier,
provisioningProfile: testProvisioningProfileFragment.provisioningProfile,
};
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
nonInteractive: false,
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
@@ -64,30 +57,7 @@ describe('ConfigureProvisioningProfile', () => {
// expect provisioning profile not to be updated on apple portal
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors with --freeze-credentials flag', async () => {
const ctx = createCtxMock({
freezeCredentials: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const provProfConfigurator = new ConfigureProvisioningProfile(
appLookupParams,
testTarget,
testDistCertFragmentNoDependencies,
testProvisioningProfileFragment
);
await expect(provProfConfigurator.runAsync(ctx)).rejects.toThrowError(
ForbidCredentialModificationError
);
// expect provisioning profile not to be updated on expo servers
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('errors with wrong authentication type in nonInteractive mode', async () => {
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -102,7 +72,7 @@ describe('ConfigureProvisioningProfile', () => {
testProvisioningProfileFragment
);
await expect(provProfConfigurator.runAsync(ctx)).rejects.toThrowError(
InsufficientAuthenticationNonInteractiveError
MissingCredentialsNonInteractiveError
);
// expect provisioning profile not to be updated on expo servers
@@ -10,11 +10,7 @@ import {
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../../errors';
import { AuthenticationMode } from '../../appstore/authenticateTypes';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
@@ -26,15 +22,11 @@ describe('CreateProvisioningProfile', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
const testCases = ['NON_INTERACTIVE', 'INTERACTIVE'];
test.each(testCases)('creates a Provisioning Profile in in %s Mode', async mode => {
it('creates a Provisioning Profile in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
nonInteractive: false,
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
@@ -56,29 +48,7 @@ describe('CreateProvisioningProfile', () => {
// expect provisioning profile to be created on apple portal
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors with --freeze-credentials flag', async () => {
const ctx = createCtxMock({
freezeCredentials: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const createProvProfAction = new CreateProvisioningProfile(
appLookupParams,
testTarget,
testDistCertFragmentNoDependencies
);
await expect(createProvProfAction.runAsync(ctx)).rejects.toThrowError(
ForbidCredentialModificationError
);
// expect provisioning profile not to be created on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be created on apple portal
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('errors with wrong authentication type in nonInteractive mode', async () => {
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -92,7 +62,7 @@ describe('CreateProvisioningProfile', () => {
testDistCertFragmentNoDependencies
);
await expect(createProvProfAction.runAsync(ctx)).rejects.toThrowError(
InsufficientAuthenticationNonInteractiveError
MissingCredentialsNonInteractiveError
);
// expect provisioning profile not to be created on expo servers
@@ -15,11 +15,7 @@ import {
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../../errors';
import { AuthenticationMode } from '../../appstore/authenticateTypes';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
@@ -36,185 +32,32 @@ describe('SetUpProvisioningProfile', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
const testCases = ['NON_INTERACTIVE', 'INTERACTIVE'];
test.each(testCases)(
'repairs existing Provisioning Profile with bad build credentials in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => [
{
provisioningProfileId: nullthrows(
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0]
.provisioningProfile
).developerPortalIdentifier,
},
]),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
() => testIosAppBuildCredentialsFragment
),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
}
);
test.each(testCases)(
'sets up a new Provisioning Profile with bad build credentials in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
}
);
test.each(testCases)(
'skips setting up a Provisioning Profile with prior build credentials configured properly in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
}
);
test.each(testCases)(
'sets up a Provisioning Profile with no prior build credentials configured in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
}
);
it('errors with --freeze-credentials flag', async () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
freezeCredentials: true,
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => [
{
provisioningProfileId: nullthrows(
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0].provisioningProfile
).developerPortalIdentifier,
},
]),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
() => testIosAppBuildCredentialsFragment
),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
@@ -225,9 +68,79 @@ describe('SetUpProvisioningProfile', () => {
testTarget,
IosDistributionType.AppStore
);
await expect(setupProvisioningProfileAction.runAsync(ctx)).rejects.toThrowError(
ForbidCredentialModificationError
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
//listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
@@ -236,8 +149,39 @@ describe('SetUpProvisioningProfile', () => {
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
it('errors with wrong authentication type in nonInteractive mode', async () => {
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
@@ -252,7 +196,7 @@ describe('SetUpProvisioningProfile', () => {
IosDistributionType.AppStore
);
await expect(setupProvisioningProfileAction.runAsync(ctx)).rejects.toThrowError(
InsufficientAuthenticationNonInteractiveError
MissingCredentialsNonInteractiveError
);
// expect build credentials not to be created or updated on expo servers
@@ -326,7 +326,7 @@ export async function getDevicesForAppleTeamAsync(
): Promise<AppleDeviceFragment[]> {
return await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(
graphqlClient,
account.name,
account.id,
appleTeamIdentifier,
{
useCache,
@@ -4,76 +4,27 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyByAccountQuery,
AppStoreConnectApiKeyFragment,
AppStoreConnectApiKeysPaginatedByAccountQuery,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const AppStoreConnectApiKeyQuery = {
async getAllForAccountAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<AppStoreConnectApiKeyFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppStoreConnectApiKeyFragment>> => {
return await AppStoreConnectApiKeyQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching ASC Keys...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<
AppStoreConnectApiKeysPaginatedByAccountQuery['account']['byName']['appStoreConnectApiKeysPaginated']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppStoreConnectApiKeysPaginatedByAccountQuery>(
.query<AppStoreConnectApiKeyByAccountQuery>(
gql`
query AppStoreConnectApiKeysPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
query AppStoreConnectApiKeyByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
appStoreConnectApiKeysPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...AppStoreConnectApiKeyFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
appStoreConnectApiKeys {
id
...AppStoreConnectApiKeyFragment
}
}
}
@@ -82,10 +33,6 @@ export const AppStoreConnectApiKeyQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['AppStoreConnectApiKey'],
@@ -93,6 +40,6 @@ export const AppStoreConnectApiKeyQuery = {
)
.toPromise()
);
return data.account.byName.appStoreConnectApiKeysPaginated;
return data.account.byName.appStoreConnectApiKeys;
},
};
@@ -1,3 +1,4 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
@@ -6,16 +7,15 @@ import { DeviceNotFoundError } from '../../../../../devices/utils/errors';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppleDevice,
AppleDeviceFilterInput,
AppleDeviceFragment,
AppleDevicesByAppleTeamQuery,
AppleDevicesByIdentifierQuery,
AppleDevicesByTeamIdentifierQuery,
AppleDevicesByTeamIdentifierQueryVariables,
AppleDevicesPaginatedByAccountQuery,
AppleTeamFragment,
} from '../../../../../graphql/generated';
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
import { formatAppleTeam } from '../../../actions/AppleTeamFormatting';
export type AppleDeviceFragmentWithAppleTeam = AppleDeviceFragment & {
@@ -38,25 +38,51 @@ export type AppleDevicesByIdentifierQueryResult = AppleDeviceQueryResult & {
export const AppleDeviceQuery = {
async getAllByAppleTeamIdentifierAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
accountId: string,
appleTeamIdentifier: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<AppleDeviceFragmentWithAppleTeam[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppleDeviceFragmentWithAppleTeam>> => {
return await AppleDeviceQuery.getAllForAccountPaginatedAsync(graphqlClient, accountName, {
...relayArgs,
filter: {
appleTeamIdentifier,
},
useCache,
});
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'Fetching Apple devices...',
});
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesByAppleTeamQuery>(
gql`
query AppleDevicesByAppleTeamQuery($accountId: ID!, $appleTeamIdentifier: String!) {
appleTeam {
byAppleTeamIdentifier(accountId: $accountId, identifier: $appleTeamIdentifier) {
id
...AppleTeamFragment
appleDevices {
id
...AppleDeviceFragment
appleTeam {
id
...AppleTeamFragment
}
}
}
}
}
${print(AppleTeamFragmentNode)}
${print(AppleDeviceFragmentNode)}
`,
{
accountId,
appleTeamIdentifier,
},
{
additionalTypenames: ['AppleDevice'],
requestPolicy: useCache ? 'cache-first' : 'network-only',
}
)
.toPromise()
);
assert(
data.appleTeam.byAppleTeamIdentifier,
'byAppleTeamIdentifier should be defined in this context - enforced by GraphQL'
);
const { appleDevices } = data.appleTeam.byAppleTeamIdentifier;
assert(appleDevices, 'Apple Devices should be defined in this context - enforced by GraphQL');
return appleDevices;
},
async getAllForAppleTeamAsync(
@@ -113,21 +139,41 @@ export const AppleDeviceQuery = {
graphqlClient: ExpoGraphqlClient,
accountName: string,
identifier: string
): Promise<AppleDeviceFragmentWithAppleTeam> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppleDeviceFragmentWithAppleTeam>> => {
return await AppleDeviceQuery.getAllForAccountPaginatedAsync(graphqlClient, accountName, {
...relayArgs,
filter: {
identifier,
},
});
};
const devices = await fetchEntireDatasetAsync({
paginatedGetterAsync,
});
const device = devices[0];
): Promise<AppleDevicesByIdentifierQueryResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesByIdentifierQuery>(
gql`
query AppleDevicesByIdentifier($accountName: String!, $identifier: String!) {
account {
byName(accountName: $accountName) {
id
appleDevices(identifier: $identifier) {
id
model
identifier
name
deviceClass
enabled
appleTeam {
id
appleTeamIdentifier
appleTeamName
}
}
}
}
}
`,
{ accountName, identifier },
{
additionalTypenames: ['AppleDevice', 'AppleTeam'],
}
)
.toPromise()
);
const device = data.account.byName.appleDevices[0];
if (!device) {
throw new DeviceNotFoundError(
`Device with id ${identifier} was not found on account ${accountName}.`
@@ -135,86 +181,4 @@ export const AppleDeviceQuery = {
}
return device;
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
filter,
useCache = true,
}: {
after?: string;
first?: number;
before?: string;
last?: number;
filter?: AppleDeviceFilterInput;
useCache?: boolean;
}
): Promise<AppleDevicesPaginatedByAccountQuery['account']['byName']['appleDevicesPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesPaginatedByAccountQuery>(
gql`
query AppleDevicesPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
$filter: AppleDeviceFilterInput
) {
account {
byName(accountName: $accountName) {
id
appleDevicesPaginated(
after: $after
first: $first
before: $before
last: $last
filter: $filter
) {
edges {
cursor
node {
id
...AppleDeviceFragment
appleTeam {
id
...AppleTeamFragment
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
}
${print(AppleTeamFragmentNode)}
${print(AppleDeviceFragmentNode)}
`,
{
accountName,
after,
first,
before,
last,
filter,
},
{
additionalTypenames: ['AppleDevice'],
requestPolicy: useCache ? 'cache-first' : 'network-only',
}
)
.toPromise()
);
return data.account.byName.appleDevicesPaginated;
},
};
@@ -5,14 +5,13 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppleDistributionCertificateByAccountQuery,
AppleDistributionCertificateByAppQuery,
AppleDistributionCertificateFragment,
AppleDistributionCertificatesPaginatedByAccountQuery,
IosDistributionType,
} from '../../../../../graphql/generated';
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const AppleDistributionCertificateQuery = {
async getForAppAsync(
@@ -82,65 +81,17 @@ export const AppleDistributionCertificateQuery = {
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<AppleDistributionCertificateFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppleDistributionCertificateFragment>> => {
return await AppleDistributionCertificateQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching Apple Distribution Certificates...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<
AppleDistributionCertificatesPaginatedByAccountQuery['account']['byName']['appleDistributionCertificatesPaginated']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDistributionCertificatesPaginatedByAccountQuery>(
.query<AppleDistributionCertificateByAccountQuery>(
gql`
query AppleDistributionCertificatesPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
query AppleDistributionCertificateByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
appleDistributionCertificatesPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...AppleDistributionCertificateFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
appleDistributionCertificates {
id
...AppleDistributionCertificateFragment
}
}
}
@@ -149,10 +100,6 @@ export const AppleDistributionCertificateQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['AppleDistributionCertificate'],
@@ -160,6 +107,6 @@ export const AppleDistributionCertificateQuery = {
)
.toPromise()
);
return data.account.byName.appleDistributionCertificatesPaginated;
return data.account.byName.appleDistributionCertificates;
},
};
@@ -3,75 +3,25 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
ApplePushKeyFragment,
ApplePushKeysPaginatedByAccountQuery,
} from '../../../../../graphql/generated';
import { ApplePushKeyByAccountQuery, ApplePushKeyFragment } from '../../../../../graphql/generated';
import { ApplePushKeyFragmentNode } from '../../../../../graphql/types/credentials/ApplePushKey';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const ApplePushKeyQuery = {
async getAllForAccountAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<ApplePushKeyFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<ApplePushKeyFragment>> => {
return await ApplePushKeyQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching Apple Push Keys...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<ApplePushKeysPaginatedByAccountQuery['account']['byName']['applePushKeysPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ApplePushKeysPaginatedByAccountQuery>(
.query<ApplePushKeyByAccountQuery>(
gql`
query ApplePushKeysPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
query ApplePushKeyByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
applePushKeysPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...ApplePushKeyFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
applePushKeys {
id
...ApplePushKeyFragment
}
}
}
@@ -80,10 +30,6 @@ export const ApplePushKeyQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['ApplePushKey'],
@@ -91,6 +37,6 @@ export const ApplePushKeyQuery = {
)
.toPromise()
);
return data.account.byName.applePushKeysPaginated;
return data.account.byName.applePushKeys;
},
};
@@ -18,18 +18,13 @@ export class SetUpBuildCredentialsCommandAction {
public readonly projectInfo: CredentialsContextProjectInfo | null,
public readonly getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn,
private readonly platform: Platform,
private readonly profileName: string,
private readonly projectDir: string
private readonly profileName: string
) {}
async runAsync(): Promise<void> {
if (this.platform === Platform.IOS) {
return await new SetUpIosBuildCredentials(this, this.projectDir, this.profileName).runAsync();
return await new SetUpIosBuildCredentials(this, process.cwd(), this.profileName).runAsync();
}
return await new SetUpAndroidBuildCredentials(
this,
this.projectDir,
this.profileName
).runAsync();
return await new SetUpAndroidBuildCredentials(this, process.cwd(), this.profileName).runAsync();
}
}
@@ -38,7 +38,7 @@ export class SetUpIosBuildCredentials extends ManageIos {
}
const ctx = new CredentialsContext({
projectDir: this.projectDir,
projectDir: process.cwd(),
projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
@@ -44,7 +44,6 @@ describe(AccountResolver, () => {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};
describe('when inside project dir', () => {
@@ -34,7 +34,7 @@ export default class DeviceCreateAction {
await runDeveloperPortalMethodAsync(
this.graphqlClient,
this.appStoreApi,
this.account,
this.account.id,
this.appleTeam
);
} else if (method === RegistrationMethod.INPUT) {
@@ -9,7 +9,7 @@ import {
import AppStoreApi from '../../../credentials/ios/appstore/AppStoreApi';
import { getRequestContext } from '../../../credentials/ios/appstore/authenticate';
import { AuthCtx } from '../../../credentials/ios/appstore/authenticateTypes';
import { AccountFragment, AppleDeviceClass, AppleTeam } from '../../../graphql/generated';
import { AppleDeviceClass, AppleTeam } from '../../../graphql/generated';
import Log from '../../../log';
import { ora } from '../../../ora';
import { promptAsync } from '../../../prompts';
@@ -25,14 +25,14 @@ const DEVICE_CLASS_TO_GRAPHQL_TYPE: Partial<Record<DeviceClass, AppleDeviceClass
export async function runDeveloperPortalMethodAsync(
graphqlClient: ExpoGraphqlClient,
appStoreApi: AppStoreApi,
account: AccountFragment,
accountId: string,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>
): Promise<void> {
const appleAuthCtx = await appStoreApi.ensureAuthenticatedAsync();
const unregisteredPortalDevices = await findUnregisteredPortalDevicesAsync(
graphqlClient,
appleAuthCtx,
account.name,
accountId,
appleTeam
);
if (unregisteredPortalDevices.length === 0) {
@@ -40,7 +40,7 @@ export async function runDeveloperPortalMethodAsync(
return;
}
const devicesToImport = await chooseDevicesToImportAsync(unregisteredPortalDevices);
await importDevicesAsync(graphqlClient, account.id, appleTeam, devicesToImport);
await importDevicesAsync(graphqlClient, accountId, appleTeam, devicesToImport);
}
async function importDevicesAsync(
@@ -88,12 +88,12 @@ async function importDeviceChunkAsync(
async function findUnregisteredPortalDevicesAsync(
graphqlClient: ExpoGraphqlClient,
appleAuthCtx: AuthCtx,
accountName: string,
accountId: string,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>
): Promise<Device[]> {
const expoRegisteredDevices = await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(
graphqlClient,
accountName,
accountId,
appleTeam.appleTeamIdentifier,
{ useCache: false }
);
@@ -6,6 +6,5 @@ export function createCtxMock(options: { nonInteractive?: boolean } = {}): EASUp
graphqlClient: jest.fn() as any,
nonInteractive: options.nonInteractive ?? false,
app: { exp: testAppJson, projectId: 'test-project-id', projectDir: '/' },
vcsClient: jest.fn() as any,
};
}
-2
View File
@@ -1,13 +1,11 @@
import { ExpoConfig } from '@expo/config-types';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { Client } from '../vcs/vcs';
export type EASUpdateContext = {
graphqlClient: ExpoGraphqlClient;
nonInteractive: boolean;
app: { exp: ExpoConfig; projectId: string; projectDir: string };
vcsClient: Client;
};
export interface EASUpdateAction<T = any> {
+237 -379
View File
File diff suppressed because it is too large. Load diff
@@ -1,61 +0,0 @@
import assert from 'assert';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
MarkCliDoneInOnboardingUserPreferencesMutation,
MarkCliDoneInOnboardingUserPreferencesMutationVariables,
UserPreferencesOnboardingInput,
} from '../generated';
export const UserPreferencesMutation = {
async markCliDoneInOnboardingUserPreferencesAsync(
graphqlClient: ExpoGraphqlClient,
userPreferencesData: Partial<UserPreferencesOnboardingInput> & { appId: string }
): Promise<{
isCLIDone: boolean;
appId: string;
}> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
MarkCliDoneInOnboardingUserPreferencesMutation,
MarkCliDoneInOnboardingUserPreferencesMutationVariables
>(
gql`
mutation MarkCliDoneInOnboardingUserPreferencesMutation(
$preferences: UserPreferencesInput!
) {
me {
setPreferences(preferences: $preferences) {
onboarding {
appId
isCLIDone
}
}
}
}
`,
{
preferences: {
onboarding: {
...userPreferencesData,
isCLIDone: true,
lastUsed: new Date().toISOString(),
},
},
}
)
.toPromise()
);
const appId = data.me.setPreferences.onboarding?.appId;
assert(appId, 'App ID must be defined');
const isCLIDone = data.me.setPreferences.onboarding?.isCLIDone;
assert(isCLIDone, 'isCLIDone must be defined and true');
return {
appId,
isCLIDone,
};
},
};
@@ -22,16 +22,6 @@ export const UserQuery = {
id
...AccountFragment
}
preferences {
onboarding {
appId
platform
deviceType
environment
isCLIDone
lastUsed
}
}
}
... on Robot {
firstName
@@ -12,13 +12,6 @@ export const AppFragmentNode = gql`
name
...AccountFragment
}
githubRepository {
id
metadata {
githubRepoOwnerName
githubRepoName
}
}
}
${AccountFragmentNode}
`;
@@ -37,6 +37,7 @@ export const BuildFragmentNode = gql`
}
}
channel
releaseChannel
distribution
iosEnterpriseProvisioning
buildProfile
@@ -54,6 +55,7 @@ export const BuildFragmentNode = gql`
updatedAt
message
completedAt
resourceClass
expirationDate
isForIosSimulator
}
-81
View File
@@ -1,81 +0,0 @@
import { runCommandAsync } from './runCommand';
import Log from '../log';
import { confirmAsync, promptAsync } from '../prompts';
export async function runGitCloneAsync({
targetProjectDir,
githubRepositoryName,
githubUsername,
useSsh = true,
}: {
githubUsername: string;
githubRepositoryName: string;
targetProjectDir: string;
useSsh?: boolean;
}): Promise<{
targetProjectDir: string;
}> {
const url = useSsh
? `[email protected]:${githubUsername}/${githubRepositoryName}.git`
: `https://github.com/${githubUsername}/${githubRepositoryName}.git`;
try {
await runCommandAsync({
command: 'git',
args: ['clone', url, targetProjectDir],
shouldPrintStderrLineAsStdout: line => {
return line.includes('Cloning into');
},
showSpinner: false,
});
return { targetProjectDir };
} catch (error: any) {
if (error.stderr.includes('already exists')) {
Log.warn(`Directory ${targetProjectDir} already exists.`);
const shouldContinue = await confirmAsync({
message: 'Do you want to clone your project to some other destination?',
});
if (!shouldContinue) {
throw new Error('Directory already exists. Aborting...');
}
const { newTargetProjectDir } = await promptAsync({
type: 'text',
name: 'newTargetProjectDir',
message: 'New target directory path:',
validate: (input: string) => input !== '',
});
return await runGitCloneAsync({
githubRepositoryName,
githubUsername,
targetProjectDir: newTargetProjectDir,
});
} else if (useSsh && error.stderr.includes('Permission denied')) {
Log.warn('It seems like you do not have permission to clone the repository using SSH.');
const shouldContinue = await confirmAsync({
message: 'Do you want to clone the repository using HTTPS instead?',
});
if (!shouldContinue) {
throw new Error('Permission denied. Aborting...');
}
return await runGitCloneAsync({
githubRepositoryName,
githubUsername,
targetProjectDir,
useSsh: false,
});
} else {
throw error;
}
}
}
export async function runGitPushAsync({
targetProjectDir,
}: {
targetProjectDir: string;
}): Promise<void> {
await runCommandAsync({
command: 'git',
args: ['push'],
cwd: targetProjectDir,
});
}
@@ -1,23 +0,0 @@
import { runCommandAsync } from './runCommand';
export async function installDependenciesAsync({
projectDir,
}: {
projectDir: string;
}): Promise<void> {
// TODO: add support for other package managers
await runCommandAsync({
command: 'npm',
args: ['install'],
cwd: projectDir,
shouldShowStderrLine: line => {
return (
!line.includes('WARN') &&
!line.includes('deprecated') &&
!line.includes('no longer maintained') &&
!line.includes('has been moved') &&
!(line === 'npm')
);
},
});
}
@@ -1,75 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import Log from '../log';
import { Ora, ora } from '../ora';
export async function runCommandAsync({
cwd,
args,
command,
shouldShowStderrLine,
shouldPrintStderrLineAsStdout,
showSpinner = true,
}: {
cwd?: string;
args: string[];
command: string;
shouldShowStderrLine?: (line: string) => boolean;
shouldPrintStderrLineAsStdout?: (line: string) => boolean;
showSpinner?: boolean;
}): Promise<void> {
Log.log(`🏗️ Running ${chalk.bold(`${command} ${args.join(' ')}`)}...`);
let spinner: Ora | undefined;
if (showSpinner) {
spinner = ora(`${chalk.bold(`${command} ${args.join(' ')}`)}`).start();
}
const spawnPromise = spawnAsync(command, args, {
stdio: ['inherit', 'pipe', 'pipe'],
cwd,
});
const {
child: { stdout, stderr },
} = spawnPromise;
if (!stdout || !stderr) {
throw new Error(`Failed to spawn ${command}`);
}
stdout.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
Log.log(`${chalk.gray(`[${command}]`)} ${line}`);
}
});
stderr.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
if (shouldShowStderrLine && !shouldShowStderrLine(line)) {
continue;
}
const log = `${chalk.gray(`[${command}]`)} ${line}`;
if (shouldPrintStderrLineAsStdout && shouldPrintStderrLineAsStdout(line)) {
Log.log(log);
} else {
Log.warn(`${chalk.gray(`[${command}]`)} ${line}`);
}
}
});
try {
await spawnPromise;
} catch (error) {
if (showSpinner) {
spinner?.fail(`${chalk.bold(`${command} ${args.join(' ')}`)} failed`);
} else {
Log.error(`❌ ${chalk.bold(`${command} ${args.join(' ')}`)} failed`);
}
throw error;
}
if (showSpinner) {
spinner?.succeed(`✅ ${chalk.bold(`${command} ${args.join(' ')}`)} succeeded`);
} else {
Log.log(`✅ ${chalk.bold(`${command} ${args.join(' ')}`)} succeeded`);
}
Log.log('');
}
@@ -133,30 +133,9 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
projectId: '',
exp: {} as any,
vcsClient,
nonInteractive: false,
})
).rejects.toThrowError(/we can't update this file programmatically/);
});
it('throws an error in non-interactive mode', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
vol.fromJSON(
{
'app.json': '{ "blah": {} }',
},
'/app'
);
await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '',
exp: {} as any,
vcsClient,
nonInteractive: true,
})
).rejects.toThrowError(/non-interactive/);
});
it('prompts for the Android package if using app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
@@ -184,7 +163,6 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
projectId: '',
exp: {} as any,
vcsClient,
nonInteractive: false,
})
).resolves.toBe('com.expo.notdominik');
expect(promptAsync).toHaveBeenCalled();
@@ -218,7 +196,6 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
projectId: '',
exp: {} as any,
vcsClient,
nonInteractive: false,
})
).resolves.toBe('com.expo.notdominik');
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
@@ -28,14 +28,12 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync({
projectId,
exp,
vcsClient,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
@@ -45,13 +43,7 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync({
moduleName: gradleUtils.DEFAULT_MODULE_NAME,
});
} catch {
return await configureApplicationIdAsync({
graphqlClient,
projectDir,
projectId,
exp,
nonInteractive,
});
return await configureApplicationIdAsync({ graphqlClient, projectDir, projectId, exp });
}
}
@@ -136,22 +128,12 @@ async function configureApplicationIdAsync({
projectDir,
projectId,
exp,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<string> {
if (nonInteractive) {
throw new Error(
`The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore(
'https://docs.expo.dev/versions/latest/config/app/#package'
)}`
);
}
const paths = getConfigFilePaths(projectDir);
// we can't automatically update app.config.js
if (paths.dynamicConfigPath) {
@@ -25,7 +25,6 @@ export async function getApplicationIdentifierAsync({
buildProfile,
platform,
vcsClient,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
@@ -34,7 +33,6 @@ export async function getApplicationIdentifierAsync({
buildProfile: BuildProfile;
platform: Platform;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<string> {
if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
@@ -47,7 +45,6 @@ export async function getApplicationIdentifierAsync({
projectId,
exp,
vcsClient,
nonInteractive,
});
}
@@ -63,7 +60,6 @@ export async function getApplicationIdentifierAsync({
projectId,
exp,
vcsClient,
nonInteractive,
});
}
@@ -111,29 +111,9 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
projectId: '1234',
exp: {} as any,
vcsClient,
nonInteractive: false,
})
).rejects.toThrowError(/we can't update this file programmatically/);
});
it('throws an error in non-interactive mode', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
vol.fromJSON(
{
'app.json': '{ "blah": {} }',
},
'/app'
);
await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
graphqlClient,
projectDir: '/app',
projectId: '1234',
exp: {} as any,
vcsClient,
nonInteractive: true,
})
).rejects.toThrowError(/non-interactive/);
});
it('prompts for the bundle identifier if using app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
@@ -161,7 +141,6 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
projectId: '1234',
exp: {} as any,
vcsClient,
nonInteractive: false,
})
).resolves.toBe('com.expo.notdominik');
expect(promptAsync).toHaveBeenCalled();
@@ -193,7 +172,6 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
projectId: '1234',
exp: {} as any,
vcsClient,
nonInteractive: false,
})
).resolves.toBe('com.expo.notdominik');
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
@@ -21,14 +21,12 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync({
projectId,
exp,
vcsClient,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
@@ -41,7 +39,6 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync({
projectDir,
exp,
projectId,
nonInteractive,
});
}
}
@@ -123,22 +120,12 @@ async function configureBundleIdentifierAsync({
projectDir,
projectId,
exp,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<string> {
if (nonInteractive) {
throw new Error(
`The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore(
'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier'
)}`
);
}
const paths = getConfigFilePaths(projectDir);
// we can't automatically update app.config.js
if (paths.dynamicConfigPath) {
@@ -161,7 +148,6 @@ async function configureBundleIdentifierAsync({
exp,
projectId
);
const { bundleIdentifier } = await promptAsync({
name: 'bundleIdentifier',
type: 'text',
+15 -7
View File
@@ -67,11 +67,6 @@ export function isExpoUpdatesInstalled(projectDir: string): boolean {
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
}
export function isExpoNotificationsInstalled(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return !!(packageJson.dependencies && 'expo-notifications' in packageJson.dependencies);
}
export function isExpoUpdatesInstalledAsDevDependency(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return !!(packageJson.devDependencies && 'expo-updates' in packageJson.devDependencies);
@@ -145,6 +140,19 @@ export async function enforceRollBackToEmbeddedUpdateSupportAsync(
);
}
export async function isClassicUpdatesSupportedAsync(projectDir: string): Promise<boolean> {
const expoUpdatesPackageVersion = await getExpoUpdatesPackageVersionIfInstalledAsync(projectDir);
if (expoUpdatesPackageVersion === null) {
return false;
}
if (expoUpdatesPackageVersion.includes('canary')) {
return false;
}
return semver.lt(expoUpdatesPackageVersion, '0.19.0');
}
export async function isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(
projectDir: string
): Promise<boolean> {
@@ -157,8 +165,8 @@ export async function isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAs
return true;
}
// Anything SDK 51 or greater uses the expo-updates CLI
return semver.gte(expoUpdatesPackageVersion, '0.25.4');
// TODO(wschurman): add semver check once we know the SDK51 version of expo-updates that supports this
return false;
}
export async function installExpoUpdatesAsync(
+21 -24
View File
@@ -1,6 +1,6 @@
import { ExpoConfig, Platform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
@@ -13,6 +13,7 @@ import path from 'path';
import promiseLimit from 'promise-limit';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
import { resolveWorkflowAsync } from './workflow';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getDefaultBranchNameAsync } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -682,15 +683,13 @@ export function getRequestedPlatform(
export async function getRuntimeVersionObjectAsync({
exp,
platforms,
workflows,
projectDir,
env,
vcsClient,
}: {
exp: ExpoConfig;
platforms: Platform[];
workflows: Record<Platform, Workflow>;
projectDir: string;
env: Env | undefined;
vcsClient: Client;
}): Promise<{ platform: string; runtimeVersion: string }[]> {
return await Promise.all(
platforms.map(async platform => {
@@ -699,9 +698,8 @@ export async function getRuntimeVersionObjectAsync({
runtimeVersion: await getRuntimeVersionForPlatformAsync({
exp,
platform,
workflow: workflows[platform],
projectDir,
env,
vcsClient,
}),
};
})
@@ -711,15 +709,13 @@ export async function getRuntimeVersionObjectAsync({
async function getRuntimeVersionForPlatformAsync({
exp,
platform,
workflow,
projectDir,
env,
vcsClient,
}: {
exp: ExpoConfig;
platform: Platform;
workflow: Workflow;
projectDir: string;
env: Env | undefined;
vcsClient: Client;
}): Promise<string> {
if (platform === 'web') {
return 'UNVERSIONED';
@@ -727,20 +723,16 @@ async function getRuntimeVersionForPlatformAsync({
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
try {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const extraArgs = Log.isDebug ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env }
);
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(projectDir, [
'runtimeversion:resolve',
'--platform',
platform,
]);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
if (runtimeVersionResult.fingerprintSources) {
Log.debug(`Resolved fingeprint runtime version for platform "${platform}". Sources:`);
Log.debug(runtimeVersionResult.fingerprintSources);
}
return nullthrows(
runtimeVersionResult.runtimeVersion,
`Unable to determine runtime version for ${
@@ -758,6 +750,11 @@ async function getRuntimeVersionForPlatformAsync({
const runtimeVersion = exp[platform]?.runtimeVersion ?? exp.runtimeVersion;
if (typeof runtimeVersion === 'object') {
const workflow = await resolveWorkflowAsync(
projectDir,
platform as EASBuildJobPlatform,
vcsClient
);
if (workflow !== Workflow.MANAGED) {
throw new Error(
`You're currently using the bare workflow, where runtime version policies are not supported. You must set your runtime version manually. For example, define your runtime version as "1.0.0", not {"policy": "appVersion"} in your app config. ${learnMore(
@@ -1,6 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
import Log from '../log';
@@ -12,15 +11,11 @@ import {
export async function resolveRuntimeVersionAsync({
exp,
platform,
workflow,
projectDir,
env,
}: {
exp: ExpoConfig;
platform: 'ios' | 'android';
workflow: Workflow;
projectDir: string;
env: Env | undefined;
}): Promise<string | null> {
if (!(await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir))) {
// fall back to the previous behavior (using the @expo/config-plugins eas-cli dependency rather
@@ -29,20 +24,16 @@ export async function resolveRuntimeVersionAsync({
}
try {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const extraArgs = Log.isDebug ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env }
);
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(projectDir, [
'runtimeversion:resolve',
'--platform',
platform,
]);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
if (runtimeVersionResult.fingerprintSources) {
Log.debug(`Resolved fingeprint runtime version for platform "${platform}". Sources:`);
Log.debug(runtimeVersionResult.fingerprintSources);
}
return runtimeVersionResult.runtimeVersion ?? null;
} catch (e: any) {
// if expo-updates is not installed, there's no need for a runtime version in the build
@@ -24,7 +24,6 @@ import {
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { resolveRuntimeVersionAsync } from '../../project/resolveRuntimeVersionAsync';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { confirmAsync, promptAsync } from '../../prompts';
import { truthy } from '../../utils/expodash/filter';
import {
@@ -273,17 +272,10 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
async selectRuntimeVersionFromProjectConfigAsync(ctx: EASUpdateContext): Promise<string> {
const platforms: ('ios' | 'android')[] = ['ios', 'android'];
const workflows = await resolveWorkflowPerPlatformAsync(ctx.app.projectDir, ctx.vcsClient);
const runtimes = (
await Promise.all(
platforms.map(platform =>
resolveRuntimeVersionAsync({
projectDir: ctx.app.projectDir,
exp: ctx.app.exp,
platform,
workflow: workflows[platform],
env: undefined,
})
resolveRuntimeVersionAsync({ projectDir: ctx.app.projectDir, exp: ctx.app.exp, platform })
)
)
).filter(truthy);
@@ -1,6 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest, XML } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { RequestedPlatform } from '../../platform';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from '../../project/projectUtils';
@@ -10,25 +9,18 @@ import { ensureValidVersions } from '../utils';
/**
* Synchronize updates configuration to native files. This needs to do essentially the same thing as `withUpdates`
*/
export async function syncUpdatesConfigurationAsync({
projectDir,
exp,
workflow,
env,
}: {
projectDir: string;
exp: ExpoConfig;
workflow: Workflow;
env: Env | undefined;
}): Promise<void> {
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp, RequestedPlatform.Android);
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
await expoUpdatesCommandAsync(
projectDir,
['configuration:syncnative', '--platform', 'android', '--workflow', workflow],
{ env }
);
await expoUpdatesCommandAsync(projectDir, [
'configuration:syncnative',
'--platform',
'android',
]);
return;
}
@@ -61,6 +53,18 @@ export async function syncUpdatesConfigurationAsync({
await XML.writeXMLAsync({ path: stringsJSONPath, xml: updatedStringsResourceXML });
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifest = await getAndroidManifestAsync(projectDir);
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidConfig.Updates.Config.RELEASE_CHANNEL
);
} catch {
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifest = await getAndroidManifestAsync(projectDir);
+5 -20
View File
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config-types';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -264,33 +264,20 @@ async function ensureEASUpdateIsConfiguredNativelyAsync(
projectDir,
platform,
workflows,
env,
}: {
exp: ExpoConfig;
projectDir: string;
platform: RequestedPlatform;
workflows: Record<Platform, Workflow>;
env: Env | undefined;
}
): Promise<void> {
if (['all', 'android'].includes(platform) && workflows.android === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync({
projectDir,
exp,
workflow: workflows.android,
env,
});
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
}
if (['all', 'ios'].includes(platform) && workflows.ios === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync({
vcsClient,
projectDir,
exp,
workflow: workflows.ios,
env,
});
await syncIosUpdatesConfigurationAsync(vcsClient, projectDir, exp);
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
}
}
@@ -319,7 +306,8 @@ export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: stri
const easBuildProfilesWithChannels = Object.keys(easJsonRawObject.build).reduce(
(acc, profileNameKey) => {
const buildProfile = easJsonRawObject.build[profileNameKey];
const isNotAlreadyConfigured = !buildProfile.channel;
const isNotAlreadyConfigured = !buildProfile.channel && !buildProfile.releaseChannel;
if (isNotAlreadyConfigured) {
return {
...acc,
@@ -369,14 +357,12 @@ export async function ensureEASUpdateIsConfiguredAsync({
projectDir,
vcsClient,
platform,
env,
}: {
exp: ExpoConfig;
projectId: string;
projectDir: string;
vcsClient: Client;
platform: RequestedPlatform | null;
env: Env | undefined;
}): Promise<void> {
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(
projectDir,
@@ -414,7 +400,6 @@ export async function ensureEASUpdateIsConfiguredAsync({
projectDir,
platform,
workflows,
env,
});
}
@@ -1,6 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { RequestedPlatform } from '../../platform';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from '../../project/projectUtils';
@@ -9,27 +8,15 @@ import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
import { ensureValidVersions } from '../utils';
export async function syncUpdatesConfigurationAsync({
vcsClient,
projectDir,
exp,
workflow,
env,
}: {
vcsClient: Client;
projectDir: string;
exp: ExpoConfig;
workflow: Workflow;
env: Env | undefined;
}): Promise<void> {
export async function syncUpdatesConfigurationAsync(
vcsClient: Client,
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp, RequestedPlatform.Ios);
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
await expoUpdatesCommandAsync(
projectDir,
['configuration:syncnative', '--platform', 'ios', '--workflow', workflow],
{ env }
);
await expoUpdatesCommandAsync(projectDir, ['configuration:syncnative', '--platform', 'ios']);
return;
}
@@ -58,6 +45,15 @@ async function writeExpoPlistAsync(
await vcsClient.trackFileAsync(expoPlistPath);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist = await readExpoPlistAsync(projectDir);
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
} catch {
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist = await readExpoPlistAsync(projectDir);
@@ -54,6 +54,8 @@ const ExpoPlistWithChannel = `
<true/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
<key>EXUpdatesReleaseChannel</key>
<string>default</string>
<key>EXUpdatesSDKVersion</key>
<string>40.0.0</string>
<key>EXUpdatesURL</key>
@@ -75,6 +77,8 @@ const ExpoPlistWithOutChannel = `
<true/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
<key>EXUpdatesReleaseChannel</key>
<string>default</string>
<key>EXUpdatesSDKVersion</key>
<string>40.0.0</string>
<key>EXUpdatesURL</key>
+31 -1
View File
@@ -12,11 +12,13 @@ import {
UpdateFragment,
User,
} from '../graphql/generated';
import { learnMore } from '../log';
import Log, { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import { confirmAsync } from '../prompts';
import { getActorDisplayName } from '../user/User';
import groupBy from '../utils/expodash/groupBy';
import formatFields from '../utils/formatFields';
import { ProfileData } from '../utils/profiles';
export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'> & {
actor?:
@@ -272,3 +274,31 @@ export async function checkEASUpdateURLIsSetAsync(
return configuredURL === expectedURL;
}
export async function validateBuildProfileConfigMatchesProjectConfigAsync(
exp: ExpoConfig,
buildProfile: ProfileData<'build'>,
projectId: string,
nonInteractive: boolean
): Promise<void> {
if ((await checkEASUpdateURLIsSetAsync(exp, projectId)) && buildProfile.profile.releaseChannel) {
const warning = `» Your project is configured for EAS Update, but build profile "${
buildProfile.profileName
}" in ${chalk.bold('eas.json')} specifies the \`releaseChannel\` property.
For EAS Update, you need to specify the \`channel\` property, or your build will not be able to receive any updates
${learnMore('https://docs.expo.dev/eas-update/getting-started/#configure-your-project')}`;
Log.warn(warning);
if (!nonInteractive) {
const answer = await confirmAsync({
message: `Would you like to proceed?`,
});
if (!answer) {
Log.log('Aborting...');
process.exit(1);
}
}
}
}
+2 -2
View File
@@ -171,8 +171,8 @@ export default class SessionManager {
Log.log(
`Log in to EAS with email or username (exit and run ${chalk.bold(
'eas login --help'
)} to see other login options)`
'eas login'
)} for other options)`
);
const { username, password } = await promptAsync([
@@ -19,7 +19,6 @@ const userStub: Actor = {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};
const ssoUserStub: Actor = {
@@ -40,7 +39,6 @@ const ssoUserStub: Actor = {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};
const robotStub: Actor = {
+6 -22
View File
@@ -1,4 +1,3 @@
import { Env } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import resolveFrom, { silent as silentResolveFrom } from 'resolve-from';
@@ -6,13 +5,8 @@ import { link } from '../log';
export class ExpoUpdatesCLIModuleNotFoundError extends Error {}
export class ExpoUpdatesCLIInvalidCommandError extends Error {}
export class ExpoUpdatesCLICommandFailedError extends Error {}
export async function expoUpdatesCommandAsync(
projectDir: string,
args: string[],
options: { env: Env | undefined }
): Promise<string> {
export async function expoUpdatesCommandAsync(projectDir: string, args: string[]): Promise<string> {
let expoUpdatesCli;
try {
expoUpdatesCli =
@@ -30,23 +24,13 @@ export async function expoUpdatesCommandAsync(
}
try {
return (
await spawnAsync(expoUpdatesCli, args, {
stdio: 'pipe',
env: { ...process.env, ...options.env },
})
).stdout;
return (await spawnAsync(expoUpdatesCli, args)).stdout;
} catch (e: any) {
if (e.stderr && typeof e.stderr === 'string') {
if (e.stderr.includes('Invalid command')) {
throw new ExpoUpdatesCLIInvalidCommandError(
`The command specified by ${args} was not valid in the \`expo-updates\` CLI.`
);
} else {
throw new ExpoUpdatesCLICommandFailedError(e.stderr);
}
if (e.stderr && typeof e.stderr === 'string' && e.stderr.includes('Invalid command')) {
throw new ExpoUpdatesCLIInvalidCommandError(
`The command specified by ${args} was not valid in the \`expo-updates\` CLI.`
);
}
throw e;
}
}
+26 -65
View File
@@ -17,10 +17,6 @@ import {
import { Client } from '../vcs';
export default class GitClient extends Client {
constructor(private maybeCwdOverride?: string) {
super();
}
public override async ensureRepoExistsAsync(): Promise<void> {
try {
if (!(await isGitInstalledAsync())) {
@@ -62,16 +58,14 @@ export default class GitClient extends Client {
const cwd = process.cwd();
const repoRoot = PackageManagerUtils.findWorkspaceRoot(cwd) ?? cwd;
const confirmInit = await confirmAsync({
message: `Would you like us to run 'git init' in ${
this.maybeCwdOverride ?? repoRoot
} for you?`,
message: `Would you like us to run 'git init' in ${repoRoot} for you?`,
});
if (!confirmInit) {
throw new Error(
'A git repository is required for building your project. Initialize it and run this command again.'
);
}
await spawnAsync('git', ['init'], { cwd: this.maybeCwdOverride ?? repoRoot });
await spawnAsync('git', ['init'], { cwd: repoRoot });
Log.log("We're going to make an initial commit for your repository.");
@@ -98,16 +92,10 @@ export default class GitClient extends Client {
try {
if (commitAllFiles) {
await spawnAsync('git', ['add', '-A'], {
cwd: this.maybeCwdOverride,
});
await spawnAsync('git', ['add', '-A']);
}
await spawnAsync('git', ['add', '-u'], {
cwd: this.maybeCwdOverride,
});
await spawnAsync('git', ['commit', '-m', commitMessage], {
cwd: this.maybeCwdOverride,
});
await spawnAsync('git', ['add', '-u']);
await spawnAsync('git', ['commit', '-m', commitMessage]);
} catch (err: any) {
if (err?.stdout) {
Log.error(err.stdout);
@@ -124,24 +112,17 @@ export default class GitClient extends Client {
}
public override async showChangedFilesAsync(): Promise<void> {
const gitStatusOutput = await gitStatusAsync({
showUntracked: true,
cwd: this.maybeCwdOverride,
});
const gitStatusOutput = await gitStatusAsync({ showUntracked: true });
Log.log(gitStatusOutput);
}
public override async hasUncommittedChangesAsync(): Promise<boolean> {
const changes = await gitStatusAsync({ showUntracked: true, cwd: this.maybeCwdOverride });
const changes = await gitStatusAsync({ showUntracked: true });
return changes.length > 0;
}
public async getRootPathAsync(): Promise<string> {
return (
await spawnAsync('git', ['rev-parse', '--show-toplevel'], {
cwd: this.maybeCwdOverride,
})
).stdout.trim();
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
}
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
@@ -166,10 +147,7 @@ export default class GitClient extends Client {
if (await this.hasUncommittedChangesAsync()) {
Log.error('Detected inconsistent filename casing between your local filesystem and git.');
Log.error('This will likely cause your build to fail. Impacted files:');
await spawnAsync('git', ['status', '--short'], {
stdio: 'inherit',
cwd: this.maybeCwdOverride,
});
await spawnAsync('git', ['status', '--short'], { stdio: 'inherit' });
Log.newLine();
Log.error(
`Error: Resolve filename casing inconsistencies before proceeding. ${learnMore(
@@ -178,13 +156,14 @@ export default class GitClient extends Client {
);
throw new Error('You have some uncommitted changes in your repository.');
}
await spawnAsync(
'git',
['clone', '--no-hardlinks', '--depth', '1', gitRepoUri, destinationPath],
{
cwd: this.maybeCwdOverride,
}
);
await spawnAsync('git', [
'clone',
'--no-hardlinks',
'--depth',
'1',
gitRepoUri,
destinationPath,
]);
} finally {
await setGitCaseSensitivityAsync(isCaseSensitive);
}
@@ -192,29 +171,19 @@ export default class GitClient extends Client {
public override async getCommitHashAsync(): Promise<string | undefined> {
try {
return (
await spawnAsync('git', ['rev-parse', 'HEAD'], {
cwd: this.maybeCwdOverride,
})
).stdout.trim();
return (await spawnAsync('git', ['rev-parse', 'HEAD'])).stdout.trim();
} catch {
return undefined;
}
}
public override async trackFileAsync(file: string): Promise<void> {
await spawnAsync('git', ['add', '--intent-to-add', file], {
cwd: this.maybeCwdOverride,
});
await spawnAsync('git', ['add', '--intent-to-add', file]);
}
public override async getBranchNameAsync(): Promise<string | null> {
try {
return (
await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
cwd: this.maybeCwdOverride,
})
).stdout.trim();
return (await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout.trim();
} catch {
return null;
}
@@ -222,28 +191,20 @@ export default class GitClient extends Client {
public override async getLastCommitMessageAsync(): Promise<string | null> {
try {
return (
await spawnAsync('git', ['--no-pager', 'log', '-1', '--pretty=%B'], {
cwd: this.maybeCwdOverride,
})
).stdout.trim();
return (await spawnAsync('git', ['--no-pager', 'log', '-1', '--pretty=%B'])).stdout.trim();
} catch {
return null;
}
}
public override async showDiffAsync(): Promise<void> {
const outputTooLarge =
(await getGitDiffOutputAsync(this.maybeCwdOverride)).split(/\r\n|\r|\n/).length > 100;
await gitDiffAsync({ withPager: outputTooLarge, cwd: this.maybeCwdOverride });
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
await gitDiffAsync({ withPager: outputTooLarge });
}
public async isFileUntrackedAsync(path: string): Promise<boolean> {
const withUntrackedFiles = await gitStatusAsync({
showUntracked: true,
cwd: this.maybeCwdOverride,
});
const trackedFiles = await gitStatusAsync({ showUntracked: false, cwd: this.maybeCwdOverride });
const withUntrackedFiles = await gitStatusAsync({ showUntracked: true });
const trackedFiles = await gitStatusAsync({ showUntracked: false });
const pathWithoutLeadingDot = path.replace(/^\.\//, ''); // remove leading './' from path
return (
withUntrackedFiles.includes(pathWithoutLeadingDot) &&
@@ -254,7 +215,7 @@ export default class GitClient extends Client {
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
try {
await spawnAsync('git', ['check-ignore', '-q', filePath], {
cwd: this.maybeCwdOverride ?? path.normalize(await this.getRootPathAsync()),
cwd: path.normalize(await this.getRootPathAsync()),
});
return true;
} catch {
+7 -32
View File
@@ -22,45 +22,20 @@ export async function doesGitRepoExistAsync(): Promise<boolean> {
}
interface GitStatusOptions {
showUntracked: boolean;
cwd: string | undefined;
showUntracked?: boolean;
}
export async function gitStatusAsync({ showUntracked, cwd }: GitStatusOptions): Promise<string> {
return (
await spawnAsync('git', ['status', '-s', showUntracked ? '-uall' : '-uno'], {
cwd,
})
).stdout;
export async function gitStatusAsync({ showUntracked }: GitStatusOptions = {}): Promise<string> {
return (await spawnAsync('git', ['status', '-s', showUntracked ? '-uall' : '-uno'])).stdout;
}
export async function getGitDiffOutputAsync(cwd: string | undefined): Promise<string> {
return (
await spawnAsync('git', ['--no-pager', 'diff'], {
cwd,
})
).stdout;
export async function getGitDiffOutputAsync(): Promise<string> {
return (await spawnAsync('git', ['--no-pager', 'diff'])).stdout;
}
export async function gitDiffAsync({
withPager = false,
cwd,
}: {
withPager?: boolean;
cwd: string | undefined;
}): Promise<void> {
}: { withPager?: boolean } = {}): Promise<void> {
const options = withPager ? [] : ['--no-pager'];
try {
await spawnAsync('git', [...options, 'diff'], {
stdio: ['ignore', 'inherit', 'inherit'],
cwd,
});
} catch (error: any) {
if (typeof error.message === 'string' && error.message.includes('SIGPIPE')) {
// This error is thrown when the user exits the pager with `q`.
// do nothing
return;
}
throw error;
}
await spawnAsync('git', [...options, 'diff'], { stdio: ['ignore', 'inherit', 'inherit'] });
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "9.0.1",
"version": "7.8.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/eas-build-job": "1.0.107",
"@expo/eas-build-job": "1.0.95",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "11.2.0",
+20 -15
View File
@@ -40,7 +40,7 @@
},
"promptToConfigurePushNotifications": {
"type": "boolean",
"description": "Specifies where EAS CLI should prompt to configure Push Notifications credentials. It defaults to true if expo-notifications is in your project dependencies, otherwise it defaults to false.",
"description": "Specifies where EAS CLI should prompt to configure Push Notifications credentials. Defaults to true.",
"default": true
}
}
@@ -111,10 +111,17 @@
"Provide your own `credentials.json` file. [learn more](https://docs.expo.dev/app-signing/local-credentials/)"
]
},
"releaseChannel": {
"type": "string",
"description": "Name of the release channel for the `expo-updates` package. Learn more: https://docs.expo.dev/distribution/release-channels/",
"markdownDescription": "Name of the release channel for the `expo-updates` package. [Learn more](https://docs.expo.dev/distribution/release-channels/)\n\nIf you do not specify a channel, your binary will pull releases from the `default` channel. If you do not use `expo-updates` in your project then this property will have no effect.\n\n**This field only applies to the Classic Update service**; if you use EAS Update, use the [channel](https://docs.expo.dev/build-reference/eas-json/#channel) field instead.",
"pattern": "^[a-z0-9._-]+$",
"patternErrorMessage": "Release channel names can only contain lowercase letters, numbers and special characters `.`, `_` and `-`."
},
"channel": {
"type": "string",
"description": "The channel name identifies which EAS Update channel a build belongs to. Learn more: https://docs.expo.dev/eas-update/how-eas-update-works/",
"markdownDescription": "The channel name identifies which EAS Update channel a build belongs to. [Learn more](https://docs.expo.dev/eas-update/how-eas-update-works/)"
"description": "The channel is a name we can give to multiple builds to identify them easily. Learn more: https://docs.expo.dev/eas-update/how-eas-update-works/",
"markdownDescription": "The channel is a name we can give to multiple builds to identify them easily. [Learn more](https://docs.expo.dev/eas-update/how-eas-update-works/)\n\n**This field only applies to the EAS Update service**, if your project still uses Classic Updates then use the [releaseChannel](https://docs.expo.dev/build-reference/eas-json/#releasechannel) field instead."
},
"distribution": {
"enum": ["internal", "store"],
@@ -156,6 +163,11 @@
"description": "The version of Bun to use for the build. See: https://bun.sh/",
"markdownDescription": "The exact version of [Bun](https://bun.sh/) to use for the build."
},
"expoCli": {
"type": "string",
"description": "The version of `expo-cli` used to `prebuild` your app.",
"markdownDescription": "Version of [expo-cli](https://www.npmjs.com/package/expo-cli) used to [prebuild](https://docs.expo.dev/workflow/expo-cli/#expo-prebuild) your app. It does not have any effect on bare projects."
},
"env": {
"type": "object",
"description": "Environment variables that should be set during the build process.\n\nShould only be used for values that you would commit to your git repository, i.e. not passwords or secrets.",
@@ -229,12 +241,8 @@
"enum": [
"auto",
"latest",
"sdk-51",
"sdk-50",
"sdk-49",
"ubuntu-22.04-jdk-17-ndk-r25b",
"ubuntu-22.04-jdk-11-ndk-r23b",
"ubuntu-20.04-jdk-11-ndk-r23b",
"ubuntu-22.04-jdk-17-ndk-r21e",
"ubuntu-22.04-jdk-11-ndk-r21e",
"ubuntu-20.04-jdk-11-ndk-r21e",
@@ -245,10 +253,9 @@
],
"markdownEnumDescriptions": [
"When using this option the build image is selected automatically based on the project configuration, detected Expo SDK and React Native versions.",
"The latest Android image currently available. It is resolved to `ubuntu-22.04-jdk-17-ndk-r25b`. The `latest` to image mapping will be updated as new images are released.",
"The recommended image for SDK 51 builds",
"The recommended image for SDK 50 builds",
"The recommended image for SDK 49 builds"
"The latest Android image currently available. It is resolved to `ubuntu-22.04-jdk-17-ndk-r21e`. The `latest` to image mapping will be updated as new images are released.",
"The recommended image for SDK 50 builds: `ubuntu-22.04-jdk-17-ndk-r21e`",
"The recommended image for SDK 49 builds: `ubuntu-22.04-jdk-11-ndk-r21e`"
]
},
{
@@ -365,7 +372,6 @@
"enum": [
"auto",
"latest",
"sdk-51",
"sdk-50",
"sdk-49",
"macos-sonoma-14.4-xcode-15.3",
@@ -376,9 +382,8 @@
"markdownEnumDescriptions": [
"When using this option the build image is selected automatically based on the project configuration, detected Expo SDK and React Native versions.",
"The latest iOS image currently available. It is resolved to `macos-sonoma-14.4-xcode-15.3`. The `latest` to image mapping will be updated as new images are released.",
"The recommended image for SDK 51 builds",
"The recommended image for SDK 50 builds",
"The recommended image for SDK 49 builds"
"The recommended image for SDK 50 builds: `macos-ventura-13.6-xcode-15.2`",
"The recommended image for SDK 49 builds: `macos-ventura-13.6-xcode-15.2`"
]
},
{
@@ -508,6 +508,18 @@ test('invalid semver value', async () => {
);
});
test('invalid release channel', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: { releaseChannel: 'feature/myfeature' },
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
});
test('get profile names', async () => {
await fs.writeJson('/project/eas.json', {
build: {
-3
View File
@@ -38,9 +38,6 @@ export interface CommonBuildProfile {
pnpm?: string;
bun?: string;
yarn?: string;
/**
* @deprecated
*/
expoCli?: string;
// credentials
-10
View File
@@ -51,16 +51,6 @@ export class EasJsonUtils {
});
}
if (buildProfile.expoCli) {
warnings.push({
message: [
`The "build.${profileName}.expoCli" field in eas.json is deprecated and will be removed in the future.`,
`Global Expo CLI is deprecated. Since Expo SDK 46 local Expo CLI shipped with "expo" package is used. Remove this field from eas.json.`,
`Using this field has no effect on EAS Build process.`,
],
});
}
warnings.push(...EasJsonUtils.getCustomPathsDeprecationWarnings(rawEasJson, profileName));
return warnings;
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more