Compare commits

..
Author SHA1 Message Date
Piotr Szeremeta 6b3074e436 Revert "Add eas env:push and env:pull commands (#2495)"
This reverts commit 56510f0925.
2024-08-27 14:40:12 +02:00
102 changed files with 1510 additions and 11466 deletions

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
name: Changelog bot
on:
issue_comment:
types: [created]
jobs:
pr-commented:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 20
- name: Checkout Pull Request
run: gh pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Add changelog entry
working-directory: ./scripts
run: |
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
if [[ ! "$PARAMS" =~ " " ]]; then
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
PR_COMMENT_BODY: ${{ github.event.comment.body }}
- name: Commit and push
run: |
# use the last commit author name and email
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
+2 -1
View File
@@ -36,5 +36,6 @@ jobs:
refresh-message-position: true
message: ${{ contains(github.event.pull_request.labels.*.name, 'no changelog') && '⏩ The changelog entry check has been skipped since the "no changelog" label is present.' || '✅ Thank you for adding the changelog entry!' }}
message-failure: |
❌ It looks like a changelog entry is missing for this PR. Add it manually to CHANGELOG.md.
❌ It looks like a changelog entry is missing for this PR. You have two options: you can add it manually, or you can use the changelog bot to do it for you.
🤖 To use the bot, simply comment on this PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`.
⏩ If this PR doesn't require a changelog entry, such as if it's an internal change that doesn't affect the user experience, you can add the "no changelog" label to the PR.
-96
View File
@@ -10,104 +10,8 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Support assigning dev domain names in non interactive mode. ([#2595](https://github.com/expo/eas-cli/pull/2595) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Simplify the output of `eas deploy --json`. ([#2596](https://github.com/expo/eas-cli/pull/2596) by [@byCedric](https://github.com/byCedric))
- Support deploying Expo Router server exports without client directory. ([#2597](https://github.com/expo/eas-cli/pull/2597) by [@byCedric](https://github.com/byCedric))
- Add exit option to `eas credentials` interactive menu. ([#2570](https://github.com/expo/eas-cli/pull/2570) by [@szdziedzic](https://github.com/szdziedzic))
## [12.5.0](https://github.com/expo/eas-cli/releases/tag/v12.5.0) - 2024-09-23
### 🎉 New features
- Log command execution to assist in debugging local builds. ([#2526](https://github.com/expo/eas-cli/pull/2526) by [@trajano](https://github.com/trajano))
- Allow submitting builds in progress ([#2543](https://github.com/expo/eas-cli/pull/2543) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Use `EAS_DANGEROUS_OVERRIDE_ANDROID_APPLICATION_ID` and `EAS_DANGEROUS_OVERRIDE_IOS_BUNDLE_IDENTIFIER` environment variables as overrides of the Android application ID and iOS bundle identifier in managed workflow too. ([#2576](https://github.com/expo/eas-cli/pull/2576) by [@sjchmiela](https://github.com/sjchmiela))
- Add destination branch arguments to update:republish command. ([#2575](https://github.com/expo/eas-cli/pull/2575) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Avoid malforming `app.json` with empty `.expo` object. ([#2573](https://github.com/expo/eas-cli/pull/2573) by [@byCedric](https://github.com/byCedric))
- Fix typo causing `worker:deploy` asset upload errors not to be shown properly. ([#2579](https://github.com/expo/eas-cli/pull/2579) by [@kitten](https://github.com/kitten))
## [12.4.1](https://github.com/expo/eas-cli/releases/tag/v12.4.1) - 2024-09-14
## [12.4.0](https://github.com/expo/eas-cli/releases/tag/v12.4.0) - 2024-09-14
### 🎉 New features
- Add `worker:alias` command to assign aliases from the CLI. ([#2548](https://github.com/expo/eas-cli/pull/2548) by [@byCedric](https://github.com/byCedric))
- Add `worker --prod` flag to deploy to production from the CLI. ([#2550](https://github.com/expo/eas-cli/pull/2550) by [@byCedric](https://github.com/byCedric))
- Add `worker --alias` flag to assign custom aliases when deploying. ([#2551](https://github.com/expo/eas-cli/pull/2551) by [@byCedric](https://github.com/byCedric)))
- Add `worker --id` flag to use a custom deployment identifier. ([#2552](https://github.com/expo/eas-cli/pull/2552) by [@byCedric](https://github.com/byCedric)))
- Add `worker --environment` flag to deploy with EAS environment variables. ([#2557](https://github.com/expo/eas-cli/pull/2557) by [@kitten](https://github.com/kitten)))
- Add `worker --export-dir` flag to select exported directory. ([#2560](https://github.com/expo/eas-cli/pull/2560) by [@byCedric](https://github.com/byCedric)))
- Add `worker --json` flag to allow integrating with 3rd parties and custom tooling. ([#2561](https://github.com/expo/eas-cli/pull/2561) by [@byCedric](https://github.com/byCedric)))
- Add `worker:alias --json` flag to allow integrating with 3rd parties and custom tooling. ([#2562](https://github.com/expo/eas-cli/pull/2562) by [@byCedric](https://github.com/byCedric)))
### 🐛 Bug fixes
- Avoid merging `expo.extra` plugin-generated data with `expo.extra.eas.projectId` in `eas init`. ([#2554](https://github.com/expo/eas-cli/pull/2554) by [@byCedric](https://github.com/byCedric)))
- Restore "export not found" error and hide recent export timestamps. ([#2566](https://github.com/expo/eas-cli/pull/2566) by [@byCedric](https://github.com/byCedric)))
- Check if export is available before validating project ID in `eas worker`. ([#2569](https://github.com/expo/eas-cli/pull/2569) by [@byCedric](https://github.com/byCedric)))
### 🧹 Chores
- Make error message for invalid CFBundleShortVersionString more descriptive and actionable. Improve CFBundleShortVersionString validation regex. ([#2542](https://github.com/expo/eas-cli/pull/2542) by [@szdziedzic](https://github.com/szdziedzic))
- Add missing `--non-interactive` argument to `worker:deploy` command. ([#2544](https://github.com/expo/eas-cli/pull/2544) by [@kitten](https://github.com/kitten))
- Source `@expo/env` dotenv files for worker deployments. ([#2545](https://github.com/expo/eas-cli/pull/2545) by [@kitten](https://github.com/kitten))
- Support `worker --production` and clean up command output. ([#2555](https://github.com/expo/eas-cli/pull/2555) by [@byCedric](https://github.com/byCedric)))
- Unify both `worker` and `worker:alias` command output. ([#2558](https://github.com/expo/eas-cli/pull/2558) by [@byCedric](https://github.com/byCedric)))
- Share similar table/json output in both `worker` and `worker:alias` command outputs. ([#2563](https://github.com/expo/eas-cli/pull/2563) by [@byCedric](https://github.com/byCedric)))
- Polish the project URL prompt when setting up new projects. ([#2564](https://github.com/expo/eas-cli/pull/2564) by [@byCedric](https://github.com/byCedric)))
- Always assume `static` exports in `eas deploy` and add modified time. ([#2565](https://github.com/expo/eas-cli/pull/2565) by [@byCedric](https://github.com/byCedric)))
- Update the `eas worker --help` `--environment` description. ([#2567](https://github.com/expo/eas-cli/pull/2567) by [@byCedric](https://github.com/byCedric)))
- Remove the cursor space after selecting project dev domain. ([#2568](https://github.com/expo/eas-cli/pull/2568) by [@byCedric](https://github.com/byCedric)))
- Reword the dev domain prompt to mention "preview URL". ([#2572](https://github.com/expo/eas-cli/pull/2572) by [@byCedric](https://github.com/byCedric)))
## [12.3.0](https://github.com/expo/eas-cli/releases/tag/v12.3.0) - 2024-09-09
### 🎉 New features
- Add `--non-interactive` and `--force` support when `--id` is not passed to the `eas init` command. ([#1983](https://github.com/expo/eas-cli/pull/1983) by [@mymattcarroll](https://github.com/mymattcarroll))
## [12.1.1](https://github.com/expo/eas-cli/releases/tag/v12.1.1) - 2024-09-09
### 🐛 Bug fixes
- Fixed an issue where extensions retrieved the main app's entitlements instead of their own. ([#2532](https://github.com/expo/eas-cli/pull/2532) by [@HarrisHan](https://github.com/HarrisHan))
## [12.1.0](https://github.com/expo/eas-cli/releases/tag/v12.1.0) - 2024-09-06
### 🎉 New features
- **Internal/Experimental:** Add EAS Worker command ([#2447](https://github.com/expo/eas-cli/pull/2447) by [@kitten](https://github.com/kitten))
- Upload fingeprint source as part of eas update command. ([#2533](https://github.com/expo/eas-cli/pull/2533) by [@wschurman](https://github.com/wschurman))
## [12.0.0](https://github.com/expo/eas-cli/releases/tag/v12.0.0) - 2024-09-04
### 🛠 Breaking changes
- Prompt the users to set `appVersionSource`, while mentioning that `remote` is the default. ([#2411](https://github.com/expo/eas-cli/pull/2411) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🎉 New features
- Add support for syncing Journaling Suggestions, Managed App Installation UI, and 5G Network Slicing capabilities. ([#2525](https://github.com/expo/eas-cli/pull/2525) by [@szdziedzic](https://github.com/szdziedzic))
## [11.0.3](https://github.com/expo/eas-cli/releases/tag/v11.0.3) - 2024-08-31
### 🐛 Bug fixes
- Revert config-related packages to SDK 50 version from SDK 51 version to fix broken entitlements behavior for older SDKs. ([#2524](https://github.com/expo/eas-cli/pull/2524) by [@szdziedzic](https://github.com/szdziedzic))
## [11.0.2](https://github.com/expo/eas-cli/releases/tag/v11.0.2) - 2024-08-27
### 🐛 Bug fixes
- Remove unncessary static project config context definition from `eas build` command context, to fix resolving dynamic projest ID and slug for dynamic app configs. ([#2521](https://github.com/expo/eas-cli/pull/2521) by [@szdziedzic](https://github.com/szdziedzic))
## [11.0.1](https://github.com/expo/eas-cli/releases/tag/v11.0.1) - 2024-08-26
### 🐛 Bug fixes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "12.5.1",
"version": "11.0.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+66 -72
View File
@@ -143,7 +143,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -160,7 +160,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -177,7 +177,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -191,7 +191,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -243,7 +243,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -264,7 +264,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -284,7 +284,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -304,7 +304,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -327,7 +327,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -364,7 +364,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -383,7 +383,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -400,7 +400,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/v12.5.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -419,7 +419,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:inspect`
@@ -454,7 +454,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/v12.5.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -504,7 +504,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/v12.5.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -533,7 +533,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -559,7 +559,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/v12.5.1/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -609,7 +609,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/v12.5.1/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -628,7 +628,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/v12.5.1/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -647,7 +647,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/v12.5.1/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -664,7 +664,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/v12.5.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -685,7 +685,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -707,7 +707,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/v12.5.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -727,7 +727,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -760,7 +760,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/v12.5.1/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -783,7 +783,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -804,7 +804,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/v12.5.1/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -821,7 +821,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -839,7 +839,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/v12.5.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas device:create`
@@ -853,7 +853,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/v12.5.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -873,7 +873,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/v12.5.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -894,7 +894,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/v12.5.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -915,7 +915,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -929,7 +929,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/v12.5.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -943,7 +943,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -971,11 +971,10 @@ create or link an EAS project
```
USAGE
$ eas init [--id <value>] [--force] [--non-interactive]
$ eas init [--force --id <value>] [--non-interactive ]
FLAGS
--force Whether to create a new project/link an existing project without additional prompts or overwrite
any existing project ID when running with --id flag
--force Whether to overwrite any existing project ID
--id=<value> ID of the EAS project to link
--non-interactive Run the command in non-interactive mode.
@@ -1051,7 +1050,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1068,7 +1067,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/v12.5.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1085,7 +1084,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/v12.5.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1115,7 +1114,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/v12.5.1/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1129,7 +1128,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1137,11 +1136,10 @@ create or link an EAS project
```
USAGE
$ eas project:init [--id <value>] [--force] [--non-interactive]
$ eas project:init [--force --id <value>] [--non-interactive ]
FLAGS
--force Whether to create a new project/link an existing project without additional prompts or overwrite
any existing project ID when running with --id flag
--force Whether to overwrite any existing project ID
--id=<value> ID of the EAS project to link
--non-interactive Run the command in non-interactive mode.
@@ -1152,7 +1150,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1170,7 +1168,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas secret:create`
@@ -1193,7 +1191,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/v12.5.1/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [packages/eas-cli/src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1211,7 +1209,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/v12.5.1/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [packages/eas-cli/src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1225,7 +1223,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/v12.5.1/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [packages/eas-cli/src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1245,7 +1243,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/v12.5.1/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [packages/eas-cli/src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1276,7 +1274,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1310,7 +1308,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1328,7 +1326,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/v12.5.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1349,7 +1347,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/v12.5.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1371,7 +1369,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1379,18 +1377,14 @@ roll back to an existing update
```
USAGE
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [--destination-channel <value> |
--destination-branch <value>] [-m <value>] [-p android|ios|all] [--private-key-path <value>] [--json
--non-interactive]
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [-m <value>] [-p android|ios|all]
[--private-key-path <value>] [--json --non-interactive]
FLAGS
-m, --message=<value> Short message describing the republished update group
-p, --platform=(android|ios|all) [default: all]
--branch=<value> Branch name to select an update group to republish from
--channel=<value> Channel name to select an update group to republish from
--destination-branch=<value> Branch name to republish to if republishing to a different branch
--destination-channel=<value> Channel name to select a branch to republish to if republishing to a different
branch
--group=<value> Update group ID to republish
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
@@ -1403,7 +1397,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/v12.5.1/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1431,7 +1425,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/v12.5.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1451,7 +1445,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/v12.5.1/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1471,7 +1465,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1492,7 +1486,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1512,7 +1506,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1530,7 +1524,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1552,7 +1546,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1569,7 +1563,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v12.5.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
-1
View File
@@ -7,7 +7,6 @@ documents:
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
- 'src/worker/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
+588 -5743
View File
File diff suppressed because it is too large. Load diff
+17 -22
View File
@@ -1,35 +1,34 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "12.5.1",
"version": "11.0.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "1.8.0",
"@expo/apple-utils": "1.7.1",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "8.5.4",
"@expo/config-plugins": "7.8.4",
"@expo/config-types": "50.0.0",
"@expo/eas-build-job": "1.0.136",
"@expo/eas-json": "12.5.0",
"@expo/env": "^0.3.0",
"@expo/json-file": "8.2.37",
"@expo/config": "9.0.3",
"@expo/config-plugins": "8.0.8",
"@expo/config-types": "51.0.2",
"@expo/eas-build-job": "1.0.133",
"@expo/eas-json": "11.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.117",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "1.1.2",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.20",
"@expo/osascript": "2.1.3",
"@expo/package-manager": "1.5.2",
"@expo/pkcs12": "0.1.2",
"@expo/plist": "0.1.3",
"@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "6.7.3",
"@expo/prebuild-config": "7.0.8",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.136",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.130",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -64,7 +63,6 @@
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.1.2",
"minizlib": "3.0.1",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
@@ -80,14 +78,12 @@
"semver": "7.5.4",
"slash": "3.0.0",
"tar": "6.2.1",
"tar-stream": "3.1.7",
"terminal-link": "2.1.1",
"tslib": "2.6.2",
"turndown": "7.1.2",
"untildify": "4.0.0",
"uuid": "9.0.1",
"wrap-ansi": "7.0.0",
"zod": "^3.23.8"
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
@@ -109,13 +105,12 @@
"@types/prompts": "2.4.2",
"@types/semver": "7.5.6",
"@types/tar": "6.1.10",
"@types/tar-stream": "3.1.3",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.7",
"@types/wrap-ansi": "3.0.0",
"axios": "0.28.1",
"eslint-plugin-graphql": "4.0.0",
"express": "4.20.0",
"express": "4.19.2",
"memfs": "3.4.13",
"mockdate": "3.0.5",
"nock": "13.4.0",
@@ -1,5 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BuildVersionGetView reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL 1`] = `"Aborting..."`;
exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add {"cli": { "appVersionSource": "remote" }} in eas.json or re-run this command without "--non-interactive" flag."`;
@@ -1,18 +1,10 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import {
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
import BuildVersionGetView from '../../commands/build/version/get';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
jest.mock('../../project/applicationIdentifier');
@@ -20,7 +12,16 @@ jest.mock('fs');
jest.mock('../../log');
jest.mock('../../utils/json');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../prompts');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionGetView, () => {
afterEach(() => {
@@ -48,29 +49,6 @@ describe(BuildVersionGetView, () => {
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(Log.log).toHaveBeenCalledWith(`Android versionCode - ${chalk.bold('100')}`);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version for platform android when no remote version is set', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
@@ -167,51 +145,6 @@ describe(BuildVersionGetView, () => {
});
test('reading version when appVersionSource is set to local ', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(
BuildVersionGetView,
['--non-interactive', '--json', '--platform=android'],
ctx
);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
test('reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
test('reading version aborts when the appVersionSource is not specified and the user chooses to configure it manually', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
});
test('reading version sets appVersionSource to LOCAL and aborts when the appVersionSource is not specified and is run in non-interactive mode', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
@@ -223,10 +156,6 @@ describe(BuildVersionGetView, () => {
['--non-interactive', '--json', '--platform=android'],
ctx
);
await expect(cmd.run()).rejects.toThrowError(
`This project is not configured for using remote version source. Add ${chalk.bold(
'{"cli": { "appVersionSource": "remote" }}'
)} in eas.json or re-run this command without "--non-interactive" flag.`
);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
});
@@ -1,3 +1,4 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
@@ -7,15 +8,12 @@ import {
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import BuildVersionSetView from '../../commands/build/version/set';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
jest.mock('../../project/applicationIdentifier');
@@ -27,6 +25,16 @@ jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSetView, () => {
afterEach(() => {
jest.clearAllMocks();
@@ -59,34 +67,6 @@ describe(BuildVersionSetView, () => {
);
});
test('setting version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
version: '1000',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(AppVersionMutation.createAppVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
expect.objectContaining({
buildVersion: '1000',
storeVersion: '1.0.0',
})
);
});
test('printing current remote version before prompting for a new one', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
@@ -135,9 +115,7 @@ describe(BuildVersionSetView, () => {
});
test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
@@ -147,24 +125,8 @@ describe(BuildVersionSetView, () => {
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('setting version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => false);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborting...');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
@@ -175,33 +137,4 @@ describe(BuildVersionSetView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('setting version when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they allow auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => true);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('setting version aborts when the appVersionSource is not specified and the user chooses to configure manually', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
});
@@ -1,4 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
@@ -8,8 +9,6 @@ import {
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version';
@@ -20,7 +19,6 @@ import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import { getAppBuildGradleAsync } from '../../project/android/gradleUtils';
import { resolveTargetsAsync } from '../../project/ios/target';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import { resolveWorkflowAsync } from '../../project/workflow';
import * as prompts from '../../prompts';
@@ -39,6 +37,16 @@ jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSyncView, () => {
afterEach(() => {
jest.clearAllMocks();
@@ -71,34 +79,6 @@ describe(BuildVersionSyncView, () => {
expect(syncAndroidAsync).not.toHaveBeenCalled();
});
test('syncing version for managed project on platform android when appVersionSource is not set and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
version: '1000',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(syncAndroidAsync).not.toHaveBeenCalled();
});
test('syncing version for bare project on platform android', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()),
@@ -199,25 +179,9 @@ describe(BuildVersionSyncView, () => {
});
test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborting...');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('syncing version aborts when appVersionSource is not set and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
@@ -226,9 +190,7 @@ describe(BuildVersionSyncView, () => {
});
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
@@ -239,33 +201,4 @@ describe(BuildVersionSyncView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('syncing version when appVersionSource is not set and the user chooses to set it to LOCAL and they allow auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await cmd.run();
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('syncing version aborts when appVersionSource is not set and the user chooses to configure manually', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
});
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config-types';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import { EasJson } from '@expo/eas-json';
import { Command, Config } from '@oclif/core';
import { vol } from 'memfs';
import path from 'path';
@@ -148,23 +148,3 @@ export const getError = <TError = any>(call: () => unknown): TError | NoErrorThr
return error as TError;
}
};
export function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
export function withLocalVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.LOCAL,
},
};
}
@@ -1,77 +0,0 @@
import { AppVersionSource, EasJsonAccessor } from '@expo/eas-json';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { easCliVersion } from '../../utils/easCli';
import GitClient from '../../vcs/clients/git';
import { Client } from '../../vcs/vcs';
import { ensureProjectConfiguredAsync } from '../configure';
jest.mock('fs');
jest.mock('../../vcs/vcs');
jest.mock('../../vcs/clients/git');
beforeEach(async () => {
vol.reset();
});
describe(ensureProjectConfiguredAsync, () => {
it('returns false and does not configure if eas.json exists', async () => {
vol.fromJSON({
'./eas.json': JSON.stringify({
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
},
}),
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const vcsClientMock = jest.mocked(new GitClient());
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
const result = await ensureProjectConfiguredAsync({
projectDir: '.',
nonInteractive: false,
vcsClient: vcsClientMock as unknown as Client,
});
expect(result).toBeFalsy();
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
});
it('returns true and configures if eas.json does not exist', async () => {
const writeFileMock = jest.spyOn(fs, 'writeFile');
writeFileMock.mockImplementation(async (...args) => {
const easJsonPath = args[0] as string;
const easJsonData = args[1] as string;
vol.fromJSON({
[easJsonPath]: easJsonData,
});
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy();
const vcsClientMock = jest.mocked(new GitClient());
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
const result = await ensureProjectConfiguredAsync({
projectDir: '.',
nonInteractive: false,
vcsClient: vcsClientMock as unknown as Client,
});
expect(result).toBeTruthy();
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const easJson = await EasJsonAccessor.fromProjectPath('.').readAsync();
expect(easJson.cli?.appVersionSource).toEqual(AppVersionSource.REMOTE);
});
});
+1 -1
View File
@@ -107,7 +107,7 @@ export async function prepareAndroidBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.env,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
+57 -12
View File
@@ -2,7 +2,7 @@ import {
ArchiveSource,
ArchiveSourceType,
BuildJob,
FingerprintSource,
FingerprintSourceType,
Metadata,
Platform,
} from '@expo/eas-build-job';
@@ -13,6 +13,8 @@ import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { BuildContext } from './context';
import {
@@ -62,11 +64,11 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { getTmpDirectory } from '../utils/paths';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -184,7 +186,7 @@ export async function prepareBuildRequestForPlatformAsync<
return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions, ctx.env);
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
@@ -667,14 +669,14 @@ async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string;
fingerprintSource?: FingerprintSource;
fingerprintSource?: Metadata['fingerprintSource'];
}> {
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
exp: ctx.exp,
platform: ctx.platform,
workflow: ctx.workflow,
projectDir: ctx.projectDir,
env: ctx.env,
env: ctx.buildProfile.env,
cwd: ctx.projectDir,
});
@@ -683,19 +685,62 @@ async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
}
/**
* It's ok for fingerprintSources or runtimeVersion to be empty
* It's ok for fingerprintSources to be empty
* fingerprintSources only exist if the project is using runtimeVersion.policy: fingerprint
*/
if (!resolvedRuntimeVersion.fingerprint || !resolvedRuntimeVersion.runtimeVersion) {
if (!resolvedRuntimeVersion.fingerprint) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
};
}
return await maybeUploadFingerprintAsync({
runtimeVersion: resolvedRuntimeVersion.runtimeVersion,
fingerprint: resolvedRuntimeVersion.fingerprint,
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
await fs.mkdirp(getTmpDirectory());
const fingerprintLocation = path.join(getTmpDirectory(), `${uuidv4()}-runtime-fingerprint.json`);
await fs.writeJSON(fingerprintLocation, {
hash: resolvedRuntimeVersion.runtimeVersion,
sources: resolvedRuntimeVersion.fingerprint.fingerprintSources,
});
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
fingerprintSource: {
type: FingerprintSourceType.PATH,
path: fingerprintLocation,
isDebugFingerprint: resolvedRuntimeVersion.fingerprint.isDebugFingerprintSource,
},
};
}
let fingerprintGCSBucketKey = undefined;
try {
fingerprintGCSBucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasUpdateFingerprint,
fingerprintLocation
);
} catch (err: any) {
let errMessage = 'Failed to upload fingerprint to EAS';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
};
} finally {
await fs.remove(fingerprintLocation);
}
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
fingerprintSource: {
type: FingerprintSourceType.GCS,
bucketKey: fingerprintGCSBucketKey,
isDebugFingerprint: resolvedRuntimeVersion.fingerprint.isDebugFingerprintSource,
},
};
}
+2 -5
View File
@@ -1,4 +1,4 @@
import { AppVersionSource, EasJson, EasJsonAccessor } from '@expo/eas-json';
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -55,7 +55,6 @@ async function configureAsync({
const EAS_JSON_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
},
build: {
development: {
@@ -65,9 +64,7 @@ const EAS_JSON_DEFAULT: EasJson = {
preview: {
distribution: 'internal',
},
production: {
autoIncrement: true,
},
production: {},
},
submit: {
production: {},
-1
View File
@@ -63,5 +63,4 @@ export interface BuildContext<T extends Platform> {
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
env: Record<string, string>;
}
+2 -7
View File
@@ -43,7 +43,6 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel,
freezeCredentials,
repack,
env,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -65,11 +64,8 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env,
});
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
@@ -91,7 +87,7 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor,
graphqlClient,
analytics,
env,
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
@@ -151,7 +147,6 @@ export async function createBuildContextAsync<T extends Platform>({
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
env,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -1,109 +0,0 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
buildProfile,
buildProfileName,
graphqlClient,
getProjectConfig,
opts,
}: {
buildProfile: BuildProfile;
buildProfileName: string;
graphqlClient: ExpoGraphqlClient | null;
opts: Opts;
getProjectConfig(opts: Opts): Promise<Config>;
}): Promise<Config & { env: Env }> {
if (!graphqlClient) {
Log.warn('An Expo user account is required to fetch environment variables.');
const config = await getProjectConfig(opts);
return { env: buildProfile.env ?? {}, ...config };
}
const { projectId } = await getProjectConfig({ env: buildProfile.env, ...opts });
const env = await resolveEnvVarsAsync({
buildProfile,
buildProfileName,
graphqlClient,
projectId,
});
const config = await getProjectConfig({ ...opts, env });
return { env, ...config };
}
async function resolveEnvVarsAsync({
buildProfile,
buildProfileName,
graphqlClient,
projectId,
}: {
buildProfile: BuildProfile;
buildProfileName: string;
graphqlClient: ExpoGraphqlClient;
projectId: string;
}): Promise<Env> {
const environment =
buildProfile.environment?.toUpperCase() ?? process.env.EAS_CURRENT_ENVIRONMENT;
if (!environment || !isEnvironment(environment)) {
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
buildProfile.env && Object.keys(buildProfile.env).length > 0
? Object.keys(buildProfile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...buildProfile.env };
}
try {
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
environment,
}
);
const serverEnvVars = Object.fromEntries(
environmentVariables
.filter(({ name, value }) => name && value)
.map(({ name, value }) => [name, value])
) as Record<string, string>;
const envVarsWithSource: Record<string, 'build profile' | 'EAS server'> = {
...Object.fromEntries(Object.keys(serverEnvVars).map(key => [key, 'EAS server'])),
...(buildProfile.env
? Object.fromEntries(Object.keys(buildProfile.env).map(key => [key, 'build profile']))
: null),
};
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile and "${environment.toLowerCase()}" environment: ${
Object.keys(envVarsWithSource).length > 0
? Object.keys(envVarsWithSource)
.map(key => `${key} (source: ${envVarsWithSource[key]})`)
.join('\n')
: 'no environment variables specified'
}\n${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...serverEnvVars, ...buildProfile.env };
} catch (e) {
Log.error(`Failed to pull env variables for environment ${environment} from EAS servers`);
Log.error(e);
Log.error(
'This can possibly be a bug in EAS/EAS CLI. Report it here: https://github.com/expo/eas-cli/issues'
);
return { ...buildProfile.env };
}
}
+1 -19
View File
@@ -3,7 +3,6 @@ import {
ArchiveSourceType,
BuildMode,
BuildTrigger,
FingerprintSourceType,
Metadata,
Workflow,
} from '@expo/eas-build-job';
@@ -16,10 +15,8 @@ import {
BuildMetadataInput,
BuildWorkflow,
DistributionType,
FingerprintSourceInput,
BuildMode as GraphQLBuildMode,
BuildTrigger as GraphQLBuildTrigger,
FingerprintSourceType as GraphQLFingeprintSourceType,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
WorkerLoggerLevel,
@@ -45,8 +42,7 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return {
...metadata,
fingerprintSource:
metadata.fingerprintSource && transformFingerprintSource(metadata.fingerprintSource),
fingerprintSource: metadata.fingerprintSource as BuildMetadataInput['fingerprintSource'],
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -57,20 +53,6 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
};
}
export function transformFingerprintSource(
fingerprintSource: NonNullable<Metadata['fingerprintSource']>
): FingerprintSourceInput | null {
if (fingerprintSource.type !== FingerprintSourceType.GCS) {
return null;
}
return {
type: GraphQLFingeprintSourceType.Gcs,
bucketKey: fingerprintSource.bucketKey,
isDebugFingerprint: fingerprintSource.isDebugFingerprint,
};
}
function transformCredentialsSource(
credentialsSource: Metadata['credentialsSource']
): BuildCredentialsSource {
@@ -5,7 +5,6 @@ import { vol } from 'memfs';
import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { learnMore } from '../../../log';
import { readPlistAsync } from '../../../utils/plist';
import { resolveVcsClient } from '../../../vcs';
import {
@@ -317,9 +316,7 @@ describe(readShortVersionAsync, () => {
vcsClient
)
).rejects.toThrowError(
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: 0.0.7.1.028. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
'CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: 0.0.7.1.028'
);
});
});
+3 -3
View File
@@ -24,7 +24,7 @@ import {
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
const { buildProfile, env } = ctx;
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
@@ -47,7 +47,7 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env,
env: buildProfile.env,
vcsClient: ctx.vcsClient,
});
const applicationTarget = findApplicationTarget(targets);
@@ -89,7 +89,7 @@ export async function prepareIosBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.env,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
+8 -24
View File
@@ -11,7 +11,7 @@ import { Target } from '../../credentials/ios/types';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { findApplicationTarget } from '../../project/ios/target';
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
@@ -23,7 +23,7 @@ import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
const SHORT_VERSION_REGEX = /^\d+(\.\d+){0,2}$/;
const SHORT_VERSION_REGEX = /^\d+.\d+.\d+$/;
export enum BumpStrategy {
APP_VERSION,
@@ -100,27 +100,11 @@ export async function bumpVersionInAppJsonAsync({
}
}
function validateShortVersion({
shortVersion,
workflow,
}: {
shortVersion: string | undefined;
workflow: Workflow;
}): void {
function validateShortVersion(shortVersion: string | undefined): void {
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
if (workflow === Workflow.MANAGED) {
throw new Error(
`The required format for "version" field from app.json/app.config.ts is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
} else {
throw new Error(
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
}
throw new Error(
`CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: ${shortVersion}`
);
}
}
@@ -138,10 +122,10 @@ export async function readShortVersionAsync(
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
validateShortVersion({ shortVersion, workflow });
validateShortVersion(shortVersion);
return shortVersion;
} else {
validateShortVersion({ shortVersion: exp.version, workflow });
validateShortVersion(exp.version);
return exp.version;
}
}
+12 -22
View File
@@ -3,11 +3,10 @@ import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.135';
const PLUGIN_PACKAGE_VERSION = '1.0.131';
export enum LocalBuildMode {
/**
@@ -40,8 +39,7 @@ export interface LocalBuildOptions {
export async function runLocalBuildAsync(
job: Job,
metadata: Metadata,
options: LocalBuildOptions,
env: Record<string, string>
options: LocalBuildOptions
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner;
@@ -56,26 +54,18 @@ export async function runLocalBuildAsync(
};
process.on('SIGINT', interruptHandler);
try {
const mergedEnv = {
...env,
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
};
// log command execution to assist in debugging local builds
Log.debug('Running local build, using local-build-plugin', {
command,
args,
env: mergedEnv,
});
const spawnPromise = spawnAsync(command, args, {
stdio: options.verbose ? 'inherit' : 'pipe',
env: mergedEnv,
env: {
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
},
});
childProcess = spawnPromise.child;
await spawnPromise;
+14 -42
View File
@@ -2,6 +2,7 @@ import { ExpoConfig } from '@expo/config-types';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonAccessor,
EasJsonUtils,
@@ -18,7 +19,6 @@ import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
@@ -57,9 +57,8 @@ import {
validateAppVersionRuntimePolicySupportAsync,
} from '../project/projectUtils';
import {
ensureAppVersionSourceIsSetAsync,
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettingsAsync,
validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { runAsync } from '../run/run';
@@ -130,6 +129,13 @@ export async function runBuildAndSubmitAsync(
profileName: flags.profile ?? undefined,
projectDir,
});
Log.log(
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
buildProfiles[0].profile.env
? Object.keys(buildProfiles[0].profile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
for (const buildProfile of buildProfiles) {
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
@@ -167,12 +173,7 @@ export async function runBuildAndSubmitAsync(
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
{};
for (const buildProfile of buildProfiles) {
await validateBuildProfileVersionSettingsAsync(
buildProfile,
easJsonCliConfig,
projectDir,
flags
);
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
const maybeMetadata = await validateCustomBuildConfigAsync({
projectDir,
profile: buildProfile.profile,
@@ -191,15 +192,6 @@ export async function runBuildAndSubmitAsync(
for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform);
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
flags,
@@ -212,7 +204,6 @@ export async function runBuildAndSubmitAsync(
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -260,6 +251,7 @@ export async function runBuildAndSubmitAsync(
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
selectedSubmitProfileName: flags.submitProfile,
@@ -348,7 +340,6 @@ async function prepareAndStartBuildAsync({
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
env,
}: {
projectDir: string;
flags: BuildFlags;
@@ -361,7 +352,6 @@ async function prepareAndStartBuildAsync({
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
@@ -384,7 +374,6 @@ async function prepareAndStartBuildAsync({
buildLoggerLevel: flags.buildLoggerLevel,
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
env,
});
if (moreBuilds) {
@@ -423,25 +412,6 @@ async function prepareAndStartBuildAsync({
}
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
if (
easJsonCliConfig?.appVersionSource === undefined &&
buildProfile.profile.autoIncrement !== 'version'
) {
if (buildProfile.profile.autoIncrement !== true) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
} else {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
easJsonCliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
easJsonCliConfig,
flags.nonInteractive
);
}
}
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
}
@@ -478,6 +448,7 @@ async function prepareAndStartSubmissionAsync({
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
selectedSubmitProfileName,
nonInteractive,
@@ -486,6 +457,7 @@ async function prepareAndStartSubmissionAsync({
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
selectedSubmitProfileName?: string;
nonInteractive: boolean;
@@ -497,7 +469,7 @@ async function prepareAndStartSubmissionAsync({
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildCtx.env,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
-23
View File
@@ -1,9 +1,3 @@
import { ResourceClass } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { LocalBuildOptions } from './local';
import { RequestedPlatform } from '../platform';
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
@@ -20,20 +14,3 @@ export enum BuildDistributionType {
/** @deprecated Use simulator flag instead */
SIMULATOR = 'simulator',
}
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
nonInteractive: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
}
+2 -2
View File
@@ -9,9 +9,9 @@ import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
if (ctx.env?.NODE_ENV === 'production') {
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
Log.warn(
'You set NODE_ENV=production in the build profile or environment variables. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
);
Log.newLine();
}
@@ -18,15 +18,13 @@ import { Actor, getActorUsername } from '../../../user/User';
* Save an EAS project ID to the appropriate field in the app config.
*
* @deprecated Should not be used outside of context functions except in the init command.
* @deprecated Starting from `@expo/config` from SDK 52, the `modifyConfigAsync` function is merging existing data. Once this is released, we can use that instead of manually merging.
*/
export async function saveProjectIdToAppConfigAsync(
projectDir: string,
projectId: string,
options: { env?: Env } = {}
): Promise<void> {
// NOTE(cedric): we disable plugins to avoid writing plugin-generated content to `expo.extra`
const exp = getPrivateExpoConfig(projectDir, { skipPlugins: true, ...options });
const exp = getPrivateExpoConfig(projectDir, options);
const result = await createOrModifyExpoConfigAsync(
projectDir,
{
+3 -16
View File
@@ -6,7 +6,6 @@ import chalk from 'chalk';
import { handleDeprecatedEasJsonAsync } from '.';
import { waitForBuildEndAsync } from '../../build/build';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
@@ -22,7 +21,6 @@ import {
BuildStatus,
BuildWorkflow,
DistributionType,
EnvironmentVariableEnvironment,
IosJobOverridesInput,
ProjectArchiveSourceType,
StatuspageServiceName,
@@ -48,7 +46,6 @@ interface BuildResignFlags {
sourceProfile?: string;
maybeBuildId?: string;
wait: boolean;
environment?: EnvironmentVariableEnvironment;
}
interface RawBuildResignFlags {
json: boolean;
@@ -60,7 +57,6 @@ interface RawBuildResignFlags {
'source-profile': string | undefined;
wait: boolean;
id: string | undefined;
environment: EnvironmentVariableEnvironment | undefined;
}
export default class BuildResign extends EasCommand {
@@ -144,15 +140,7 @@ export default class BuildResign extends EasCommand {
platform,
flags.targetProfile ?? 'production'
);
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile,
buildProfileName: flags.targetProfile ?? 'production',
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.env },
});
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const build = await this.ensureBuildSelectedAsync(
{
@@ -173,7 +161,7 @@ export default class BuildResign extends EasCommand {
user: actor,
graphqlClient,
analytics,
env,
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
});
@@ -193,7 +181,7 @@ export default class BuildResign extends EasCommand {
projectDir,
exp,
xcodeBuildContext,
env,
env: buildProfile.env,
vcsClient,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
@@ -260,7 +248,6 @@ export default class BuildResign extends EasCommand {
targetProfile: flags['target-profile'],
maybeBuildId: flags.id,
wait: flags.wait,
environment: flags.environment,
};
}
@@ -3,7 +3,6 @@ import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../../commandUtils/flags';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -75,12 +74,8 @@ export default class BuildVersionGetView extends EasCommand {
});
const results: { [key in Platform]?: string } = {};
for (const { profile, platform } of buildProfiles) {
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profile,
buildProfileName: flags.profile ?? 'production',
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: profile.env },
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env: profile.env,
});
validateAppConfigForRemoteVersionSource(exp, platform);
@@ -94,7 +89,6 @@ export default class BuildVersionGetView extends EasCommand {
platform,
vcsClient,
nonInteractive: flags['non-interactive'],
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -4,7 +4,6 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../../../commandUtils/EasCommand';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -65,13 +64,7 @@ export default class BuildVersionSetView extends EasCommand {
flags.profile ?? undefined
);
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profile,
buildProfileName: flags.profile ?? 'production',
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: profile.env },
});
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: profile.env });
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
validateAppConfigForRemoteVersionSource(exp, platform);
@@ -85,7 +78,6 @@ export default class BuildVersionSetView extends EasCommand {
platform,
vcsClient,
nonInteractive: false,
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -5,7 +5,6 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../../build/android/version';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../../build/ios/version';
import EasCommand from '../../../commandUtils/EasCommand';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -88,12 +87,8 @@ export default class BuildVersionSyncView extends EasCommand {
projectDir,
});
for (const profileInfo of buildProfiles) {
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profileInfo.profile,
buildProfileName: profileInfo.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: profileInfo.profile.env },
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env: profileInfo.profile.env,
});
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
@@ -108,7 +103,6 @@ export default class BuildVersionSyncView extends EasCommand {
platform: profileInfo.platform,
vcsClient,
nonInteractive: false,
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
+6 -22
View File
@@ -4,9 +4,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../commandUtils/EasCommand';
import { createGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
@@ -35,7 +33,6 @@ export default class Config extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.SessionManagment,
};
async runAsync(): Promise<void> {
@@ -43,15 +40,10 @@ export default class Config extends EasCommand {
if (flags.json) {
enableJsonOutput();
}
const {
platform: maybePlatform,
profile: maybeProfile,
'non-interactive': nonInteractive,
} = flags;
const { getDynamicPublicProjectConfigAsync, projectDir, sessionManager } =
await this.getContextAsync(Config, {
nonInteractive,
});
const { platform: maybePlatform, profile: maybeProfile } = flags;
const { getDynamicPublicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
nonInteractive: false,
});
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
const profileName =
@@ -88,16 +80,8 @@ export default class Config extends EasCommand {
Log.log(JSON.stringify(profile, null, 2));
}
} else {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const { exp: appConfig } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profile,
buildProfileName: profileName,
graphqlClient,
getProjectConfig: getDynamicPublicProjectConfigAsync,
opts: { env: profile.env },
const { exp: appConfig } = await getDynamicPublicProjectConfigAsync({
env: profile.env,
});
if (flags.json) {
+6 -7
View File
@@ -114,10 +114,11 @@ export default class EnvironmentVariableCreate extends EasCommand {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment
);
const existingVariable = existingVariables.find(variable => variable.name === name);
if (existingVariable) {
@@ -190,9 +191,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
`Created a new variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
);
} else if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
});
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
const existingVariable = sharedVariables.find(variable => variable.name === name);
if (existingVariable) {
throw new Error(
+2 -5
View File
@@ -58,11 +58,8 @@ export default class EnvironmentVariableDelete extends EasCommand {
const variables =
scope === EnvironmentVariableScope.Project && environment
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId });
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, projectId, environment)
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
if (!name) {
({ name } = await promptAsync({
-185
View File
@@ -1,185 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
type ParsedFlags =
| {
nonInteractive: true;
environment: EnvironmentVariableEnvironment;
}
| {
nonInteractive: false;
environment?: EnvironmentVariableEnvironment;
};
interface RawFlags {
environment?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
}
export default class EnvExec extends EasCommand {
static override description =
'execute a bash command with environment variables from the selected environment';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASEnvironmentFlag,
...EASNonInteractiveFlag,
};
static override args = [
{
name: 'BASH_COMMAND',
required: true,
description: 'bash command to execute with the environment variables from the environment',
},
];
async runAsync(): Promise<void> {
const {
flags,
args: { BASH_COMMAND: command },
} = await this.parse(EnvExec);
const parsedFlags = this.sanitizeFlags(flags);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvExec, {
nonInteractive: parsedFlags.nonInteractive,
});
const environment =
parsedFlags.environment ?? (await promptVariableEnvironmentAsync(parsedFlags.nonInteractive));
const environmentVariables = await this.loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
environment,
});
await this.runCommandWithEnvVarsAsync({ command, environmentVariables });
}
private sanitizeFlags(rawFlags: RawFlags): ParsedFlags {
const environment = rawFlags.environment;
if (rawFlags['non-interactive']) {
if (!environment) {
throw new Error(
'You must specify an environment when running in non-interactive mode. Use the --environment flag.'
);
}
return {
nonInteractive: true,
environment,
};
}
return {
nonInteractive: false,
environment: rawFlags.environment,
};
}
private async runCommandWithEnvVarsAsync({
command,
environmentVariables,
}: {
command: string;
environmentVariables: Record<string, string>;
}): Promise<void> {
Log.log(`Running command: ${chalk.bold(command)}`);
const spawnPromise = spawnAsync('bash', ['-c', command], {
stdio: ['inherit', 'pipe', 'pipe'],
env: {
...process.env,
...environmentVariables,
},
});
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('[stdout]')} ${line}`);
}
});
stderr.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
Log.warn(`${chalk.gray('[stderr]')} ${line}`);
}
});
try {
await spawnPromise;
} catch (error) {
Log.error(`❌ ${chalk.bold(command)} failed`);
throw error;
}
}
private async loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
environment,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
environment: EnvironmentVariableEnvironment;
}): Promise<Record<string, string>> {
const environmentVariablesQueryResult =
await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
});
const secretEnvironmentVariables = environmentVariablesQueryResult.filter(
({ value }) => !value
);
if (secretEnvironmentVariables.length > 0) {
Log.warn(`The following environment variables are secret and cannot be downloaded locally:`);
for (const { name } of secretEnvironmentVariables) {
Log.warn(`- ${name}`);
}
Log.warn('Proceeding with the rest of the environment variables.');
Log.newLine();
}
const nonSecretEnvironmentVariables = environmentVariablesQueryResult.filter(
({ value }) => !!value
);
if (nonSecretEnvironmentVariables.length === 0) {
throw new Error('No readable environment variables found for the selected environment.');
}
Log.log(
`Loaded environment variables for the selected environment "${environment.toLowerCase()}":`
);
for (const { name } of nonSecretEnvironmentVariables) {
Log.log(`- ${name}`);
}
Log.newLine();
const environmentVariables: Record<string, string> = {};
for (const { name, value } of nonSecretEnvironmentVariables) {
if (value) {
environmentVariables[name] = value;
}
}
return environmentVariables;
}
}
+10 -15
View File
@@ -72,6 +72,7 @@ export default class EnvironmentVariableGet extends EasCommand {
if (!environment && scope === EnvironmentVariableScope.Project) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const variable = await getVariableAsync(graphqlClient, scope, projectId, name, environment);
if (!variable) {
@@ -116,32 +117,26 @@ async function getVariableAsync(
graphqlClient: ExpoGraphqlClient,
scope: string,
projectId: string,
name: string | undefined,
name: string,
environment: string | undefined
): Promise<EnvironmentVariableFragment | null> {
if (!environment && scope === EnvironmentVariableScope.Project) {
throw new Error('Environment is required.');
}
if (!name) {
throw new Error("Variable name is required. Run the command with '--name VARIABLE_NAME' flag.");
}
if (environment && scope === EnvironmentVariableScope.Project) {
const appVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment,
filterNames: [name],
});
[name]
);
return appVariables[0];
}
if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
filterNames: [name],
}
);
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId, [
name,
]);
return sharedVariables[0];
}
+1 -3
View File
@@ -40,9 +40,7 @@ export default class EnvironmentVariableLink extends EasCommand {
});
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
});
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
if (!name) {
name = await selectAsync(
+6 -52
View File
@@ -1,18 +1,12 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
} from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
} from '../../graphql/generated';
import { EnvironmentVariableScope } from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { formatVariable } from '../../utils/formatVariable';
@@ -29,10 +23,6 @@ export default class EnvironmentValueList extends EasCommand {
};
static override flags = {
'include-sensitive': Flags.boolean({
description: 'Display sensitive values in the output',
default: false,
}),
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
@@ -40,7 +30,7 @@ export default class EnvironmentValueList extends EasCommand {
async runAsync(): Promise<void> {
let {
flags: { environment, format, scope, 'include-sensitive': includeSensitive },
flags: { environment, format, scope },
} = await this.parse(EnvironmentValueList);
const {
privateProjectConfig: { projectId },
@@ -53,12 +43,10 @@ export default class EnvironmentValueList extends EasCommand {
environment = await promptVariableEnvironmentAsync(false);
}
const variables = await this.getVariablesForScopeAsync(graphqlClient, {
scope,
includingSensitive: includeSensitive,
environment,
projectId,
});
const variables =
scope === EnvironmentVariableScope.Project && environment
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, projectId, environment)
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
if (format === 'short') {
for (const variable of variables) {
@@ -81,38 +69,4 @@ export default class EnvironmentValueList extends EasCommand {
);
}
}
private async getVariablesForScopeAsync(
graphqlClient: ExpoGraphqlClient,
{
scope,
includingSensitive,
environment,
projectId,
}: {
scope: EnvironmentVariableScope;
includingSensitive: boolean;
environment?: EnvironmentVariableEnvironment;
projectId: string;
}
): Promise<EnvironmentVariableFragment[]> {
if (scope === EnvironmentVariableScope.Project && environment) {
if (includingSensitive) {
return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return includingSensitive
? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, {
appId: projectId,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId });
}
}
-94
View File
@@ -1,94 +0,0 @@
import { Flags } from '@oclif/core';
import * as fs from 'fs-extra';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableFragment } from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
export default class EnvironmentVariablePull extends EasCommand {
static override description = 'pull env file';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASEnvironmentFlag,
...EASNonInteractiveFlag,
path: Flags.string({
description: 'Path to the result `.env` file',
default: '.env.local',
}),
};
async runAsync(): Promise<void> {
let {
flags: { environment, path: targetPath, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentVariablePull);
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariablePull, {
nonInteractive,
});
targetPath = targetPath ?? '.env.local';
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
environment,
}
);
if (!nonInteractive && (await fs.exists(targetPath))) {
const result = await confirmAsync({
message: `File ${targetPath} already exists. Do you want to overwrite it?`,
});
if (!result) {
Log.log('Aborting...');
throw new Error(`File ${targetPath} already exists.`);
}
}
const filePrefix = `# Environment: ${environment}\n\n`;
const envFileContent = environmentVariables
.map((variable: EnvironmentVariableFragment) => {
if (variable.value === null) {
return `# ${variable.name}=***** (secret variables are not available for reading)`;
}
return `${variable.name}=${variable.value}`;
})
.join('\n');
await fs.writeFile(targetPath, filePrefix + envFileContent);
const secretEnvVariables = environmentVariables.filter(
(variable: EnvironmentVariableFragment) => variable.value === null
);
if (secretEnvVariables.length > 0) {
Log.warn(
`The eas env:pull command tried to pull environment variables with "secret" visibility. The variables with "secret" visibility are not available for reading, therefore thet were marked as "*****" in the generated .env file. Provide values for these manually in ${targetPath} if needed. Skipped variables: ${secretEnvVariables
.map(v => v.name)
.join('\n')}`
);
Log.warn();
}
Log.log(
`Pulled environment variables from ${environment.toLowerCase()} environment to ${targetPath}.`
);
}
}
-210
View File
@@ -1,210 +0,0 @@
import { Flags } from '@oclif/core';
import dotenv from 'dotenv';
import fs from 'fs-extra';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag } from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
import {
EnvironmentVariableMutation,
EnvironmentVariablePushInput,
} from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
type PushFlags = {
environment?: EnvironmentVariableEnvironment;
path: string;
};
export default class EnvironmentVariablePush extends EasCommand {
static override description = 'push env file';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASEnvironmentFlag,
path: Flags.string({
description: 'Path to the input `.env` file',
default: '.env.local',
}),
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariablePush);
const { environment, path: envPath } = this.validateFlags(flags);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariablePush, {
nonInteractive: false,
});
const updateVariables: Record<string, EnvironmentVariablePushInput> =
await this.parseEnvFileAsync(envPath, environment);
const variableNames = Object.keys(updateVariables);
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
filterNames: variableNames,
});
const existingDifferentVariables: EnvironmentVariableFragment[] = [];
// Remove variables that are the same as the ones in the environment
existingVariables.forEach(variable => {
const existingVariableUpdate = updateVariables[variable.name];
if (existingVariableUpdate && existingVariableUpdate.value !== variable.value) {
existingDifferentVariables.push(variable);
} else {
delete updateVariables[variable.name];
}
});
const existingDifferentSharedVariables = existingDifferentVariables.filter(
variable => variable.scope === EnvironmentVariableScope.Shared
);
if (existingDifferentSharedVariables.length > 0) {
const existingDifferentSharedVariablesNames = existingDifferentSharedVariables.map(
variable => variable.name
);
Log.error('Shared variables cannot be overwritten by eas env:push command.');
Log.error('Remove them from the env file or unlink them from the project to continue:');
existingDifferentSharedVariablesNames.forEach(name => {
Log.error(`- ${name}`);
});
throw new Error('Shared variables cannot be overwritten by eas env:push command');
}
if (existingDifferentVariables.length > 0) {
Log.warn('Some variables already exist in the environment.');
const variableNames = existingDifferentVariables.map(variable => variable.name);
const confirmationMessage =
variableNames.length > 1
? `The ${variableNames.join(
', '
)} environment variables already exist in ${environment} environment. Do you want to override them all?`
: `The ${variableNames[0]} environment variable already exists in ${environment} environment. Do you want to override it?`;
const confirm = await confirmAsync({
message: confirmationMessage,
});
let variablesToOverwrite: string[] = [];
if (!confirm && existingDifferentVariables.length === 1) {
throw new Error('No new variables to push.');
}
if (confirm) {
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
} else {
const promptResult = await promptAsync({
type: 'multiselect',
name: 'variablesToOverwrite',
message: 'Select variables to overwrite:',
// @ts-expect-error property missing from `@types/prompts`
optionsPerPage: 20,
choices: existingDifferentVariables.map(variable => ({
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
variable.value ?? '(secret)'
})`,
value: variable.name,
})),
});
variablesToOverwrite = promptResult.variablesToOverwrite;
}
for (const existingVariable of existingVariables) {
const name = existingVariable.name;
if (variablesToOverwrite.includes(name)) {
updateVariables[name]['overwrite'] = true;
} else {
delete updateVariables[name];
}
}
}
// Check if any of the sensitive variables already exist in the environment. Prompt the user to overwrite them.
const existingSensitiveVariables = existingVariables.filter(
variable => variable.value === null
);
if (existingSensitiveVariables.length > 0) {
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
variable => `- ${variable.name}`
);
const confirm = await confirmAsync({
message: `You are about to overwrite sensitive variables.\n${existingSensitiveVariablesNames.join(
'\n'
)}\n Do you want to continue?`,
});
if (!confirm) {
throw new Error('Aborting...');
}
}
const variablesToPush = Object.values(updateVariables);
if (variablesToPush.length === 0) {
Log.log('No new variables to push.');
return;
}
await EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync(
graphqlClient,
variablesToPush,
projectId
);
Log.log(`Uploaded env file to ${environment} environment.`);
}
private async parseEnvFileAsync(
envPath: string,
environment: string
): Promise<Record<string, EnvironmentVariablePushInput>> {
if (!(await fs.exists(envPath))) {
throw new Error(`File ${envPath} does not exist.`);
}
const pushInput: Record<string, EnvironmentVariablePushInput> = {};
const variables: Record<string, string> = dotenv.parse(await fs.readFile(envPath, 'utf8'));
for (const [name, value] of Object.entries(variables)) {
pushInput[name] = {
name,
value,
environment,
visibility: name.startsWith('EXPO_SENSITIVE')
? EnvironmentVariableVisibility.Sensitive
: EnvironmentVariableVisibility.Public,
};
}
return pushInput;
}
private validateFlags(flags: PushFlags): Required<PushFlags> {
if (!flags.environment) {
throw new Error('Please provide an environment to push the env file to.');
}
return { ...flags, environment: flags.environment };
}
}
+5 -4
View File
@@ -46,10 +46,11 @@ export default class EnvironmentVariableUnlink extends EasCommand {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment
);
const linkedVariables = appVariables.filter(
({ scope }) => scope === EnvironmentVariableScope.Shared
);
+6 -7
View File
@@ -80,10 +80,11 @@ export default class EnvironmentVariableUpdate extends EasCommand {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment
);
if (!name) {
name = await selectAsync(
'Select variable',
@@ -133,9 +134,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
`Updated variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
);
} else if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
});
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
if (!name) {
name = await selectAsync(
+23 -54
View File
@@ -8,7 +8,6 @@ import { getProjectDashboardUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { AppPrivacy, Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
@@ -34,10 +33,14 @@ export default class ProjectInit extends EasCommand {
description: 'ID of the EAS project to link',
}),
force: Flags.boolean({
description:
'Whether to create a new project/link an existing project without additional prompts or overwrite any existing project ID when running with --id flag',
description: 'Whether to overwrite any existing project ID',
dependsOn: ['id'],
}),
// this is the same as EASNonInteractiveFlag but with the dependsOn
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
dependsOn: ['id'],
}),
...EASNonInteractiveFlag,
};
static override contextDefinition = {
@@ -213,11 +216,10 @@ export default class ProjectInit extends EasCommand {
});
}
private static async initializeWithoutExplicitIDAsync(
private static async initializeWithInteractiveSelectionAsync(
graphqlClient: ExpoGraphqlClient,
actor: Actor,
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
projectDir: string
): Promise<string> {
const exp = getPrivateExpoConfig(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
@@ -243,20 +245,6 @@ export default class ProjectInit extends EasCommand {
if (!accountName) {
if (allAccounts.length === 1) {
accountName = allAccounts[0].name;
} else if (nonInteractive) {
if (!force) {
throw new Error(
`There are multiple accounts that you have access to: ${allAccounts
.map(a => a.name)
.join(
', '
)}. Explicitly set the owner property in your app config or run this command with the --force flag to proceed with a default account: ${
allAccounts[0].name
}.`
);
}
accountName = allAccounts[0].name;
Log.log(`Using default account ${accountName} for non-interactive and force mode`);
} else {
const choices = ProjectInit.getAccountChoices(
actor,
@@ -286,21 +274,13 @@ export default class ProjectInit extends EasCommand {
projectName
);
if (existingProjectIdOnServer) {
if (!force) {
if (nonInteractive) {
throw new Error(
`Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Use --force flag to continue with this project.`
);
}
const affirmedLink = await confirmAsync({
message: `Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Link this project?`,
});
if (!affirmedLink) {
throw new Error(
`Project ID configuration canceled. Re-run the command to select a different account/project.`
);
}
const affirmedLink = await confirmAsync({
message: `Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Link this project?`,
});
if (!affirmedLink) {
throw new Error(
`Project ID configuration canceled. Re-run the command to select a different account/project.`
);
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
@@ -313,18 +293,11 @@ export default class ProjectInit extends EasCommand {
);
}
if (!force) {
if (nonInteractive) {
throw new Error(
`Project does not exist: ${projectFullName}. Use --force flag to create this project.`
);
}
const affirmedCreate = await confirmAsync({
message: `Would you like to create a project for ${projectFullName}?`,
});
if (!affirmedCreate) {
throw new Error(`Project ID configuration canceled for ${projectFullName}.`);
}
const affirmedCreate = await confirmAsync({
message: `Would you like to create a project for ${projectFullName}?`,
});
if (!affirmedCreate) {
throw new Error(`Project ID configuration canceled for ${projectFullName}.`);
}
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
@@ -432,14 +405,10 @@ export default class ProjectInit extends EasCommand {
});
idForConsistency = idArgument;
} else {
idForConsistency = await ProjectInit.initializeWithoutExplicitIDAsync(
idForConsistency = await ProjectInit.initializeWithInteractiveSelectionAsync(
graphqlClient,
actor,
projectDir,
{
force,
nonInteractive,
}
projectDir
);
}
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform } from '@expo/eas-build-job';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import { EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
@@ -411,7 +411,6 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({
const easJson: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
},
build: {
development: {
@@ -432,7 +431,6 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({
},
production: {
channel: 'production',
autoIncrement: true,
...easBuildGitHubConfig,
},
},
@@ -1,217 +0,0 @@
import { Platform, SubmissionConfig } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import nullthrows from 'nullthrows';
import { z } from 'zod';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
AndroidSubmissionConfigInput,
IosSubmissionConfigInput,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
} from '../../graphql/generated';
import { AppStoreConnectApiKeyQuery } from '../../graphql/queries/AppStoreConnectApiKeyQuery';
import { GoogleServiceAccountKeyQuery } from '../../graphql/queries/GoogleServiceAccountKeyQuery';
import AndroidSubmitCommand from '../../submit/android/AndroidSubmitCommand';
import { SubmissionContext, createSubmissionContextAsync } from '../../submit/context';
import IosSubmitCommand from '../../submit/ios/IosSubmitCommand';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
/**
* This command will be run on the EAS workers.
* This command resolves credentials and other
* configuration, that normally would be included in the
* job and metadata objects, and prints them to stdout.
*/
export default class SubmitInternal extends EasCommand {
static override hidden = true;
static override flags = {
platform: Flags.enum<Platform>({
options: [Platform.ANDROID, Platform.IOS],
required: true,
}),
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
id: Flags.string({
description: 'ID of the build to submit',
required: true,
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(SubmitInternal);
// This command is always run with implicit --non-interactive and --json options
enableJsonOutput();
const {
loggedIn: { actor, graphqlClient },
privateProjectConfig: { exp, projectId, projectDir },
analytics,
vcsClient,
} = await this.getContextAsync(SubmitInternal, {
nonInteractive: true,
vcsClientOverride: new GitNoCommitClient(),
});
const submissionProfile = await EasJsonUtils.getSubmitProfileAsync(
EasJsonAccessor.fromProjectPath(projectDir),
flags.platform,
flags.profile
);
const ctx = await createSubmissionContextAsync({
platform: flags.platform,
projectDir,
profile: submissionProfile,
archiveFlags: {
id: flags.id,
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
actor,
graphqlClient,
analytics,
exp,
projectId,
vcsClient,
specifiedProfile: flags.profile,
});
let config;
if (ctx.platform === Platform.IOS) {
const command = new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
const submitter = await command.runAsync();
const mutationInput = await submitter.getSubmissionInputAsync();
const iosConfig = mutationInput.submissionConfig;
const ascApiKeyJson = await getAppStoreConnectApiKeyJsonAsync({
iosConfig,
graphqlClient,
});
const configInput: z.input<typeof SubmissionConfig.Ios.SchemaZ> = {
ascAppIdentifier: iosConfig.ascAppIdentifier,
isVerboseFastlaneEnabled: iosConfig.isVerboseFastlaneEnabled ?? undefined,
...(ascApiKeyJson
? { ascApiJsonKey: ascApiKeyJson }
: {
appleIdUsername: nullthrows(iosConfig.appleIdUsername),
appleAppSpecificPassword: nullthrows(iosConfig.appleAppSpecificPassword),
}),
};
config = SubmissionConfig.Ios.SchemaZ.parse(configInput);
} else if (ctx.platform === Platform.ANDROID) {
const command = new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>);
const submitter = await command.runAsync();
const mutationInput = await submitter.getSubmissionInputAsync();
const androidConfig = mutationInput.submissionConfig;
const changesNotSentForReview = androidConfig.changesNotSentForReview ?? undefined;
const releaseStatus = androidConfig.releaseStatus
? graphQlReleaseStatusToConfigReleaseStatus[androidConfig.releaseStatus]
: undefined;
const googleServiceAccountKeyJson = nullthrows(
await getGoogleServiceAccountKeyJsonAsync({
androidConfig,
graphqlClient,
})
);
const track = graphQlTrackToConfigTrack[androidConfig.track];
const configInput: z.input<typeof SubmissionConfig.Android.SchemaZ> = {
changesNotSentForReview,
googleServiceAccountKeyJson,
track,
...(releaseStatus === SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS
? {
releaseStatus: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
rollout: androidConfig.rollout ?? undefined,
}
: { releaseStatus }),
};
config = SubmissionConfig.Android.SchemaZ.parse(configInput);
} else {
throw new Error(`Unsupported platform: ${ctx.platform}`);
}
printJsonOnlyOutput({ config });
}
}
async function getGoogleServiceAccountKeyJsonAsync({
androidConfig,
graphqlClient,
}: {
androidConfig: AndroidSubmissionConfigInput;
graphqlClient: ExpoGraphqlClient;
}): Promise<string | null> {
if (androidConfig.googleServiceAccountKeyJson) {
return androidConfig.googleServiceAccountKeyJson;
} else if (androidConfig.googleServiceAccountKeyId) {
const key = await GoogleServiceAccountKeyQuery.getByIdAsync(
graphqlClient,
androidConfig.googleServiceAccountKeyId
);
return key.keyJson;
}
return null;
}
async function getAppStoreConnectApiKeyJsonAsync({
iosConfig,
graphqlClient,
}: {
iosConfig: IosSubmissionConfigInput;
graphqlClient: ExpoGraphqlClient;
}): Promise<string | null> {
if (iosConfig.ascApiKey) {
return JSON.stringify({
key_id: iosConfig.ascApiKey.keyIdentifier,
issuer_id: iosConfig.ascApiKey.issuerIdentifier,
key: iosConfig.ascApiKey.keyP8,
});
} else if (iosConfig.ascApiKeyId) {
const key = await AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, iosConfig.ascApiKeyId);
return JSON.stringify({
key_id: key.keyIdentifier,
issuer_id: key.issuerIdentifier,
key: key.keyP8,
});
}
return null;
}
const graphQlReleaseStatusToConfigReleaseStatus = {
[SubmissionAndroidReleaseStatus.Draft]: SubmissionConfig.Android.ReleaseStatus.DRAFT,
[SubmissionAndroidReleaseStatus.InProgress]: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
[SubmissionAndroidReleaseStatus.Completed]: SubmissionConfig.Android.ReleaseStatus.COMPLETED,
[SubmissionAndroidReleaseStatus.Halted]: SubmissionConfig.Android.ReleaseStatus.HALTED,
};
const graphQlTrackToConfigTrack = {
[SubmissionAndroidTrack.Production]: SubmissionConfig.Android.ReleaseTrack.PRODUCTION,
[SubmissionAndroidTrack.Beta]: SubmissionConfig.Android.ReleaseTrack.BETA,
[SubmissionAndroidTrack.Alpha]: SubmissionConfig.Android.ReleaseTrack.ALPHA,
[SubmissionAndroidTrack.Internal]: SubmissionConfig.Android.ReleaseTrack.INTERNAL,
};
@@ -21,7 +21,7 @@ import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { resolveVcsClient } from '../../../vcs';
const projectRoot = '/test-project';
@@ -47,7 +47,7 @@ jest.mock('@expo/config-plugins');
jest.mock('../../../branch/queries');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
@@ -106,9 +106,7 @@ describe(UpdatePublish.name, () => {
const { projectId } = mockTestProject();
const { platforms, runtimeVersion } = mockTestExport();
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: 'branch123', branchName: 'branchFromChannel' });
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
@@ -140,16 +138,7 @@ describe(UpdatePublish.name, () => {
// Add configuration to the project that should not be included in the update
const { appJson } = mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
expoConfig: {},
});
const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });
@@ -11,9 +11,8 @@ import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { BranchQuery } from '../../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { selectUpdateGroupOnBranchAsync } from '../../../update/queries';
import {
getCodeSigningInfoAsync,
@@ -45,8 +44,7 @@ jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../graphql/queries/BranchQuery');
jest.mock('../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../update/queries');
jest.mock('../../../ora', () => ({
ora: () => ({
@@ -59,7 +57,6 @@ jest.mock('../../../fetch');
describe(UpdateRepublish.name, () => {
afterEach(() => {
vol.reset();
jest.resetAllMocks();
});
it('errors when providing both --group and --branch', async () => {
@@ -92,16 +89,6 @@ describe(UpdateRepublish.name, () => {
);
});
it('errors when providing both --destination-channel and --destination-branch', async () => {
const flags = ['--group=1234', '--destination-channel=test', '--destination-branch=wat'];
mockTestProject();
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
/--destination-branch=wat cannot also be provided when using --destination-channel/
);
});
it('errors when update group is empty', async () => {
const flags = ['--group=1234'];
@@ -284,9 +271,7 @@ describe(UpdateRepublish.name, () => {
mockTestProject();
// Mock resolving the channel to branch name, only valid for a single branch connected
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: updateStub.branch.id, branchName: 'branchFromChannel' });
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
@@ -321,94 +306,6 @@ describe(UpdateRepublish.name, () => {
])
);
});
it('republishes to a different branch when --destination-branch is supplied', async () => {
const flags = ['--branch=branch123', '--message=test-republish', '--destination-branch=other'];
mockTestProject();
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
branch: { id: 'other-id', name: 'other' },
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
jest.mocked(BranchQuery.getBranchByNameAsync).mockResolvedValue({
id: 'other-id',
name: 'other',
});
await new UpdateRepublish(flags, commandOptions).run();
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.objectContaining({ branchName: 'branch123' })
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: 'other-id',
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
});
it('republishes to a different branch when --destination-channel is supplied', async () => {
const flags = ['--branch=branch123', '--message=test-republish', '--destination-channel=blah'];
mockTestProject();
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
branch: { id: 'other-id', name: 'other' },
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: 'blah-id', branchName: 'blah' });
await new UpdateRepublish(flags, commandOptions).run();
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.objectContaining({ branchName: 'branch123' })
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: 'blah-id',
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
});
});
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
@@ -19,7 +19,7 @@ import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { resolveVcsClient } from '../../../vcs';
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
@@ -50,7 +50,7 @@ jest.mock('../../../project/projectUtils', () => ({
enforceRollBackToEmbeddedUpdateSupportAsync: jest.fn(),
}));
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
@@ -112,9 +112,7 @@ describe(UpdateRollBackToEmbedded.name, () => {
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: updateStub.branch.id, branchName: 'branchFromChannel' });
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
@@ -146,16 +144,7 @@ describe(UpdateRollBackToEmbedded.name, () => {
// Add configuration to the project that should not be included in the update
mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
expoConfig: {},
});
const platforms = ['ios'];
+38 -66
View File
@@ -4,7 +4,6 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { transformFingerprintSource } from '../../build/graphql';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
@@ -22,7 +21,6 @@ import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
RawAsset,
@@ -34,9 +32,9 @@ import {
filterCollectedAssetsByRequestedPlatforms,
generateEasMetadataAsync,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeToUpdateRolloutInfoGroupMappingAsync,
getRuntimeVersionInfoObjectsAsync,
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
platformDisplayNames,
@@ -376,7 +374,7 @@ export default class UpdatePublish extends EasCommand {
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersionInfoObjects = await getRuntimeVersionInfoObjectsAsync({
const runtimeVersions = await getRuntimeVersionObjectAsync({
exp,
platforms: realizedPlatforms,
projectDir,
@@ -386,40 +384,21 @@ export default class UpdatePublish extends EasCommand {
},
env: undefined,
});
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects
);
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping = await Promise.all(
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
? (
await maybeUploadFingerprintAsync({
runtimeVersion: info.runtimeVersion,
fingerprint: info.fingerprint,
graphqlClient,
})
).fingerprintSource ?? null
: null,
};
})
);
const runtimeVersionToRolloutInfoGroup =
rolloutPercentage !== undefined
? await getRuntimeToUpdateRolloutInfoGroupMappingAsync(graphqlClient, {
appId: projectId,
branchName,
rolloutPercentage,
runtimeToPlatformsAndFingerprintInfoMapping,
runtimeToPlatformMapping,
})
: undefined;
@@ -427,43 +406,39 @@ export default class UpdatePublish extends EasCommand {
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(
({ runtimeVersion, platforms, fingerprintSource }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
const rolloutInfoGroupForRuntimeVersion = runtimeVersionToRolloutInfoGroup
? runtimeVersionToRolloutInfoGroup.get(runtimeVersion)
: null;
const localRolloutInfoGroup = rolloutInfoGroupForRuntimeVersion
? Object.fromEntries(
platforms.map(platform => [
platform,
rolloutInfoGroupForRuntimeVersion[platform as keyof UpdateRolloutInfoGroup],
])
)
: null;
const rolloutInfoGroupForRuntimeVersion = runtimeVersionToRolloutInfoGroup
? runtimeVersionToRolloutInfoGroup.get(runtimeVersion)
: null;
const localRolloutInfoGroup = rolloutInfoGroupForRuntimeVersion
? Object.fromEntries(
platforms.map(platform => [
platform,
rolloutInfoGroupForRuntimeVersion[platform as keyof UpdateRolloutInfoGroup],
])
)
: null;
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
runtimeFingerprintSource: fingerprintSource
? transformFingerprintSource(fingerprintSource)
: null,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
@@ -538,10 +513,7 @@ export default class UpdatePublish extends EasCommand {
Log.addNewLineIfNone();
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
@@ -7,11 +7,10 @@ import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { UpdateToRepublish, republishAsync } from '../../update/republish';
import { truncateString as truncateUpdateMessage } from '../../update/utils';
@@ -23,8 +22,6 @@ const defaultRepublishPlatforms: Platform[] = ['android', 'ios'];
type UpdateRepublishRawFlags = {
branch?: string;
channel?: string;
'destination-channel'?: string;
'destination-branch'?: string;
group?: string;
message?: string;
platform: string;
@@ -36,8 +33,6 @@ type UpdateRepublishRawFlags = {
type UpdateRepublishFlags = {
branchName?: string;
channelName?: string;
destinationChannelName?: string;
destinationBranchName?: string;
groupId?: string;
updateMessage?: string;
platform: Platform[];
@@ -62,15 +57,6 @@ export default class UpdateRepublish extends EasCommand {
description: 'Update group ID to republish',
exclusive: ['branch', 'channel'],
}),
'destination-channel': Flags.string({
description:
'Channel name to select a branch to republish to if republishing to a different branch',
exclusive: ['destination-branch'],
}),
'destination-branch': Flags.string({
description: 'Branch name to republish to if republishing to a different branch',
exclusive: ['destination-channel'],
}),
message: Flags.string({
char: 'm',
description: 'Short message describing the republished update group',
@@ -144,21 +130,13 @@ export default class UpdateRepublish extends EasCommand {
);
}
const arbitraryUpdate = updatesToPublish[0];
const targetBranch = await getOrAskTargetBranchAsync(
graphqlClient,
projectId,
flags,
arbitraryUpdate
);
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
const arbitraryUpdate = updatesToPublish[0];
await republishAsync({
graphqlClient,
app: { exp, projectId },
updatesToPublish,
targetBranch,
targetBranch: { branchId: arbitraryUpdate.branchId, branchName: arbitraryUpdate.branchName },
updateMessage,
codeSigningInfo,
json: flags.json,
@@ -169,8 +147,6 @@ export default class UpdateRepublish extends EasCommand {
const branchName = rawFlags.branch;
const channelName = rawFlags.channel;
const groupId = rawFlags.group;
const destinationChannelName = rawFlags['destination-channel'];
const destinationBranchName = rawFlags['destination-branch'];
const nonInteractive = rawFlags['non-interactive'];
const privateKeyPath = rawFlags['private-key-path'];
@@ -184,8 +160,6 @@ export default class UpdateRepublish extends EasCommand {
return {
branchName,
channelName,
destinationChannelName,
destinationBranchName,
groupId,
platform,
updateMessage: rawFlags.message,
@@ -196,34 +170,6 @@ export default class UpdateRepublish extends EasCommand {
}
}
async function getOrAskTargetBranchAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: UpdateRepublishFlags,
arbitraryUpdate: UpdateToRepublish
): Promise<{ branchId: string; branchName: string }> {
// if branch name supplied, use that
if (flags.destinationBranchName) {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId: projectId,
name: flags.destinationBranchName,
});
return { branchId: branch.id, branchName: branch.name };
}
// if provided channel name but was non-interactive
if (flags.destinationChannelName) {
return await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
flags.destinationChannelName
);
}
// if neither provided, assume republish on same branch
return { branchId: arbitraryUpdate.branchId, branchName: arbitraryUpdate.branchName };
}
/** Retrieve the update group from either the update group id, or select from branch name. */
async function getOrAskUpdatesAsync(
graphqlClient: ExpoGraphqlClient,
@@ -347,11 +293,7 @@ async function askUpdatesFromChannelNameAsync(
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelName
);
const branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
projectId,
@@ -24,12 +24,11 @@ import {
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import {
RuntimeVersionInfo,
UpdatePublishPlatform,
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
getRuntimeVersionInfoObjectsAsync,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
@@ -194,7 +193,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersionInfoObjects = await getRuntimeVersionInfoObjectsAsync({
const runtimeVersions = await getRuntimeVersionObjectAsync({
exp,
platforms: realizedPlatforms,
projectDir,
@@ -204,10 +203,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
},
env: undefined,
});
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
@@ -219,7 +214,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
updateMessage,
branchId,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
runtimeVersions,
realizedPlatforms,
});
publishSpinner.succeed('Published!');
@@ -240,10 +235,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
Log.addNewLineIfNone();
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
@@ -298,7 +290,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
updateMessage,
branchId,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
runtimeVersions,
realizedPlatforms,
}: {
graphqlClient: ExpoGraphqlClient;
@@ -307,17 +299,17 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
updateMessage: string | undefined;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
runtimeToPlatformsAndFingerprintInfoMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
})[];
runtimeVersions: { platform: UpdatePublishPlatform; runtimeVersion: string }[];
realizedPlatforms: PublishPlatform[];
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const rollbackInfoGroups = Object.fromEntries(
realizedPlatforms.map(platform => [platform, true])
);
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformsAndFingerprintInfoMapping.map(
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const localRollbackInfoGroup = Object.fromEntries(
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
@@ -1,233 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { WorkerDeploymentAliasFragment } from '../../graphql/generated';
import Log from '../../log';
import { Ora, ora } from '../../ora';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import {
assignWorkerDeploymentAliasAsync,
assignWorkerDeploymentProductionAsync,
selectWorkerDeploymentOnAppAsync,
} from '../../worker/deployment';
import { formatWorkerDeploymentJson, formatWorkerDeploymentTable } from '../../worker/utils/logs';
interface DeployAliasFlags {
nonInteractive: boolean;
json: boolean;
aliasName?: string | null;
deploymentIdentifier?: string | null;
isProduction: boolean;
}
interface RawDeployAliasFlags {
'non-interactive': boolean;
json: boolean;
alias?: string;
id?: string;
prod: boolean;
}
export default class WorkerAlias extends EasCommand {
static override description = 'Assign deployment aliases';
static override aliases = ['deploy:alias', 'deploy:promote'];
// TODO(@kitten): Keep command hidden until worker deployments are live
static override hidden = true;
static override state = 'beta';
static override flags = {
prod: Flags.boolean({
aliases: ['production'],
description: 'Promote an existing deployment to production.',
default: false,
}),
alias: Flags.string({
description: 'Custom alias to assign to the existing deployment.',
helpValue: 'name',
required: false,
}),
id: Flags.string({
description: 'Unique identifier of an existing deployment.',
helpValue: 'xyz123',
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
override async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(WorkerAlias);
const flags = this.sanitizeFlags(rawFlags);
if (flags.json) {
enableJsonOutput();
}
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkerAlias, {
nonInteractive: true,
});
const { projectId } = await getDynamicPrivateProjectConfigAsync();
const aliasName = await resolveDeploymentAliasAsync(flags);
const deploymentId = await resolveDeploymentIdAsync({
...flags,
graphqlClient,
projectId,
aliasName,
});
let progress: null | Ora = null;
let deploymentAlias: null | Awaited<ReturnType<typeof assignWorkerDeploymentAliasAsync>> = null;
if (aliasName) {
try {
progress = ora(chalk`Assigning alias {bold ${aliasName}} to deployment`).start();
deploymentAlias = await assignWorkerDeploymentAliasAsync({
graphqlClient,
appId: projectId,
deploymentId,
aliasName,
});
progress.text = chalk`Assigned alias {bold ${aliasName}} to deployment`;
} catch (error: any) {
progress?.fail(chalk`Failed to assign {bold ${aliasName}} alias to deployment`);
throw error;
}
}
let deploymentProdAlias: null | Awaited<
ReturnType<typeof assignWorkerDeploymentProductionAsync>
> = null;
if (flags.isProduction) {
try {
progress = (progress ?? ora()).start(chalk`Promoting deployment to {bold production}`);
deploymentProdAlias = await assignWorkerDeploymentProductionAsync({
graphqlClient,
appId: projectId,
deploymentId,
});
progress.text = chalk`Promoted deployment to {bold production}`;
} catch (error: any) {
progress?.fail(chalk`Failed to promote deployment to {bold production}`);
throw error;
}
}
progress?.succeed(
!deploymentAlias
? chalk`Promoted deployment to {bold production}`
: chalk`Promoted deployment to {bold production} with alias {bold ${deploymentAlias.aliasName}}`
);
// Either use the alias, or production deployment information
const deployment = deploymentAlias?.workerDeployment ?? deploymentProdAlias?.workerDeployment;
if (flags.json) {
printJsonOnlyOutput(
formatWorkerDeploymentJson({
projectId,
deployment: deployment!,
aliases: [deploymentAlias].filter(Boolean) as WorkerDeploymentAliasFragment[],
production: deploymentProdAlias,
})
);
return;
}
Log.addNewLineIfNone();
Log.log(`🎉 Your deployment is modified`);
Log.addNewLineIfNone();
Log.log(
formatWorkerDeploymentTable({
projectId,
deployment: deployment!,
aliases: [deploymentAlias].filter(Boolean) as WorkerDeploymentAliasFragment[],
production: deploymentProdAlias,
})
);
}
private sanitizeFlags(flags: RawDeployAliasFlags): DeployAliasFlags {
return {
nonInteractive: flags['non-interactive'],
json: flags['json'],
aliasName: flags.alias?.trim().toLowerCase(),
deploymentIdentifier: flags.id?.trim().toLowerCase(),
isProduction: flags.prod,
};
}
}
async function resolveDeploymentAliasAsync(flags: DeployAliasFlags): Promise<string | null> {
if (flags.aliasName?.trim()) {
return flags.aliasName.trim().toLowerCase();
}
// Skip alias prompt when promoting deployments to prod
if (flags.isProduction) {
return null;
}
if (flags.nonInteractive) {
throw new Error('The `--alias` flag must be set when running in `--non-interactive` mode.');
}
const { alias: aliasName } = await promptAsync({
type: 'text',
name: 'alias',
message: 'Enter the alias to assign to a deployment',
validate: (value: string) => !!value.trim(),
hint: 'The alias name is case insensitive and must be URL safe',
});
return aliasName.trim().toLowerCase();
}
async function resolveDeploymentIdAsync({
graphqlClient,
deploymentIdentifier,
aliasName,
projectId,
nonInteractive,
}: DeployAliasFlags & {
graphqlClient: ExpoGraphqlClient;
projectId: string;
}): Promise<string> {
if (deploymentIdentifier) {
return deploymentIdentifier;
}
if (nonInteractive) {
throw new Error('The `--id` flag must be set when running in `--non-interactive` mode.');
}
const deployment = await selectWorkerDeploymentOnAppAsync({
graphqlClient,
appId: projectId,
selectTitle: chalk`deployment to assign the {underline ${aliasName}} alias`,
});
if (!deployment) {
throw new Error(
'No deployments found for this project, create a new deployment with "eas deploy"'
);
}
return deployment.deploymentIdentifier;
}
@@ -1,409 +0,0 @@
import { format as formatTimeAgo } from '@expo/timeago.js';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import fs from 'node:fs';
import * as path from 'node:path';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { createProgressTracker } from '../../utils/progress';
import * as WorkerAssets from '../../worker/assets';
import {
assignWorkerDeploymentAliasAsync,
assignWorkerDeploymentProductionAsync,
getSignedDeploymentUrlAsync,
} from '../../worker/deployment';
import { UploadParams, batchUploadAsync, uploadAsync } from '../../worker/upload';
import {
formatWorkerDeploymentJson,
formatWorkerDeploymentTable,
getDeploymentUrlFromFullName,
} from '../../worker/utils/logs';
const isDirectory = (directoryPath: string): Promise<boolean> =>
fs.promises
.stat(directoryPath)
.then(stat => stat.isDirectory())
.catch(() => false);
interface DeployFlags {
nonInteractive: boolean;
json: boolean;
isProduction: boolean;
aliasName?: string;
environment?: EnvironmentVariableEnvironment;
deploymentIdentifier?: string;
exportDir: string;
}
interface RawDeployFlags {
'non-interactive': boolean;
environment?: string;
json: boolean;
prod: boolean;
alias?: string;
id?: string;
'export-dir': string;
}
export default class WorkerDeploy extends EasCommand {
static override description = 'Deploy your Expo web build';
static override aliases = ['deploy'];
static override usage = [chalk`deploy {dim [options]}`, `deploy --prod`];
// TODO(@kitten): Keep command hidden until worker deployments are live
static override hidden = true;
static override state = 'beta';
static override flags = {
prod: Flags.boolean({
aliases: ['production'],
description: 'Create a new production deployment.',
default: false,
}),
alias: Flags.string({
description: 'Custom alias to assign to the new deployment.',
helpValue: 'name',
}),
id: Flags.string({
description: 'Custom unique identifier for the new deployment.',
helpValue: 'xyz123',
}),
'export-dir': Flags.string({
description: 'Directory where the Expo project was exported.',
helpValue: 'dir',
default: 'dist',
}),
environment: {
...EASEnvironmentFlag.environment,
description: 'Deploy with EAS Environment Variables matching the specified environment.',
},
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(WorkerDeploy);
const flags = this.sanitizeFlags(rawFlags);
if (flags.json) {
enableJsonOutput();
}
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
projectDir,
} = await this.getContextAsync(WorkerDeploy, flags);
const projectDist = await resolveExportedProjectAsync(flags, projectDir);
const { projectId } = await getDynamicPrivateProjectConfigAsync();
logExportedProjectInfo(projectDist);
async function* emitWorkerTarballAsync(params: {
assetMap: WorkerAssets.AssetMap;
manifest: WorkerAssets.Manifest;
}): AsyncGenerator<WorkerAssets.FileEntry> {
yield ['assets.json', JSON.stringify(params.assetMap)];
yield ['manifest.json', JSON.stringify(params.manifest)];
if (projectDist.type === 'server' && projectDist.serverPath) {
const workerFiles = WorkerAssets.listWorkerFilesAsync(projectDist.serverPath);
for await (const workerFile of workerFiles) {
yield [`server/${workerFile.normalizedPath}`, workerFile.data];
}
}
}
async function uploadTarballAsync(tarPath: string, uploadUrl: string): Promise<any> {
const { response } = await uploadAsync({
url: uploadUrl,
filePath: tarPath,
compress: false,
headers: {
accept: 'application/json',
},
});
if (response.status === 413) {
throw new Error(
'Upload failed! (Payload too large)\n' +
`The files in "${path.relative(
projectDir,
projectDist.path
)}" (at: ${projectDir}) exceed the maximum file size (10MB gzip).`
);
} else if (!response.ok) {
throw new Error(`Upload failed! (${response.statusText})`);
} else {
const json = await response.json();
if (!json.success || !json.result || typeof json.result !== 'object') {
throw new Error(json.message ? `Upload failed: ${json.message}` : 'Upload failed!');
}
return json.result;
}
}
async function uploadAssetsAsync(
assetMap: WorkerAssets.AssetMap,
uploads: Record<string, string>
): Promise<void> {
if (typeof uploads !== 'object' || !uploads) {
return;
}
// TODO(@kitten): Batch and upload multiple files in parallel
const uploadParams: UploadParams[] = [];
const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
if (!assetPath) {
return;
}
for await (const asset of WorkerAssets.listAssetMapFilesAsync(assetPath, assetMap)) {
const uploadURL = uploads[asset.normalizedPath];
if (uploadURL) {
uploadParams.push({ url: uploadURL, filePath: asset.path });
}
}
const progress = {
total: uploadParams.length,
pending: 0,
percent: 0,
transferred: 0,
};
const updateProgress = createProgressTracker({
total: progress.total,
message(ratio) {
const percent = `${Math.floor(ratio * 100)}`;
const details = chalk.dim(
`(${progress.pending} Pending, ${progress.transferred} Completed, ${progress.total} Total)`
);
return `Uploading assets: ${percent.padStart(3)}% ${details}`;
},
completedMessage: 'Uploaded assets',
});
try {
for await (const signal of batchUploadAsync(uploadParams)) {
if ('response' in signal) {
progress.pending--;
progress.percent = ++progress.transferred / progress.total;
} else {
progress.pending++;
}
updateProgress({ progress });
}
} catch (error: any) {
updateProgress({ isComplete: true, error });
throw error;
}
updateProgress({ isComplete: true });
}
let assetMap: WorkerAssets.AssetMap;
let tarPath: string;
let deployResult: any;
let progress = ora('Preparing project').start();
try {
const manifest = await WorkerAssets.createManifestAsync(
{
environment: flags.environment,
projectDir,
projectId,
},
graphqlClient
);
assetMap = await WorkerAssets.createAssetMapAsync(
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path
);
tarPath = await WorkerAssets.packFilesIterableAsync(
emitWorkerTarballAsync({
assetMap,
manifest,
})
);
const uploadUrl = await getSignedDeploymentUrlAsync(graphqlClient, {
appId: projectId,
deploymentIdentifier: flags.deploymentIdentifier,
// NOTE(cedric): this function might ask the user for a dev-domain name,
// when that happens, no ora spinner should be running.
onSetupDevDomain: () => progress.stop(),
nonInteractive: flags.nonInteractive,
});
progress.start('Creating deployment');
deployResult = await uploadTarballAsync(tarPath, uploadUrl);
progress.succeed('Created deployment');
} catch (error: any) {
progress.fail('Failed to create deployment');
throw error;
}
await uploadAssetsAsync(assetMap, deployResult.uploads);
let deploymentAlias: null | Awaited<ReturnType<typeof assignWorkerDeploymentAliasAsync>> = null;
if (flags.aliasName) {
progress = ora(chalk`Assigning alias {bold ${flags.aliasName}} to deployment`).start();
try {
deploymentAlias = await assignWorkerDeploymentAliasAsync({
graphqlClient,
appId: projectId,
deploymentId: deployResult.id,
aliasName: flags.aliasName,
});
// Only stop the spinner when not promoting to production
if (!flags.isProduction) {
progress.succeed(chalk`Assigned alias {bold ${flags.aliasName}} to deployment`);
}
} catch (error: any) {
progress.fail(chalk`Failed to assign {bold ${flags.aliasName}} alias to deployment`);
throw error;
}
}
let deploymentProdAlias: null | Awaited<
ReturnType<typeof assignWorkerDeploymentProductionAsync>
> = null;
if (flags.isProduction) {
try {
if (!flags.aliasName) {
progress = ora(chalk`Promoting deployment to {bold production}`).start();
} else {
progress.text = chalk`Promoting deployment to {bold production}`;
}
deploymentProdAlias = await assignWorkerDeploymentProductionAsync({
graphqlClient,
appId: projectId,
deploymentId: deployResult.id,
});
progress.succeed(
!flags.aliasName
? chalk`Promoted deployment to {bold production}`
: chalk`Promoted deployment to {bold production} with alias {bold ${flags.aliasName}}`
);
} catch (error: any) {
progress.fail('Failed to promote deployment to production');
throw error;
}
}
if (flags.json) {
printJsonOnlyOutput(
formatWorkerDeploymentJson({
projectId,
deployment: {
deploymentIdentifier: deployResult.id,
url: getDeploymentUrlFromFullName(deployResult.fullName),
},
aliases: [deploymentAlias],
production: deploymentProdAlias,
})
);
return;
}
Log.addNewLineIfNone();
Log.log(`🎉 Your deployment is ready`);
Log.addNewLineIfNone();
Log.log(
formatWorkerDeploymentTable({
projectId,
deployment: {
deploymentIdentifier: deployResult.id,
url: getDeploymentUrlFromFullName(deployResult.fullName),
},
aliases: [deploymentAlias],
production: deploymentProdAlias,
})
);
if (!deploymentProdAlias) {
Log.addNewLineIfNone();
Log.log('🚀 When you are ready to deploy to production:');
Log.log(chalk` $ eas deploy {bold --prod}`);
}
}
private sanitizeFlags(flags: RawDeployFlags): DeployFlags {
return {
nonInteractive: flags['non-interactive'],
json: flags['json'],
isProduction: !!flags.prod,
aliasName: flags.alias?.trim().toLowerCase(),
deploymentIdentifier: flags.id?.trim(),
exportDir: flags['export-dir'],
};
}
}
async function resolveExportedProjectAsync(
flags: DeployFlags,
projectDir: string
): Promise<
| { type: 'static'; modifiedAt: Date | null; path: string }
| {
type: 'server';
modifiedAt: Date | null;
path: string;
serverPath: string;
clientPath?: string;
}
> {
const exportPath = path.join(projectDir, flags.exportDir);
const serverPath = path.join(exportPath, 'server');
const clientPath = path.join(exportPath, 'client');
const [exportDirStat, expoRoutesStat, hasClientDir] = await Promise.all([
fs.promises.stat(exportPath).catch(() => null),
fs.promises.stat(path.join(serverPath, '_expo/routes.json')).catch(() => null),
isDirectory(clientPath),
]);
if (!exportDirStat?.isDirectory()) {
throw new Error(
`No "${flags.exportDir}/" folder found. Prepare your project for deployment with "npx expo export --platform web"`
);
}
if (expoRoutesStat?.isFile()) {
return {
type: 'server',
path: exportPath,
modifiedAt: exportDirStat.mtime,
serverPath,
clientPath: hasClientDir ? clientPath : undefined,
};
}
return { type: 'static', path: exportPath, modifiedAt: exportDirStat.mtime };
}
function logExportedProjectInfo(
project: Awaited<ReturnType<typeof resolveExportedProjectAsync>>
): void {
let modifiedAgo = '';
// Only show the timestamp for exports older than 1 minute
if (project.modifiedAt && Date.now() - project.modifiedAt.getTime() > 60_000) {
modifiedAgo = ` - exported ${formatTimeAgo(project.modifiedAt)}`;
}
Log.log(chalk`{dim > Project export: ${project.type}${modifiedAgo}}`);
}
@@ -747,47 +747,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
entitlement: 'com.apple.developer.journal.allow',
name: 'Journaling Suggestions',
capability: CapabilityType.JOURNALING_SUGGESTIONS,
validateOptions: createValidateStringArrayOptions(['suggestions']),
getOptions: getDefinedOptions,
},
{
entitlement: 'com.apple.developer.managed-app-distribution.install-ui',
name: 'Managed App Installation UI',
capability: CapabilityType.MANAGED_APP_INSTALLATION_UI,
validateOptions: createValidateStringArrayOptions(['managed-app']),
getOptions: getDefinedOptions,
},
{
entitlement: 'com.apple.developer.networking.slicing.appcategory',
name: '5G Network Slicing',
capability: CapabilityType.NETWORK_SLICING,
validateOptions: createValidateStringArrayOptions([
'gaming-6014',
'communication-9000',
'streaming-9001',
]),
getOptions: getDefinedOptions,
},
{
entitlement: 'com.apple.developer.networking.slicing.trafficcategory',
name: '5G Network Slicing',
capability: CapabilityType.NETWORK_SLICING,
validateOptions: createValidateStringArrayOptions([
'defaultslice-1',
'video-2',
'background-3',
'voice-4',
'callsignaling-5',
'responsivedata-6',
'avstreaming-7',
'responsiveav-8',
]),
getOptions: getDefinedOptions,
},
// VMNET
// These don't appear to have entitlements, so it's unclear how we can automatically enable / disable them at this time.
@@ -36,7 +36,6 @@ export enum AndroidActionType {
UpdateCredentialsJson,
SetUpBuildCredentialsFromCredentialsJson,
SetUpBuildCredentials,
Exit,
}
export enum IosActionType {
@@ -61,5 +60,4 @@ export enum IosActionType {
UseExistingAscApiKeyForSubmissions,
CreateAscApiKeyForSubmissions,
RemoveAscApiKey,
Exit,
}
@@ -26,11 +26,6 @@ export const highLevelActions: ActionInfo[] = [
title: 'Go back',
scope: Scope.Manager,
},
{
value: AndroidActionType.Exit,
title: 'Exit',
scope: Scope.Manager,
},
];
export const credentialsJsonActions: ActionInfo[] = [
@@ -27,11 +27,6 @@ export const highLevelActions: ActionInfo[] = [
title: 'Go back',
scope: Scope.Manager,
},
{
value: IosActionType.Exit,
title: 'Exit',
scope: Scope.Manager,
},
];
export const credentialsJsonActions: ActionInfo[] = [
@@ -145,8 +145,6 @@ export class ManageAndroid {
} else if (chosenAction === AndroidActionType.GoBackToCaller) {
await this.callingAction.runAsync(ctx);
return;
} else if (chosenAction === AndroidActionType.Exit) {
return;
}
}
@@ -154,8 +154,6 @@ export class ManageIos {
} else if (chosenAction === IosActionType.GoBackToCaller) {
await this.callingAction.runAsync(ctx);
return;
} else if (chosenAction === IosActionType.Exit) {
return;
}
} else if (actionInfo.scope === Scope.Project) {
assert(
+118 -783
View File
File diff suppressed because it is too large. Load diff
@@ -4,7 +4,6 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
CreateBulkEnvironmentVariablesForAppMutation,
CreateEnvironmentVariableForAccountMutation,
CreateEnvironmentVariableForAppMutation,
DeleteEnvironmentVariableMutation,
@@ -197,34 +196,4 @@ export const EnvironmentVariableMutation = {
return data.environmentVariable.deleteEnvironmentVariable;
},
async createBulkEnvironmentVariablesForAppAsync(
graphqlClient: ExpoGraphqlClient,
input: EnvironmentVariablePushInput[],
appId: string
): Promise<boolean> {
await withErrorHandlingAsync(
graphqlClient
.mutation<CreateBulkEnvironmentVariablesForAppMutation>(
gql`
mutation CreateBulkEnvironmentVariablesForApp(
$input: [CreateEnvironmentVariableInput!]!
$appId: ID!
) {
environmentVariable {
createBulkEnvironmentVariablesForApp(
environmentVariablesData: $input
appId: $appId
) {
id
}
}
}
`,
{ input, appId }
)
.toPromise()
);
return true;
},
};
@@ -1,50 +0,0 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
AppStoreConnectApiKeyByIdQuery,
AppStoreConnectApiKeyByIdQueryVariables,
} from '../generated';
export const AppStoreConnectApiKeyQuery = {
async getByIdAsync(
graphqlClient: ExpoGraphqlClient,
ascApiKeyId: string
): Promise<{
issuerIdentifier: string;
keyIdentifier: string;
keyP8: string;
}> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppStoreConnectApiKeyByIdQuery, AppStoreConnectApiKeyByIdQueryVariables>(
gql`
query AppStoreConnectApiKeyById($ascApiKeyId: ID!) {
appStoreConnectApiKey {
byId(id: $ascApiKeyId) {
id
issuerIdentifier
keyIdentifier
keyP8
}
}
}
`,
{ ascApiKeyId },
{
additionalTypenames: ['AppStoreConnectApiKey'],
}
)
.toPromise()
);
const key = data.appStoreConnectApiKey.byId;
return {
issuerIdentifier: key.issuerIdentifier,
keyIdentifier: key.keyIdentifier,
keyP8: key.keyP8,
};
},
};
@@ -4,6 +4,7 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariablesByAppIdQuery,
EnvironmentVariablesSharedQuery,
@@ -20,10 +21,12 @@ export const EnvironmentVariablesQuery = {
filterNames,
}: {
appId: string;
environment: string;
environment: EnvironmentVariableEnvironment;
filterNames?: string[];
}
): Promise<EnvironmentVariableFragment[]> {
): Promise<{
appVariables: EnvironmentVariableFragment[];
}> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
@@ -49,24 +52,20 @@ export const EnvironmentVariablesQuery = {
}
`,
{ appId, filterNames, environment },
{ additionalTypenames: ['EnvironmentVariableWithSecret'] }
{ additionalTypenames: ['EnvironmentVariable'] }
)
.toPromise()
);
return data.app?.byId.environmentVariablesIncludingSensitive ?? [];
return {
appVariables: data.app?.byId.environmentVariablesIncludingSensitive ?? [],
};
},
async byAppIdAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
environment,
filterNames,
}: {
appId: string;
environment: string;
filterNames?: string[];
}
appId: string,
environment: string,
filterNames?: string[]
): Promise<EnvironmentVariableFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -99,7 +98,8 @@ export const EnvironmentVariablesQuery = {
},
async sharedAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, filterNames }: { appId: string; filterNames?: string[] }
appId: string,
filterNames?: string[]
): Promise<EnvironmentVariableFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -129,39 +129,4 @@ export const EnvironmentVariablesQuery = {
return data.app?.byId.ownerAccount.environmentVariables ?? [];
},
async sharedWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, filterNames }: { appId: string; filterNames?: string[] }
): Promise<EnvironmentVariableFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
gql`
query EnvironmentVariablesSharedWithSensitive(
$appId: String!
$filterNames: [String!]
) {
app {
byId(appId: $appId) {
id
ownerAccount {
id
environmentVariablesIncludingSensitive(filterNames: $filterNames) {
id
name
value
}
}
}
}
}
`,
{ appId, filterNames },
{ additionalTypenames: ['EnvironmentVariableWithSecret'] }
)
.toPromise()
);
return data.app?.byId.ownerAccount.environmentVariablesIncludingSensitive ?? [];
},
};
@@ -1,38 +0,0 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
GoogleServiceAccountKeyByIdQuery,
GoogleServiceAccountKeyByIdQueryVariables,
} from '../generated';
export const GoogleServiceAccountKeyQuery = {
async getByIdAsync(
graphqlClient: ExpoGraphqlClient,
ascApiKeyId: string
): Promise<{ keyJson: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GoogleServiceAccountKeyByIdQuery, GoogleServiceAccountKeyByIdQueryVariables>(
gql`
query GoogleServiceAccountKeyById($ascApiKeyId: ID!) {
googleServiceAccountKey {
byId(id: $ascApiKeyId) {
id
keyJson
}
}
}
`,
{ ascApiKeyId },
{
additionalTypenames: ['GoogleServiceAccountKey'],
}
)
.toPromise()
);
return { keyJson: data.googleServiceAccountKey.byId.keyJson };
},
};
@@ -28,6 +28,6 @@ export const SubmissionFragmentNode = gql`
errorCode
message
}
logFiles
logsUrl
}
`;
@@ -46,5 +46,17 @@ describe('expoConfig', () => {
await createOrModifyExpoConfigAsync('/app', { owner: 'ccheever' });
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', { owner: 'ccheever' });
});
it('adds expo key if app.json is missing it', async () => {
jest.mocked(getConfigFilePaths).mockReturnValue({
staticConfigPath: '/app/app.json',
dynamicConfigPath: null,
});
jest.mocked(JsonFile.readAsync).mockResolvedValue({ name: 'App' });
await createOrModifyExpoConfigAsync('/app', { owner: 'ccheever' });
expect(JsonFile.writeAsync).toHaveBeenCalledWith('/app/app.json', { name: 'App', expo: {} });
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', { owner: 'ccheever' });
});
});
});
@@ -65,6 +65,9 @@ export async function getApplicationIdFromBareAsync(
projectDir: string,
gradleContext?: GradleBuildContext
): Promise<string> {
if (env.overrideAndroidApplicationId) {
return env.overrideAndroidApplicationId;
}
const errorMessage = 'Could not read applicationId from Android project.';
if (gradleContext) {
@@ -106,10 +109,6 @@ export async function getApplicationIdAsync(
vcsClient: Client,
gradleContext?: GradleBuildContext
): Promise<string> {
if (env.overrideAndroidApplicationId) {
return env.overrideAndroidApplicationId;
}
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
@@ -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 { BuildProfile } from '@expo/eas-json';
import {
@@ -26,7 +26,6 @@ export async function getApplicationIdentifierAsync({
platform,
vcsClient,
nonInteractive,
env,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
@@ -36,7 +35,6 @@ export async function getApplicationIdentifierAsync({
platform: Platform;
vcsClient: Client;
nonInteractive: boolean;
env: Env;
}): Promise<string> {
if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
@@ -73,12 +71,11 @@ export async function getApplicationIdentifierAsync({
{ exp, projectDir, nonInteractive: false, vcsClient },
profile
);
const targets = await resolveTargetsAsync({
projectDir,
exp,
xcodeBuildContext,
env,
env: profile.env,
vcsClient,
});
const applicationTarget = findApplicationTarget(targets);
+17 -2
View File
@@ -1,7 +1,9 @@
import { ExpoConfig, getConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import fs from 'fs-extra';
import Joi from 'joi';
import nullthrows from 'nullthrows';
import path from 'path';
export type PublicExpoConfig = Omit<
@@ -16,7 +18,6 @@ export type PublicExpoConfig = Omit<
export interface ExpoConfigOptions {
env?: Env;
skipSDKVersionRequirement?: boolean;
skipPlugins?: boolean;
}
interface ExpoConfigOptionsInternal extends ExpoConfigOptions {
@@ -29,6 +30,7 @@ export async function createOrModifyExpoConfigAsync(
readOptions?: { skipSDKVersionRequirement?: boolean }
): ReturnType<typeof modifyConfigAsync> {
ensureExpoConfigExists(projectDir);
await ensureStaticExpoConfigIsValidAsync(projectDir);
if (readOptions) {
return await modifyConfigAsync(projectDir, exp, readOptions);
@@ -50,7 +52,6 @@ function getExpoConfigInternal(
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
...(opts.skipPlugins ? { skipPlugins: true } : {}),
});
const { error } = MinimalAppConfigSchema.validate(exp, {
@@ -92,6 +93,20 @@ export function ensureExpoConfigExists(projectDir: string): void {
}
}
async function ensureStaticExpoConfigIsValidAsync(projectDir: string): Promise<void> {
if (isUsingStaticExpoConfig(projectDir)) {
const staticConfigPath = nullthrows(getConfigFilePaths(projectDir).staticConfigPath);
const staticConfig = await JsonFile.readAsync(staticConfigPath);
// Add the "expo" key if it doesn't exist on app.json yet, such as in
// projects initialized with RNC CLI
if (!staticConfig?.expo) {
staticConfig.expo = {};
await JsonFile.writeAsync(staticConfigPath, staticConfig);
}
}
}
export function isUsingStaticExpoConfig(projectDir: string): boolean {
const paths = getConfigFilePaths(projectDir);
return !!(paths.staticConfigPath?.endsWith('app.json') && !paths.dynamicConfigPath);
@@ -59,15 +59,14 @@ export async function getBundleIdentifierAsync(
vcsClient: Client,
xcodeContext?: { targetName?: string; buildConfiguration?: string }
): Promise<string> {
if (env.overrideIosBundleIdentifier) {
return env.overrideIosBundleIdentifier;
}
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
if (env.overrideIosBundleIdentifier) {
return env.overrideIosBundleIdentifier;
}
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
const isMultiScheme = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir).length > 1;
const isMultiTarget =
+1 -1
View File
@@ -189,7 +189,7 @@ async function resolveBareProjectDependenciesAsync({
}
);
const entitlements = await getNativeTargetEntitlementsAsync(projectDir, {
targetName: dependency.name,
targetName: target.name,
buildConfiguration,
});
result.push({
@@ -1,79 +0,0 @@
import { FingerprintSource, FingerprintSourceType } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { LocalBuildMode } from '../build/local';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { UploadSessionType } from '../graphql/generated';
import Log from '../log';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { getTmpDirectory } from '../utils/paths';
export async function maybeUploadFingerprintAsync({
runtimeVersion,
fingerprint,
graphqlClient,
localBuildMode,
}: {
runtimeVersion: string;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
};
graphqlClient: ExpoGraphqlClient;
localBuildMode?: LocalBuildMode;
}): Promise<{
runtimeVersion: string;
fingerprintSource?: FingerprintSource;
}> {
await fs.mkdirp(getTmpDirectory());
const fingerprintLocation = path.join(getTmpDirectory(), `${uuidv4()}-runtime-fingerprint.json`);
await fs.writeJSON(fingerprintLocation, {
hash: runtimeVersion,
sources: fingerprint.fingerprintSources,
});
if (localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
runtimeVersion,
fingerprintSource: {
type: FingerprintSourceType.PATH,
path: fingerprintLocation,
isDebugFingerprint: fingerprint.isDebugFingerprintSource,
},
};
}
let fingerprintGCSBucketKey = undefined;
try {
fingerprintGCSBucketKey = await uploadFileAtPathToGCSAsync(
graphqlClient,
UploadSessionType.EasUpdateFingerprint,
fingerprintLocation
);
} catch (err: any) {
let errMessage = 'Failed to upload fingerprint to EAS';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return {
runtimeVersion,
};
} finally {
await fs.remove(fingerprintLocation);
}
return {
runtimeVersion,
fingerprintSource: {
type: FingerprintSourceType.GCS,
bucketKey: fingerprintGCSBucketKey,
isDebugFingerprint: fingerprint.isDebugFingerprintSource,
},
};
}
+50 -82
View File
@@ -13,7 +13,6 @@ import path from 'path';
import promiseLimit from 'promise-limit';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
import { resolveRuntimeVersionUsingCLIAsync } from './resolveRuntimeVersionAsync';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getDefaultBranchNameAsync } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -31,7 +30,7 @@ import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log, { learnMore } from '../log';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
import {
UpdateJsonInfo,
formatUpdateMessage,
@@ -43,10 +42,12 @@ import {
shouldUseVersionedExpoCLI,
shouldUseVersionedExpoCLIWithExplicitPlatforms,
} from '../utils/expoCli';
import { ExpoUpdatesCLIModuleNotFoundError } from '../utils/expoUpdatesCli';
import {
ExpoUpdatesCLIModuleNotFoundError,
expoUpdatesCommandAsync,
} from '../utils/expoUpdatesCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import groupBy from '../utils/expodash/groupBy';
import uniqBy from '../utils/expodash/uniqBy';
import { Client } from '../vcs/vcs';
@@ -598,12 +599,7 @@ export async function getBranchNameForCommandAsync({
}
if (channelNameArg) {
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelNameArg
);
return branchName;
return await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelNameArg);
}
if (branchNameArg) {
@@ -702,15 +698,7 @@ export async function getUpdateMessageForCommandAsync(
export const defaultPublishPlatforms: UpdatePublishPlatform[] = ['android', 'ios'];
export type RuntimeVersionInfo = {
runtimeVersion: string;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
};
export async function getRuntimeVersionInfoObjectsAsync({
export async function getRuntimeVersionObjectAsync({
exp,
platforms,
workflows,
@@ -722,17 +710,12 @@ export async function getRuntimeVersionInfoObjectsAsync({
workflows: Record<ExpoConfigPlatform, Workflow>;
projectDir: string;
env: Env | undefined;
}): Promise<
{
platform: UpdatePublishPlatform;
runtimeVersionInfo: RuntimeVersionInfo;
}[]
> {
}): Promise<{ platform: UpdatePublishPlatform; runtimeVersion: string }[]> {
return await Promise.all(
platforms.map(async platform => {
return {
platform,
runtimeVersionInfo: await getRuntimeVersionInfoForPlatformAsync({
runtimeVersion: await getRuntimeVersionForPlatformAsync({
exp,
platform,
workflow: workflows[platform],
@@ -744,7 +727,7 @@ export async function getRuntimeVersionInfoObjectsAsync({
);
}
async function getRuntimeVersionInfoForPlatformAsync({
async function getRuntimeVersionForPlatformAsync({
exp,
platform,
workflow,
@@ -756,31 +739,29 @@ async function getRuntimeVersionInfoForPlatformAsync({
workflow: Workflow;
projectDir: string;
env: Env | undefined;
}): Promise<{
runtimeVersion: string;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
}> {
}): Promise<string> {
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
try {
const runtimeVersionResult = await resolveRuntimeVersionUsingCLIAsync({
platform,
workflow,
projectDir,
env,
});
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
return {
...runtimeVersionResult,
runtimeVersion: nullthrows(
runtimeVersionResult.runtimeVersion,
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
const extraArgs = Log.isDebug ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env }
);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
return nullthrows(
runtimeVersionResult.runtimeVersion,
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
);
} catch (e: any) {
// if it's a known set of errors thrown by the CLI it means that we need to default back to the
// previous behavior, otherwise we throw the error since something is wrong
@@ -810,37 +791,23 @@ async function getRuntimeVersionInfoForPlatformAsync({
);
}
return {
runtimeVersion: resolvedRuntimeVersion,
fingerprint: null,
};
return resolvedRuntimeVersion;
}
export function getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects: {
platform: UpdatePublishPlatform;
runtimeVersionInfo: RuntimeVersionInfo;
}[]
): (RuntimeVersionInfo & { platforms: UpdatePublishPlatform[] })[] {
const groupedRuntimeVersionInfoObjects = groupBy(
runtimeVersionInfoObjects,
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.runtimeVersion
);
return Object.entries(groupedRuntimeVersionInfoObjects).map(
([runtimeVersion, runtimeVersionInfoObjects]) => {
return {
runtimeVersion,
platforms: runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.platform
),
fingerprint:
runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.fingerprint
)[0] ?? null,
};
export function getRuntimeToPlatformMappingFromRuntimeVersions(
runtimeVersions: { platform: UpdatePublishPlatform; runtimeVersion: string }[]
): { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[] {
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[] =
[];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
);
}
return runtimeToPlatformMapping;
}
export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
@@ -874,18 +841,19 @@ export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
appId,
branchName,
rolloutPercentage,
runtimeToPlatformsAndFingerprintInfoMapping,
runtimeToPlatformMapping,
}: {
appId: string;
branchName: string;
rolloutPercentage: number;
runtimeToPlatformsAndFingerprintInfoMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
})[];
runtimeToPlatformMapping: { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[];
}
): Promise<Map<string, UpdateRolloutInfoGroup>> {
const runtimeToPlatformsMap = new Map(
runtimeToPlatformsAndFingerprintInfoMapping.map(r => [r.runtimeVersion, r.platforms])
runtimeToPlatformMapping.map<[string, UpdatePublishPlatform[]]>(r => [
r.runtimeVersion,
r.platforms,
])
);
return await mapMapAsync(runtimeToPlatformsMap, async (platforms, runtimeVersion) => {
return Object.fromEntries(
@@ -1,32 +1,17 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { AppVersionSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import { BuildFlags } from '../build/types';
import Log, { learnMore } from '../log';
import { confirmAsync, selectAsync } from '../prompts';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { ProfileData } from '../utils/profiles';
export enum AppVersionSourceUpdateOption {
SET_TO_REMOTE,
SET_TO_LOCAL,
ABORT,
}
export async function ensureVersionSourceIsRemoteAsync(
easJsonAccessor: EasJsonAccessor,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
let easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (easJsonCliConfig?.appVersionSource === undefined) {
easJsonCliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
easJsonCliConfig ?? undefined,
nonInteractive
);
}
const easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
return;
}
@@ -60,31 +45,10 @@ export async function ensureVersionSourceIsRemoteAsync(
Log.withTick('Updated eas.json');
}
export async function validateBuildProfileVersionSettingsAsync(
export function validateBuildProfileVersionSettings(
profileInfo: ProfileData<'build'>,
cliConfig: EasJson['cli'],
projectDir: string,
flags: BuildFlags
): Promise<void> {
if (
cliConfig?.appVersionSource === undefined &&
profileInfo.profile.autoIncrement !== 'version'
) {
if (profileInfo.profile.autoIncrement !== true) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
} else {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
cliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
cliConfig,
flags.nonInteractive
);
}
}
cliConfig: EasJson['cli']
): void {
if (cliConfig?.appVersionSource !== AppVersionSource.REMOTE) {
return;
}
@@ -134,96 +98,3 @@ export function getBuildVersionName(platform: Platform): string {
return 'buildNumber';
}
}
export async function ensureAppVersionSourceIsSetAsync(
easJsonAccessor: EasJsonAccessor,
easJsonCliConfig: EasJson['cli'] | undefined,
nonInteractive: boolean
): Promise<EasJson['cli'] | undefined> {
let selectOption, updateEasJson;
if (nonInteractive) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future Proceeding with the default "local" value. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
selectOption = AppVersionSourceUpdateOption.SET_TO_LOCAL;
updateEasJson = false;
} else {
Log.log(
'Since EAS CLI version `12.0.0` explicitly specifying app version source is required. Please select your app version source:'
);
Log.log(
`\t1) With the "local" app version source and "autoIncrement" option enabled, the build number/version code is sourced from local project files and incremented automatically if possible, by editing local project files. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/#local-version-source'
)}`
);
Log.log(
`\t2) With the "remote" app version source and "autoIncrement" option enabled, the build number/version code is stored on EAS servers and updated every time you create a new build. Remote auto-incrementation won't edit the version in the local project files, but instead, the new version will be injected automatically during the build process. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/#remote-version-source'
)}`
);
Log.log(
`Until now, this project has been using the "local" version source (which was the previous default). App version source can now be set for you automatically, or you can configure it manually by setting the "appVersionSource" value in your eas.json.`
);
selectOption = await selectAsync(`What would you like to do?`, [
{
title: 'Update eas.json to use the default "remote" version source (recommended)',
value: AppVersionSourceUpdateOption.SET_TO_REMOTE,
},
{
title: 'Update eas.json to use "local" version source (old behavior)',
value: AppVersionSourceUpdateOption.SET_TO_LOCAL,
},
{
title: "Don't update eas.json, abort command and configure manually",
value: AppVersionSourceUpdateOption.ABORT,
},
]);
updateEasJson = true;
}
if (selectOption === AppVersionSourceUpdateOption.SET_TO_LOCAL) {
if (updateEasJson) {
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
...easJsonRawObject?.cli,
appVersionSource: AppVersionSource.LOCAL,
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
}
if (easJsonCliConfig) {
easJsonCliConfig.appVersionSource = AppVersionSource.LOCAL;
}
Log.withTick('Updated eas.json');
} else if (selectOption === AppVersionSourceUpdateOption.SET_TO_REMOTE) {
if (updateEasJson) {
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
...easJsonRawObject?.cli,
appVersionSource: AppVersionSource.REMOTE,
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
}
if (easJsonCliConfig) {
easJsonCliConfig.appVersionSource = AppVersionSource.REMOTE;
}
Log.withTick('Updated eas.json');
} else {
Log.warn(
`You'll need to configure ${chalk.bold('appVersionSource')} manually. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
Errors.error('Aborted.', { exit: 1 });
}
return easJsonCliConfig;
}
@@ -9,52 +9,6 @@ import {
expoUpdatesCommandAsync,
} from '../utils/expoUpdatesCli';
export async function resolveRuntimeVersionUsingCLIAsync({
platform,
workflow,
projectDir,
env,
cwd,
}: {
platform: 'ios' | 'android';
workflow: Workflow;
projectDir: string;
env: Env | undefined;
cwd?: string;
}): Promise<{
runtimeVersion: string | null;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
}> {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const useDebugFingerprintSource = Log.isDebug;
const extraArgs = useDebugFingerprintSource ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env, cwd }
);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
return {
runtimeVersion: runtimeVersionResult.runtimeVersion ?? null,
fingerprint: runtimeVersionResult.fingerprintSources
? {
fingerprintSources: runtimeVersionResult.fingerprintSources,
isDebugFingerprintSource: useDebugFingerprintSource,
}
: null,
};
}
export async function resolveRuntimeVersionAsync({
exp,
platform,
@@ -86,7 +40,31 @@ export async function resolveRuntimeVersionAsync({
}
try {
return await resolveRuntimeVersionUsingCLIAsync({ platform, workflow, projectDir, env, cwd });
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const useDebugFingerprintSource = Log.isDebug;
const extraArgs = useDebugFingerprintSource ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env, cwd }
);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
return {
runtimeVersion: runtimeVersionResult.runtimeVersion ?? null,
fingerprint: runtimeVersionResult.fingerprintSources
? {
fingerprintSources: runtimeVersionResult.fingerprintSources,
isDebugFingerprintSource: useDebugFingerprintSource,
}
: null,
};
} catch (e: any) {
// if expo-updates is not installed, there's no need for a runtime version in the build
if (e instanceof ExpoUpdatesCLIModuleNotFoundError) {
+1 -17
View File
@@ -7,7 +7,7 @@ import * as uuid from 'uuid';
import { getRecentBuildsForSubmissionAsync } from './utils/builds';
import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { BuildFragment, BuildStatus } from '../graphql/generated';
import { BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
@@ -89,16 +89,6 @@ export type ArchiveSource =
export type ResolvedArchiveSource = ArchiveUrlSource | ArchiveGCSSource | ArchiveBuildSource;
const buildStatusMapping: Record<BuildStatus, string> = {
[BuildStatus.New]: 'new',
[BuildStatus.InQueue]: 'in queue',
[BuildStatus.InProgress]: 'in progress',
[BuildStatus.Finished]: 'finished',
[BuildStatus.Errored]: 'errored',
[BuildStatus.PendingCancel]: 'canceled',
[BuildStatus.Canceled]: 'canceled',
};
export async function getArchiveAsync(
ctx: ArchiveResolverContext,
source: ArchiveSource
@@ -354,7 +344,6 @@ function formatBuildChoice(build: BuildFragment): prompts.Choice {
gitCommitMessage,
channel,
message,
status,
} = build;
const buildDate = new Date(updatedAt);
@@ -379,7 +368,6 @@ function formatBuildChoice(build: BuildFragment): prompts.Choice {
? chalk.bold(message.length > 200 ? `${message.slice(0, 200)}...` : message)
: null,
},
{ name: 'Status', value: buildStatusMapping[status] },
];
const filteredDescriptionArray: string[] = descriptionItems
@@ -397,10 +385,6 @@ function formatBuildChoice(build: BuildFragment): prompts.Choice {
async function handlePromptSourceAsync(
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
if (ctx.nonInteractive) {
throw new Error('Please run this command with appropriate input.');
}
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
type: 'select',
+2 -6
View File
@@ -65,13 +65,9 @@ export default abstract class BaseSubmitter<
return resolvedSourceOptions;
}
public async getSubmissionInputAsync(): Promise<SubmissionInput<P>> {
const resolvedSourceOptions = await this.getSourceOptionsAsync();
return await this.createSubmissionInputAsync(resolvedSourceOptions);
}
public async submitAsync(): Promise<SubmissionFragment> {
const input = await this.getSubmissionInputAsync();
const resolvedSourceOptions = await this.getSourceOptionsAsync();
const input = await this.createSubmissionInputAsync(resolvedSourceOptions);
return await this.createSubmissionWithAnalyticsAsync(input);
}
@@ -7,7 +7,6 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionArchiveSourceType,
UploadSessionType,
} from '../../graphql/generated';
@@ -42,17 +41,6 @@ const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
status: BuildStatus.Finished,
};
const MOCK_IN_PROGRESS_BUILD_FRAGMENT: Partial<BuildFragment> = {
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
status: BuildStatus.InProgress,
};
const SOURCE_STUB_INPUT = {
@@ -258,7 +246,7 @@ describe(getArchiveAsync, () => {
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('handles build-list-select source for finished builds', async () => {
it('handles build-list-select source', async () => {
const projectId = uuidv4();
jest
.mocked(getRecentBuildsForSubmissionAsync)
@@ -281,57 +269,6 @@ describe(getArchiveAsync, () => {
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('handles build-list-select source for in-progress builds', async () => {
const projectId = uuidv4();
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_IN_PROGRESS_BUILD_FRAGMENT as BuildFragment]);
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ selectedBuild: MOCK_IN_PROGRESS_BUILD_FRAGMENT });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.buildList,
}
);
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('handles build-list-select source for both finished and in-progress builds', async () => {
const projectId = uuidv4();
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([
MOCK_IN_PROGRESS_BUILD_FRAGMENT as BuildFragment,
MOCK_BUILD_FRAGMENT as BuildFragment,
]);
jest.mocked(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.buildList,
}
);
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('prompts again if all builds have expired', async () => {
jest.mocked(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
{
@@ -4,7 +4,11 @@ import { Result, result } from '@expo/results';
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
import { SubmissionAndroidReleaseStatus, SubmissionAndroidTrack } from '../../graphql/generated';
import {
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
SubmissionFragment,
} from '../../graphql/generated';
import Log from '../../log';
import {
AmbiguousApplicationIdError,
@@ -18,7 +22,7 @@ import { SubmissionContext } from '../context';
export default class AndroidSubmitCommand {
constructor(private ctx: SubmissionContext<Platform.ANDROID>) {}
async runAsync(): Promise<AndroidSubmitter> {
async runAsync(): Promise<SubmissionFragment> {
Log.addNewLineIfNone();
const archiveSource = this.resolveArchiveSource();
if (!archiveSource.ok) {
@@ -44,7 +48,7 @@ export default class AndroidSubmitCommand {
}
const submissionOptions = await this.getAndroidSubmissionOptionsAsync(archiveSourceValue);
const submitter = new AndroidSubmitter(this.ctx, submissionOptions, archive);
return submitter;
return await submitter.submitAsync();
}
private async getAndroidSubmissionOptionsAsync(
@@ -132,9 +132,7 @@ describe(AndroidSubmitCommand, () => {
vcsClient,
});
const command = new AndroidSubmitCommand(ctx);
await expect(
command.runAsync().then(submitter => submitter.submitAsync())
).rejects.toThrowError(
await expect(command.runAsync()).rejects.toThrowError(
'Google Service Account Keys cannot be set up in --non-interactive mode.'
);
});
@@ -172,8 +170,7 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -219,8 +216,7 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -268,8 +264,7 @@ describe(AndroidSubmitCommand, () => {
vcsClient,
});
const command = new AndroidSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -334,8 +329,7 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(
graphqlClient,
@@ -397,8 +391,7 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(
graphqlClient,
@@ -468,8 +461,7 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(
graphqlClient,
@@ -12,6 +12,7 @@ import {
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { ArchiveSource, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource';
import { refreshContextSubmitProfileAsync, resolveArchiveSource } from '../commons';
@@ -20,7 +21,7 @@ import { SubmissionContext } from '../context';
export default class IosSubmitCommand {
constructor(private ctx: SubmissionContext<Platform.IOS>) {}
async runAsync(): Promise<IosSubmitter> {
async runAsync(): Promise<SubmissionFragment> {
Log.addNewLineIfNone();
const archiveSource = this.resolveArchiveSource();
if (!archiveSource.ok) {
@@ -46,7 +47,7 @@ export default class IosSubmitCommand {
}
const options = await this.resolveSubmissionOptionsAsync(archiveSourceValue);
const submitter = new IosSubmitter(this.ctx, options, archive);
return submitter;
return await submitter.submitAsync();
}
private async resolveSubmissionOptionsAsync(
@@ -109,7 +109,7 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await expect(command.runAsync().then(submitter => submitter.submitAsync())).rejects.toThrow(
await expect(command.runAsync()).rejects.toThrow(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format xxxx-xxxx-xxxx-xxxx, where x is a lowercase letter.'
);
@@ -144,9 +144,7 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await expect(
command.runAsync().then(submitter => submitter.submitAsync())
).rejects.toThrowError();
await expect(command.runAsync()).rejects.toThrowError();
});
});
@@ -182,8 +180,7 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -248,8 +245,7 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -308,8 +304,7 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -374,8 +369,7 @@ describe(IosSubmitCommand, () => {
specifiedProfile: 'specificProfile',
});
const command = new IosSubmitCommand(ctx);
const submitter = await command.runAsync();
await submitter.submitAsync();
await command.runAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
+1 -2
View File
@@ -23,8 +23,7 @@ export async function submitAsync<T extends Platform>(
ctx.platform === Platform.ANDROID
? new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>)
: new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
const submitter = await command.runAsync();
return await submitter.submitAsync();
return await command.runAsync();
},
{
attemptEvent: SubmissionEvent.SUBMIT_COMMAND_ATTEMPT,
@@ -1,226 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionArchiveSourceType,
} from '../../../graphql/generated';
import { BuildQuery } from '../../../graphql/queries/BuildQuery';
import { getRecentBuildsForSubmissionAsync } from '../builds';
jest.mock('../../../graphql/queries/BuildQuery', () => ({
BuildQuery: {
viewBuildsOnAppAsync: jest.fn(),
},
}));
const ARCHIVE_SOURCE = {
type: SubmissionArchiveSourceType.Url,
url: 'https://url.to/archive.tar.gz',
};
const MOCK_BUILD_FRAGMENTS: Partial<BuildFragment>[] = Array(5).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.Finished,
}));
const MOCK_IN_PROGRESS_BUILD_FRAGMENTS: Partial<BuildFragment>[] = Array(2).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.InProgress,
}));
const MOCK_IN_QUEUE_BUILD_FRAGMENTS = Array(2).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.InQueue,
}));
const MOCK_NEW_BUILD_FRAGMENTS = Array(1).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.New,
}));
describe(getRecentBuildsForSubmissionAsync, () => {
let graphqlClient: ExpoGraphqlClient;
beforeEach(() => {
graphqlClient = {} as any as ExpoGraphqlClient;
});
it('returns finished builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce(
MOCK_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_BUILD_FRAGMENTS.length)
) as BuildFragment[]
);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_BUILD_FRAGMENTS.slice(0, Math.min(limit, MOCK_BUILD_FRAGMENTS.length))
);
});
it('returns in-progress builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce(
MOCK_IN_PROGRESS_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_PROGRESS_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce([] as BuildFragment[]);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_IN_PROGRESS_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_PROGRESS_BUILD_FRAGMENTS.length)
)
);
});
it('returns in-queue builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce(
MOCK_IN_QUEUE_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_QUEUE_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[]);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_IN_QUEUE_BUILD_FRAGMENTS.slice(0, Math.min(limit, MOCK_IN_QUEUE_BUILD_FRAGMENTS.length))
);
});
it('returns new builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce(
MOCK_NEW_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_NEW_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[]);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_NEW_BUILD_FRAGMENTS.slice(0, Math.min(limit, MOCK_NEW_BUILD_FRAGMENTS.length))
);
});
it('returns up to "limit" newest builds regardless of status', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce(
MOCK_NEW_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_NEW_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce(
MOCK_IN_QUEUE_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_QUEUE_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce(
MOCK_IN_PROGRESS_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_PROGRESS_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce(
MOCK_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_BUILD_FRAGMENTS.length)
) as BuildFragment[]
);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject([MOCK_NEW_BUILD_FRAGMENTS[0], MOCK_IN_QUEUE_BUILD_FRAGMENTS[1]]);
});
});
+10 -25
View File
@@ -8,29 +8,14 @@ export async function getRecentBuildsForSubmissionAsync(
appId: string,
{ limit = 1 }: { limit?: number } = {}
): Promise<BuildFragment[]> {
const allowedStatuses = [
BuildStatus.New,
BuildStatus.InQueue,
BuildStatus.InProgress,
BuildStatus.Finished,
];
const buildsPromises: Promise<BuildFragment[]>[] = [];
for (const buildStatus of allowedStatuses) {
buildsPromises.push(
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId,
limit,
offset: 0,
filter: {
platform,
distribution: DistributionType.Store,
status: buildStatus,
},
})
);
}
const builds = (await Promise.all(buildsPromises)).reduce((acc, value) => acc.concat(value), []);
builds.sort((buildA, buildB) => (buildA.createdAt > buildB.createdAt ? -1 : 1));
return builds.slice(0, limit);
return await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId,
limit,
offset: 0,
filter: {
platform,
distribution: DistributionType.Store,
status: BuildStatus.Finished,
},
});
}
+15 -14
View File
@@ -22,20 +22,21 @@ export async function displayLogsAsync(
}
async function downloadAndPrintSubmissionLogsAsync(submission: SubmissionFragment): Promise<void> {
for (const logFile of submission.logFiles) {
const response = await fetch(logFile);
const logs = parseLogs(await response.text());
Log.addNewLineIfNone();
const prefix = chalk.blueBright('[logs] ');
for (const { level, msg } of logs) {
const msgWithPrefix = `${prefix}${msg}`;
if (level === 'error') {
Log.error(msgWithPrefix);
} else if (level === 'warn') {
Log.warn(msgWithPrefix);
} else {
Log.log(msgWithPrefix);
}
if (!submission.logsUrl) {
return;
}
const response = await fetch(submission.logsUrl);
const logs = parseLogs(await response.text());
Log.addNewLineIfNone();
const prefix = chalk.blueBright('[logs] ');
for (const { level, msg } of logs) {
const msgWithPrefix = `${prefix}${msg}`;
if (level === 'error') {
Log.error(msgWithPrefix);
} else if (level === 'warn') {
Log.warn(msgWithPrefix);
} else {
Log.log(msgWithPrefix);
}
}
}
@@ -1,21 +1,14 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
App,
DeploymentResult,
Runtime,
UpdateBranch,
UpdateChannel,
UpdateInsights,
} from '../../graphql/generated';
import { App, Runtime, UpdateBranch, UpdateChannel, UpdateInsights } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
jest.mock('../../graphql/queries/ChannelQuery');
jest.mock('../../graphql/queries/BranchQuery');
describe(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync, () => {
describe(getBranchNameFromChannelNameAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
});
@@ -30,14 +23,13 @@ describe(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync, () => {
}) as any
);
const result = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'channel-name'
);
expect(result.branchId).toBeTruthy();
expect(result.branchName).toBe('test-branch-name');
expect(result).toBe('test-branch-name');
});
test('errors when no branch is connected to channel', async () => {
@@ -50,11 +42,7 @@ describe(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync, () => {
);
await expect(
getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
'test-project-id',
'test-channel-name'
)
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
@@ -71,11 +59,7 @@ describe(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync, () => {
);
await expect(
getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
'test-project-id',
'test-channel-name'
)
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has multiple branches associated with it. Instead, use '--branch' instead of '--channel'"
);
@@ -91,13 +75,13 @@ describe(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync, () => {
}) as any
);
const result = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'test-channel-name'
);
expect(result.branchName).toBe('test-branch-name');
expect(result).toBe('test-branch-name');
});
});
@@ -118,7 +102,6 @@ function mockUpdateChannel({
branchMapping:
'{"data":[{"branchId":"f9dc4dbb-663f-4a19-8cf2-a783b484d2db","branchMappingLogic":"true"}],"version":0}',
updateBranches: branchNames ? mockUpdateBranches(branchNames) : [],
isPaused: false,
__typename: 'UpdateChannel',
};
}
@@ -144,7 +127,6 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
runtimeVersion: '1.0.0',
runtime: {} as Runtime, // Temporary fix to resolve type errors
insights: {} as UpdateInsights, // Temporary fix to resolve type errors
deployments: {} as DeploymentResult, // Temporary fix to resolve type errors
platform: 'ios',
manifestFragment: '...',
isRollBackToEmbedded: false,
@@ -4,12 +4,12 @@ import { createChannelOnAppAsync } from '../channel/queries';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { ChannelQuery } from '../graphql/queries/ChannelQuery';
export async function getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
export async function getBranchNameFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
channelName: string
): Promise<{ branchName: string; branchId: string }> {
let branchInfo: { branchName: string; branchId: string };
): Promise<string> {
let branchName;
try {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
@@ -18,8 +18,7 @@ export async function getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
});
if (channel.updateBranches.length === 1) {
const branch = channel.updateBranches[0];
branchInfo = { branchId: branch.id, branchName: branch.name };
branchName = channel.updateBranches[0].name;
} else if (channel.updateBranches.length === 0) {
throw new Error(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
@@ -52,8 +51,8 @@ export async function getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
);
}
branchInfo = { branchId, branchName: channelName };
branchName = channelName;
}
return branchInfo;
return branchName;
}
+2 -7
View File
@@ -58,10 +58,7 @@ export async function republishAsync({
update.branchId === arbitraryUpdate.branchId &&
update.branchName === arbitraryUpdate.branchName &&
update.runtimeVersion === arbitraryUpdate.runtimeVersion;
assert(
updatesToPublish.every(isSameGroup),
'All updates being republished must belong to the same update group'
);
assert(updatesToPublish.every(isSameGroup), 'All updates must belong to the same update group');
assert(
updatesToPublish.every(u => u.isRollBackToEmbedded) ||
@@ -93,9 +90,7 @@ export async function republishAsync({
}
}
Log.withTick(
`The republished update group will be signed with the same code signing key and algorithm as the original update`
);
Log.withTick(`The republished update group will be signed`);
}
const publishIndicator = ora('Republishing...').start();
+3 -4
View File
@@ -12,12 +12,11 @@ export async function promptVariableEnvironmentAsync(
);
}
return await selectAsync('Select environment:', [
{ title: 'development', value: EnvironmentVariableEnvironment.Development },
{ title: 'preview', value: EnvironmentVariableEnvironment.Preview },
{ title: 'production', value: EnvironmentVariableEnvironment.Production },
{ title: 'Development', value: EnvironmentVariableEnvironment.Development },
{ title: 'Preview', value: EnvironmentVariableEnvironment.Preview },
{ title: 'Production', value: EnvironmentVariableEnvironment.Production },
]);
}
export async function promptVariableValueAsync({
nonInteractive,
required = true,
-189
View File
@@ -1,189 +0,0 @@
import { get as getEnv } from '@expo/env';
import { Gzip, GzipOptions } from 'minizlib';
import { HashOptions, createHash, randomBytes } from 'node:crypto';
import fs, { createWriteStream } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { pack } from 'tar-stream';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
/** Returns whether a file or folder is ignored */
function isIgnoredName(name: string): boolean {
switch (name) {
// macOS system files
case '.DS_Store':
case '.AppleDouble':
case '.Trashes':
case '__MACOSX':
case '.LSOverride':
return true;
default:
// Backup file name convention
return name.endsWith('~');
}
}
/** Creates a temporary file write path */
async function createTempWritePathAsync(): Promise<string> {
const basename = path.basename(__filename, path.extname(__filename));
const tmpdir = await fs.promises.realpath(os.tmpdir());
const random = randomBytes(4).toString('hex');
return path.resolve(tmpdir, `tmp-${basename}-${process.pid}-${random}`);
}
/** Computes a SHA512 hash for a file */
async function computeSha512HashAsync(
filePath: fs.PathLike,
options?: HashOptions
): Promise<string> {
const hash = createHash('sha512', { encoding: 'hex', ...options });
await pipeline(fs.createReadStream(filePath), hash);
return `${hash.read()}`;
}
/** A file entry with a gzip-safe (normalized) path and a filesystem path */
interface RecursiveFileEntry {
normalizedPath: string;
path: string;
}
/** Lists plain files in base path recursively and outputs normalized paths */
function listFilesRecursively(basePath: string): AsyncGenerator<RecursiveFileEntry> {
async function* recurseAsync(parentPath?: string): AsyncGenerator<RecursiveFileEntry> {
const target = parentPath ? path.resolve(basePath, parentPath) : basePath;
const entries = await fs.promises.readdir(target, { withFileTypes: true });
for (const dirent of entries) {
const normalizedPath = parentPath ? `${parentPath}/${dirent.name}` : dirent.name;
if (isIgnoredName(dirent.name)) {
continue;
} else if (dirent.isFile()) {
yield {
normalizedPath,
path: path.resolve(target, dirent.name),
};
} else if (dirent.isDirectory()) {
yield* recurseAsync(normalizedPath);
}
}
}
return recurseAsync();
}
interface AssetMapOptions {
hashOptions?: HashOptions;
}
/** Mapping of normalized file paths to a SHA512 hash */
export type AssetMap = Record<string, string>;
/** Creates an asset map of a given target path */
async function createAssetMapAsync(
assetPath?: string,
options?: AssetMapOptions
): Promise<AssetMap> {
const map: AssetMap = Object.create(null);
if (assetPath) {
for await (const file of listFilesRecursively(assetPath)) {
map[file.normalizedPath] = await computeSha512HashAsync(file.path, options?.hashOptions);
}
}
return map;
}
export interface Manifest {
env: Record<string, string | undefined>;
}
interface CreateManifestParams {
projectId: string;
projectDir: string;
environment?: EnvironmentVariableEnvironment;
}
/** Creates a manifest configuration sent up for deployment */
export async function createManifestAsync(
params: CreateManifestParams,
graphqlClient: ExpoGraphqlClient
): Promise<Manifest> {
let env: Record<string, string | undefined>;
if (params.environment) {
env = Object.fromEntries(
(
await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: params.projectId,
environment: params.environment,
})
).map(variable => [variable.name, variable.value ?? undefined])
);
} else {
// NOTE: This is required for the .env resolution
process.env.NODE_ENV = 'production';
env = getEnv(params.projectDir).env;
}
return { env };
}
interface WorkerFileEntry {
normalizedPath: string;
path: string;
data: Buffer | string;
}
/** Reads worker files while normalizing sourcemaps and providing normalized paths */
async function* listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerFileEntry> {
for await (const file of listFilesRecursively(workerPath)) {
yield {
normalizedPath: file.normalizedPath,
path: file.path,
data: await fs.promises.readFile(file.path),
};
}
}
/** Reads files of an asset maps and enumerates normalized paths and data */
async function* listAssetMapFilesAsync(
assetPath: string,
assetMap: AssetMap
): AsyncGenerator<WorkerFileEntry> {
for (const normalizedPath in assetMap) {
const filePath = path.resolve(assetPath, normalizedPath.split('/').join(path.sep));
const data = await fs.promises.readFile(filePath);
yield {
normalizedPath,
path: filePath,
data,
};
}
}
/** Entry of a normalized (gzip-safe) path and file data */
export type FileEntry = readonly [normalizedPath: string, data: Buffer | string];
/** Packs file entries into a tar.gz file (path to tgz returned) */
async function packFilesIterableAsync(
iterable: Iterable<FileEntry> | AsyncIterable<FileEntry>,
options?: GzipOptions
): Promise<string> {
const writePath = `${await createTempWritePathAsync()}.tar.gz`;
const write = createWriteStream(writePath);
const gzip = new Gzip({ portable: true, ...options });
const tar = pack();
const writeTask$ = pipeline(tar, gzip, write);
for await (const file of iterable) {
tar.entry({ name: file[0], type: 'file' }, file[1]);
}
tar.finalize();
await writeTask$;
return writePath;
}
export {
createAssetMapAsync,
listWorkerFilesAsync,
listAssetMapFilesAsync,
packFilesIterableAsync,
};
-233
View File
@@ -1,233 +0,0 @@
import { CombinedError as GraphqlError } from '@urql/core';
import chalk from 'chalk';
import { DeploymentsMutation } from './mutations';
import { DeploymentsQuery } from './queries';
import { EXPO_BASE_DOMAIN } from './utils/logs';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { WorkerDeploymentFragment } from '../graphql/generated';
import Log from '../log';
import { promptAsync } from '../prompts';
import { memoize } from '../utils/expodash/memoize';
import { selectPaginatedAsync } from '../utils/relay';
export async function getSignedDeploymentUrlAsync(
graphqlClient: ExpoGraphqlClient,
options: {
appId: string;
deploymentIdentifier?: string | null;
/** Callback which is invoked when the project is going to setup the dev domain */
onSetupDevDomain?: () => any;
/** If the terminal is running in non interactive mode or not */
nonInteractive?: boolean;
}
): Promise<string> {
try {
return await DeploymentsMutation.createSignedDeploymentUrlAsync(graphqlClient, {
appId: options.appId,
deploymentIdentifier: options.deploymentIdentifier,
});
} catch (error: any) {
const isMissingDevDomain = (error as GraphqlError)?.graphQLErrors?.some(e =>
['APP_NO_DEV_DOMAIN_NAME'].includes(e?.extensions?.errorCode as string)
);
// Throw unexpected errors eagerly
if (!isMissingDevDomain) {
throw error;
}
// Ensure the callback is invoked, containing cleanup logic for possible spinners
options.onSetupDevDomain?.();
// Assign the dev domain name by prompting the user
await assignDevDomainNameAsync({
graphqlClient,
appId: options.appId,
nonInteractive: options.nonInteractive,
});
// Retry creating the signed URL
return await getSignedDeploymentUrlAsync(graphqlClient, options);
}
}
type PromptInstance = {
cursorOffset: number;
placeholder: boolean;
rendered: string;
initial: string;
done: boolean;
get value(): string;
set value(input: string);
};
const DEV_DOMAIN_INVALID_START_END_CHARACTERS = /^[^a-z0-9]+|[^a-z0-9-]+$/;
const DEV_DOMAIN_INVALID_REPLACEMENT_HYPHEN = /[^a-z0-9-]+/;
const DEV_DOMAIN_INVALID_MULTIPLE_HYPHENS = /(-{2,})/;
/**
* Format a dev domain name to match whats allowed on the backend.
* This is equal to our `DEV_DOMAIN_NAME_REGEX`, but implemented as a filtering function
* to help users find a valid name while typing.
*/
function formatDevDomainName(name = ''): string {
return name
.toLowerCase()
.replace(DEV_DOMAIN_INVALID_REPLACEMENT_HYPHEN, '-')
.replace(DEV_DOMAIN_INVALID_START_END_CHARACTERS, '')
.replace(DEV_DOMAIN_INVALID_MULTIPLE_HYPHENS, '-')
.trim();
}
async function promptDevDomainNameAsync(initialDevDomain: string): Promise<string> {
const rootDomain = `.${EXPO_BASE_DOMAIN}.app`;
const memoizedFormatDevDomainName = memoize(formatDevDomainName);
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Choose a preview URL for your project:',
initial: initialDevDomain,
validate: (value: string) => {
if (!value) {
return 'You have to choose a preview URL for your project';
}
if (value.length < 3) {
return 'Preview URLs must be at least 3 characters long';
}
if (value.endsWith('-')) {
return 'Preview URLs cannot end with a hyphen (-)';
}
return true;
},
onState(this: PromptInstance, state: { value?: string }) {
const value = memoizedFormatDevDomainName(state.value);
if (value !== state.value) {
this.value = value;
}
},
onRender(this: PromptInstance, kleur) {
this.cursorOffset = -rootDomain.length - 1;
if (this.done) {
// Remove the space for the cursor when the prompt is done
this.rendered = this.value + kleur.dim(`${rootDomain}`);
} else if (this.placeholder) {
this.rendered = kleur.dim(`${this.initial} ${rootDomain}`);
} else {
this.rendered = this.value + kleur.dim(` ${rootDomain}`);
}
},
});
// This should never happen due to the validation, if it does its an error
if (!name) {
throw new Error('No preview URL provided, aborting deployment.');
}
return name;
}
/**
* Assign a dev domain name to a project.
* - When running in interactive mode, it will prompt the user with a suggested domain name.
* - When running in non interactive mode, it will auto-assign the suggested domain name.
*/
export async function assignDevDomainNameAsync({
graphqlClient,
appId,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
appId: string;
nonInteractive?: boolean;
}): ReturnType<typeof DeploymentsMutation.assignDevDomainNameAsync> {
let devDomainName = await DeploymentsQuery.getSuggestedDevDomainByAppIdAsync(graphqlClient, {
appId,
});
if (!nonInteractive) {
devDomainName = await promptDevDomainNameAsync(devDomainName);
}
try {
return await DeploymentsMutation.assignDevDomainNameAsync(graphqlClient, {
appId,
name: devDomainName,
});
} catch (error) {
const isChosenNameTaken = (error as GraphqlError)?.graphQLErrors?.some(e =>
['DEV_DOMAIN_NAME_TAKEN'].includes(e?.extensions?.errorCode as string)
);
// Throw unexpected errors eagerly
if (!isChosenNameTaken) {
throw error;
}
if (!nonInteractive) {
Log.error(`The preview URL "${devDomainName}" is already taken, choose a different URL.`);
}
return await assignDevDomainNameAsync({ graphqlClient, appId, nonInteractive });
}
}
export async function assignWorkerDeploymentAliasAsync({
graphqlClient,
appId,
deploymentId,
aliasName,
}: {
graphqlClient: ExpoGraphqlClient;
appId: string;
deploymentId: string;
aliasName: string;
}): ReturnType<typeof DeploymentsMutation.assignAliasAsync> {
return await DeploymentsMutation.assignAliasAsync(graphqlClient, {
appId,
deploymentId,
aliasName,
});
}
export async function assignWorkerDeploymentProductionAsync({
graphqlClient,
appId,
deploymentId,
}: {
graphqlClient: ExpoGraphqlClient;
appId: string;
deploymentId: string;
}): ReturnType<typeof DeploymentsMutation.assignAliasAsync> {
return await DeploymentsMutation.assignAliasAsync(graphqlClient, {
appId,
deploymentId,
aliasName: null, // this will assign the deployment as production
});
}
export async function selectWorkerDeploymentOnAppAsync({
graphqlClient,
appId,
selectTitle,
pageSize,
}: {
graphqlClient: ExpoGraphqlClient;
appId: string;
selectTitle?: string;
pageSize?: number;
}): ReturnType<typeof selectPaginatedAsync<WorkerDeploymentFragment>> {
return await selectPaginatedAsync({
pageSize: pageSize ?? 25,
printedType: selectTitle ?? 'worker deployment',
queryAsync: async queryParams =>
await DeploymentsQuery.getAllDeploymentsPaginatedAsync(graphqlClient, {
...queryParams,
appId,
}),
getTitleAsync: async (deployment: WorkerDeploymentFragment) =>
chalk`${deployment.deploymentIdentifier}{dim - created at: ${new Date(
deployment.createdAt
).toLocaleString()}}`,
});
}
@@ -1,11 +0,0 @@
import gql from 'graphql-tag';
export const WorkerDeploymentFragmentNode = gql`
fragment WorkerDeploymentFragment on WorkerDeployment {
id
url
deploymentIdentifier
deploymentDomain
createdAt
}
`;
@@ -1,9 +0,0 @@
import gql from 'graphql-tag';
export const WorkerDeploymentAliasFragmentNode = gql`
fragment WorkerDeploymentAliasFragment on WorkerDeploymentAlias {
id
aliasName
url
}
`;
-116
View File
@@ -1,116 +0,0 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { WorkerDeploymentFragmentNode } from './fragments/WorkerDeployment';
import { WorkerDeploymentAliasFragmentNode } from './fragments/WorkerDeploymentAlias';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../graphql/client';
import {
AssignAliasMutation,
AssignAliasMutationVariables,
AssignDevDomainNameMutation,
AssignDevDomainNameMutationVariables,
CreateDeploymentUrlMutation,
CreateDeploymentUrlMutationVariables,
} from '../graphql/generated';
export const DeploymentsMutation = {
async createSignedDeploymentUrlAsync(
graphqlClient: ExpoGraphqlClient,
deploymentVariables: {
appId: string;
deploymentIdentifier?: string | null;
}
): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateDeploymentUrlMutation, CreateDeploymentUrlMutationVariables>(
gql`
mutation createDeploymentUrlMutation($appId: ID!, $deploymentIdentifier: ID) {
deployments {
createSignedDeploymentUrl(
appId: $appId
deploymentIdentifier: $deploymentIdentifier
) {
pendingWorkerDeploymentId
deploymentIdentifier
url
}
}
}
`,
deploymentVariables
)
.toPromise()
);
const url = data.deployments?.createSignedDeploymentUrl.url;
assert(url, 'Deployment URL must be defined');
return url;
},
async assignDevDomainNameAsync(
graphqlClient: ExpoGraphqlClient,
devDomainNameVariables: { appId: string; name: string }
): Promise<boolean> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<AssignDevDomainNameMutation, AssignDevDomainNameMutationVariables>(
gql`
mutation AssignDevDomainName($appId: ID!, $name: DevDomainName!) {
devDomainName {
assignDevDomainName(appId: $appId, name: $name) {
id
name
}
}
}
`,
devDomainNameVariables
)
.toPromise()
);
return data.devDomainName.assignDevDomainName.name === devDomainNameVariables.name;
},
async assignAliasAsync(
graphqlClient: ExpoGraphqlClient,
aliasVariables: { appId: string; deploymentId: string; aliasName: string | null }
): Promise<AssignAliasMutation['deployments']['assignAlias']> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<AssignAliasMutation, AssignAliasMutationVariables>(
gql`
mutation AssignAlias(
$appId: ID!
$deploymentId: ID!
$aliasName: WorkerDeploymentIdentifier
) {
deployments {
assignAlias(
appId: $appId
deploymentIdentifier: $deploymentId
aliasName: $aliasName
) {
id
...WorkerDeploymentAliasFragment
workerDeployment {
id
...WorkerDeploymentFragment
}
}
}
}
${print(WorkerDeploymentFragmentNode)}
${print(WorkerDeploymentAliasFragmentNode)}
`,
aliasVariables
)
.toPromise()
);
return data.deployments.assignAlias;
},
};
-89
View File
@@ -1,89 +0,0 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { WorkerDeploymentFragmentNode } from './fragments/WorkerDeployment';
import type { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../graphql/client';
import {
type PaginatedWorkerDeploymentsQuery,
type PaginatedWorkerDeploymentsQueryVariables,
SuggestedDevDomainNameQuery,
SuggestedDevDomainNameQueryVariables,
type WorkerDeploymentFragment,
} from '../graphql/generated';
import type { Connection } from '../utils/relay';
export const DeploymentsQuery = {
async getAllDeploymentsPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, first, after, last, before }: PaginatedWorkerDeploymentsQueryVariables
): Promise<Connection<WorkerDeploymentFragment>> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<PaginatedWorkerDeploymentsQuery, PaginatedWorkerDeploymentsQueryVariables>(
gql`
query PaginatedWorkerDeployments(
$appId: String!
$first: Int
$after: String
$last: Int
$before: String
) {
app {
byId(appId: $appId) {
id
workerDeployments(first: $first, after: $after, last: $last, before: $before) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
...WorkerDeploymentFragment
}
}
}
}
}
}
${print(WorkerDeploymentFragmentNode)}
`,
{ appId, first, after, last, before },
{ additionalTypenames: ['WorkerDeployment'] }
)
.toPromise()
);
return data.app.byId.workerDeployments;
},
async getSuggestedDevDomainByAppIdAsync(
graphqlClient: ExpoGraphqlClient,
{ appId }: SuggestedDevDomainNameQueryVariables
): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<SuggestedDevDomainNameQuery, SuggestedDevDomainNameQueryVariables>(
gql`
query SuggestedDevDomainName($appId: String!) {
app {
byId(appId: $appId) {
id
suggestedDevDomainName
}
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
)
.toPromise()
);
return data.app.byId.suggestedDevDomainName;
},
};
-177
View File
@@ -1,177 +0,0 @@
import * as https from 'https';
import createHttpsProxyAgent from 'https-proxy-agent';
import mime from 'mime';
import { Gzip } from 'minizlib';
import fetch, { Headers, HeadersInit, RequestInit, Response } from 'node-fetch';
import fs, { createReadStream } from 'node:fs';
import path from 'node:path';
import promiseRetry from 'promise-retry';
const MAX_RETRIES = 4;
const MAX_CONCURRENCY = 10;
const MIN_RETRY_TIMEOUT = 100;
const MAX_UPLOAD_SIZE = 5e8; // 5MB
const MIN_COMPRESSION_SIZE = 5e4; // 50kB
const isCompressible = (contentType: string | null, size: number): boolean => {
if (size < MIN_COMPRESSION_SIZE) {
// Don't compress small files
return false;
} else if (contentType && /^(?:audio|video|image)\//i.test(contentType)) {
// Never compress images, audio, or videos as they're presumably precompressed
return false;
} else if (contentType && /^application\//i.test(contentType)) {
// Only compress `application/` files if they're marked as XML/JSON/JS
return /(?:xml|json5?|javascript)$/i.test(contentType);
} else {
return true;
}
};
export interface UploadParams extends Omit<RequestInit, 'signal' | 'body'> {
filePath: string;
compress?: boolean;
url: string;
method?: string;
headers?: HeadersInit;
body?: undefined;
signal?: AbortSignal;
}
export interface UploadResult {
params: UploadParams;
response: Response;
}
let sharedAgent: https.Agent | undefined;
const getAgent = (): https.Agent => {
if (sharedAgent) {
return sharedAgent;
} else if (process.env.https_proxy) {
return (sharedAgent = createHttpsProxyAgent(process.env.https_proxy));
} else {
return (sharedAgent = new https.Agent({
keepAlive: true,
maxSockets: MAX_CONCURRENCY,
maxTotalSockets: MAX_CONCURRENCY,
scheduling: 'lifo',
timeout: 4_000,
}));
}
};
export async function uploadAsync(params: UploadParams): Promise<UploadResult> {
const {
filePath,
signal,
compress,
method = 'POST',
url,
headers: headersInit,
...requestInit
} = params;
const stat = await fs.promises.stat(params.filePath);
if (stat.size > MAX_UPLOAD_SIZE) {
throw new Error(
`Upload of "${params.filePath}" aborted: File size is greater than the upload limit (>500MB)`
);
}
const contentType = mime.getType(path.basename(params.filePath));
return await promiseRetry(
async retry => {
const headers = new Headers(headersInit);
if (contentType) {
headers.set('content-type', contentType);
}
let bodyStream: NodeJS.ReadableStream = createReadStream(filePath);
if (compress && isCompressible(contentType, stat.size)) {
const gzip = new Gzip({ portable: true });
bodyStream.on('error', error => gzip.emit('error', error));
// @ts-expect-error: Gzip implements a Readable-like interface
bodyStream = bodyStream.pipe(gzip) as NodeJS.ReadableStream;
headers.set('content-encoding', 'gzip');
}
let response: Response;
try {
response = await fetch(params.url, {
...requestInit,
method,
body: bodyStream,
headers,
agent: getAgent(),
// @ts-expect-error: Internal types don't match
signal,
});
} catch (error) {
return retry(error);
}
if (
response.status === 408 ||
response.status === 409 ||
response.status === 429 ||
(response.status >= 500 && response.status <= 599)
) {
const message = `Upload of "${filePath}" failed: ${response.statusText}`;
const text = await response.text().catch(() => null);
return retry(new Error(text ? `${message}\n${text}` : message));
} else if (response.status === 413) {
const message = `Upload of "${filePath}" failed: File size exceeded the upload limit`;
throw new Error(message);
} else if (!response.ok) {
throw new Error(`Upload of "${filePath}" failed: ${response.statusText}`);
}
return {
params,
response,
};
},
{
retries: MAX_RETRIES,
minTimeout: MIN_RETRY_TIMEOUT,
randomize: true,
factor: 2,
}
);
}
export interface UploadPending {
params: UploadParams;
}
export type BatchUploadSignal = UploadResult | UploadPending;
export async function* batchUploadAsync(
uploads: readonly UploadParams[]
): AsyncGenerator<BatchUploadSignal> {
const controller = new AbortController();
const queue = new Set<Promise<UploadResult>>();
try {
let index = 0;
while (index < uploads.length || queue.size > 0) {
while (queue.size < MAX_CONCURRENCY && index < uploads.length) {
const uploadParams = uploads[index++];
let uploadPromise: Promise<UploadResult>;
queue.add(
(uploadPromise = uploadAsync({ ...uploadParams, signal: controller.signal }).finally(() =>
queue.delete(uploadPromise)
))
);
yield { params: uploadParams };
}
yield await Promise.race(queue);
}
if (queue.size > 0) {
controller.abort();
}
} catch (error: any) {
if (error.name !== 'AbortError') {
throw error;
}
}
}
-84
View File
@@ -1,84 +0,0 @@
import chalk from 'chalk';
import type {
WorkerDeploymentAliasFragment,
WorkerDeploymentFragment,
} from '../../graphql/generated';
import formatFields, { type FormatFieldsItem } from '../../utils/formatFields';
export const EXPO_BASE_DOMAIN = process.env.EXPO_STAGING ? 'staging.expo' : 'expo';
export function getDeploymentUrlFromFullName(deploymentFullName: string): string {
return `https://${deploymentFullName}.${EXPO_BASE_DOMAIN}.app`;
}
export function getDashboardUrl(projectId: string): string {
return `https://${EXPO_BASE_DOMAIN}.dev/projects/${projectId}/hosting/deployments`;
}
type WorkerDeploymentData = {
/** Used to generate the dashboard URL to `expo.dev` */
projectId: string;
/** The actual deployment information */
deployment: Pick<WorkerDeploymentFragment, 'deploymentIdentifier' | 'url'>;
/** All modified aliases of the deployment, if any */
aliases?: (WorkerDeploymentAliasFragment | null)[];
/** The production promoting alias of the deployment, if any */
production?: WorkerDeploymentAliasFragment | null;
};
export function formatWorkerDeploymentTable(data: WorkerDeploymentData): string {
const fields: FormatFieldsItem[] = [
{ label: 'Dashboard', value: getDashboardUrl(data.projectId) },
{ label: 'Deployment URL', value: data.deployment.url },
];
if (data.aliases?.length) {
const alias = data.aliases.filter(Boolean)[0];
if (alias) {
fields.push({ label: 'Alias URL', value: alias.url });
}
}
if (data.production) {
fields.push({ label: 'Production URL', value: data.production.url });
}
const lastUrlField = fields[fields.length - 1];
lastUrlField.value = chalk.cyan(lastUrlField.value);
return formatFields(fields);
}
type WorkerDeploymentOutput = {
/** The absolute URL to the dashboard on `expo.dev` */
dashboardUrl: string;
/** The deployment identifier */
identifier: string;
/** The deployment URL */
url: string;
/** Custom aliases, if assigned */
aliases?: { id: string; url: string; name: string }[];
/** The production alias, if assigned */
production?: { id: string; url: string };
};
export function formatWorkerDeploymentJson(data: WorkerDeploymentData): WorkerDeploymentOutput {
const aliases = !data.aliases
? []
: (data.aliases.filter(Boolean) as WorkerDeploymentAliasFragment[]);
return {
dashboardUrl: getDashboardUrl(data.projectId),
identifier: data.deployment.deploymentIdentifier,
url: data.deployment.url,
aliases: !aliases.length
? undefined
: aliases.map(alias => ({ id: alias.id, url: alias.url, name: alias.aliasName })),
production: !data.production
? undefined
: {
id: data.production.id,
url: data.production.url,
},
};
}
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more