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
250 changed files with 3286 additions and 23861 deletions

No files matched your search

-20
View File
@@ -1,20 +0,0 @@
commentConfiguration:
preamble: 'Subscribed to pull request'
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/channel/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/channel/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/schema/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
+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.
-14
View File
@@ -1,14 +0,0 @@
name: codemention
on:
pull_request_target:
types: [opened, synchronize, ready_for_review]
jobs:
codemention:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: tobyhs/[email protected]
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
-281
View File
@@ -8,291 +8,10 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Added support to read platform version from `app.json`. ([#2778](https://github.com/expo/eas-cli/pull/2778) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Show `eas deploy` upload error messages. ([#2771](https://github.com/expo/eas-cli/pull/2771) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
## [14.2.0](https://github.com/expo/eas-cli/releases/tag/v14.2.0) - 2024-12-13
### 🎉 New features
- Add `eas deploy --dry-run` flag to output tarball. ([#2761](https://github.com/expo/eas-cli/pull/2761) by [@kitten](https://github.com/kitten))
### 🐛 Bug fixes
- Remove random branch name generation for --auto branch name non-vcs fallback. ([#2747](https://github.com/expo/eas-cli/pull/2747) by [@wschurman](https://github.com/wschurman))
- Upgrade @expo/multipart-body-parser. ([#2751](https://github.com/expo/eas-cli/pull/2751) by [@wschurman](https://github.com/wschurman))
- Pass env var flag to worker deployments. ([#2763](https://github.com/expo/eas-cli/pull/2763) by [@kadikraman](https://github.com/kadikraman))
- Fix request for switching providers when doing Apple auth. ([#2769](https://github.com/expo/eas-cli/pull/2769) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Improve logging and validation in `eas env:exec` command. ([#2762](https://github.com/expo/eas-cli/pull/2762) by [@szdziedzic](https://github.com/szdziedzic))
## [14.1.0](https://github.com/expo/eas-cli/releases/tag/v14.1.0) - 2024-12-10
### 🎉 New features
- Add support for enabeling broadcast Push Notifications capability option, by setting `ios.usesBroadcastPushNotifications` to `true` in app config. ([#2748](https://github.com/expo/eas-cli/pull/2748) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Add link to FYI page mentioning the workaround for Apple SMS 2FA issue. ([#2752](https://github.com/expo/eas-cli/pull/2752) by [@szdziedzic](https://github.com/szdziedzic))
## [14.0.3](https://github.com/expo/eas-cli/releases/tag/v14.0.3) - 2024-12-09
### 🐛 Bug fixes
- Bump `@expo/apple-utils` to fix sending two-factor authentication codes via SMS. ([#2750](https://github.com/expo/eas-cli/pull/2750) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Change update message to allow faster copy and paste. ([#2661](https://github.com/expo/eas-cli/pull/2661) by [@jonluca](https://github.com/jonluca))
- Allow using `$schema` field in `eas.json`. ([#2624](https://github.com/expo/eas-cli/pull/2624) by [@saiichihashimoto](https://github.com/saiichihashimoto))
## [14.0.2](https://github.com/expo/eas-cli/releases/tag/v14.0.2) - 2024-12-06
### 🧹 Chores
- Print warning instead of throwing an error when validating `CFBundleShortVersionString` against App Store requirements. ([#2741](https://github.com/expo/eas-cli/pull/2741) by [@szdziedzic](https://github.com/szdziedzic))
## [14.0.1](https://github.com/expo/eas-cli/releases/tag/v14.0.1) - 2024-12-06
### 🧹 Chores
- Bump `@expo/apple-utils` to improve error handling and input validation and change auth headers. ([#2745](https://github.com/expo/eas-cli/pull/2745) by [@szdziedzic](https://github.com/szdziedzic))
## [14.0.0](https://github.com/expo/eas-cli/releases/tag/v14.0.0) - 2024-12-06
### 🛠 Breaking changes
- Change behavior of roll-back-to-embedded to not use current project state. ([#2722](https://github.com/expo/eas-cli/pull/2722) by [@wschurman](https://github.com/wschurman))
### 🎉 New features
- Add hidden fingerprint:compare feature. ([#2736](https://github.com/expo/eas-cli/pull/2736) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Update `@expo/config` and `@expo/config-plugins` to fix `eas build` command for bare iOS projects on Windows. ([#2744](https://github.com/expo/eas-cli/pull/2744) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Fix outdated gitignored Google services file warning. ([#2730](https://github.com/expo/eas-cli/pull/2730) by [@szdziedzic](https://github.com/szdziedzic))
## [13.4.2](https://github.com/expo/eas-cli/releases/tag/v13.4.2) - 2024-11-25
### 🧹 Chores
- Upgrade `@expo` packages to SDK 52 versions. ([#2706](https://github.com/expo/eas-cli/pull/2706) by [@szdziedzic](https://github.com/szdziedzic))
## [13.4.1](https://github.com/expo/eas-cli/releases/tag/v13.4.1) - 2024-11-22
### 🧹 Chores
- Improve logging in `eas env:pull`. ([#2720](https://github.com/expo/eas-cli/pull/2720) by [@szdziedzic](https://github.com/szdziedzic))
## [13.4.0](https://github.com/expo/eas-cli/releases/tag/v13.4.0) - 2024-11-22
### 🎉 New features
- Calculate fingerprint on each update. ([#2687](https://github.com/expo/eas-cli/pull/2687) by [@quinlanj](https://github.com/quinlanj))
- Calculate fingerprint on each update republish. ([#2708](https://github.com/expo/eas-cli/pull/2708) by [@quinlanj](https://github.com/quinlanj))
- Add `eas env` commands. ([#2711](https://github.com/expo/eas-cli/pull/2711) by [@szdziedzic](https://github.com/szdziedzic))
- Add `--environment` flag to `eas update` command. ([#2711](https://github.com/expo/eas-cli/pull/2711) by [@szdziedzic](https://github.com/szdziedzic))
- Load readable environment variables from EAS servers on every build. ([#2644](https://github.com/expo/eas-cli/pull/2644) by [@szdziedzic](https://github.com/szdziedzic))
- Add environment field to `eas.schema.json`. ([#2719](https://github.com/expo/eas-cli/pull/2719) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Deprecate `eas secret` commands. ([#2705](https://github.com/expo/eas-cli/pull/2705) by [@szdziedzic](https://github.com/szdziedzic))
## [13.3.0](https://github.com/expo/eas-cli/releases/tag/v13.3.0) - 2024-11-18
### 🎉 New features
- Added `eas workflow` commands. [#2650](https://github.com/expo/eas-cli/pull/2650), [#2669](https://github.com/expo/eas-cli/pull/2669), [#2678](https://github.com/expo/eas-cli/pull/2678) by [@jonsamp](https://github.com/jonsamp) and [@sjchmiela](https://github.com/sjchmiela) ([#2683](https://github.com/expo/eas-cli/pull/2683) by [@sjchmiela](https://github.com/sjchmiela))
## [13.2.3](https://github.com/expo/eas-cli/releases/tag/v13.2.3) - 2024-11-15
## [13.2.2](https://github.com/expo/eas-cli/releases/tag/v13.2.2) - 2024-11-15
## [13.2.1](https://github.com/expo/eas-cli/releases/tag/v13.2.1) - 2024-11-14
## [13.2.0](https://github.com/expo/eas-cli/releases/tag/v13.2.0) - 2024-11-13
### 🎉 New features
- Add EAS_SKIP_AUTO_FINGERPRINT to skip fingerprint computation on build ([#2675](https://github.com/expo/eas-cli/pull/2675) by [@quinlanj](https://github.com/quinlanj))
- Default build-logger-level to debug when EXPO_DEBUG is set. ([#2676](https://github.com/expo/eas-cli/pull/2676) by [@wschurman](https://github.com/wschurman))
## [13.1.1](https://github.com/expo/eas-cli/releases/tag/v13.1.1) - 2024-11-08
## [13.1.0](https://github.com/expo/eas-cli/releases/tag/v13.1.0) - 2024-11-07
### 🎉 New features
- Compute fingerprint for builds with SDK 52 and higher ([#2663](https://github.com/expo/eas-cli/pull/2663) by [@quinlanj](https://github.com/quinlanj))
## [13.0.1](https://github.com/expo/eas-cli/releases/tag/v13.0.1) - 2024-11-05
### 🧹 Chores
- Upgrade EAS Metadata with new properties from App Store. ([#2671](https://github.com/expo/eas-cli/pull/2671) by [@byCedric](https://github.com/byCedric))
## [13.0.0](https://github.com/expo/eas-cli/releases/tag/v13.0.0) - 2024-11-05
### 🛠 Breaking changes
- Resolve versioned expo config using `npx expo config` command instead of using fixed `@expo/config` version shipped with EAS CLI, if available. ([#2529](https://github.com/expo/eas-cli/pull/2529) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Add `macos-sonoma-14.6-xcode-16.1` image and `sdk-52` image tag to `eas.schema.json`. ([#2666](https://github.com/expo/eas-cli/pull/2666) by [@szdziedzic](https://github.com/szdziedzic))
- Update `@expo` packages versions. ([#2530](https://github.com/expo/eas-cli/pull/2530) by [@szdziedzic](https://github.com/szdziedzic))
## [12.6.2](https://github.com/expo/eas-cli/releases/tag/v12.6.2) - 2024-10-29
### 🐛 Bug fixes
- Make email case insensitive during Apple authentication. ([#2659](https://github.com/expo/eas-cli/pull/2659) by [@byCedric](https://github.com/byCedric))
## [12.6.1](https://github.com/expo/eas-cli/releases/tag/v12.6.1) - 2024-10-25
### 🧹 Chores
- Create dynamic logged in context field and clean up erroneous SessionManager context field uses. ([#2648](https://github.com/expo/eas-cli/pull/2648) by [@wschurman](https://github.com/wschurman))
## [12.6.0](https://github.com/expo/eas-cli/releases/tag/v12.6.0) - 2024-10-21
### 🎉 New features
- Add `eas channel:pause` and `eas channel:resume` commands to pause/resume update delivery to builds using specific update channels and display paused status in channel details output. ([#2614](https://github.com/expo/eas-cli/pull/2614) by [@fiberjw](https://github.com/fiberjw))
- Add interactivity to eas update:edit command. ([#2638](https://github.com/expo/eas-cli/pull/2638) by [@wschurman](https://github.com/wschurman))
## [12.5.4](https://github.com/expo/eas-cli/releases/tag/v12.5.4) - 2024-10-19
## [12.5.3](https://github.com/expo/eas-cli/releases/tag/v12.5.3) - 2024-10-16
## [12.5.2](https://github.com/expo/eas-cli/releases/tag/v12.5.2) - 2024-10-11
### 🐛 Bug fixes
- Disallow republishing an update that is being rolled-out. ([#2602](https://github.com/expo/eas-cli/pull/2602) by [@wschurman](https://github.com/wschurman))
- Bump `@expo/apple-utils` to `2.0.2` resolving the Apple authentication error. ([#2641](https://github.com/expo/eas-cli/pull/2641) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Implement new `worker` deploy API flow. ([#2601](https://github.com/expo/eas-cli/pull/2601) by [@kitten](https://github.com/kitten)))
- Unhide rollout-percentage flag on update publish command. ([#2608](https://github.com/expo/eas-cli/pull/2608) by [@wschurman](https://github.com/wschurman))
- Let folks know about the new concurrency add-on ([#2611](https://github.com/expo/eas-cli/pull/2611) by [@brentvatne](https://github.com/brentvatne))
## [12.5.1](https://github.com/expo/eas-cli/releases/tag/v12.5.1) - 2024-09-27
### 🐛 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
+16
View File
@@ -0,0 +1,16 @@
* @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/submit @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @szdziedzic @khamilowicz @sjchmiela
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "14.2.0",
"version": "11.0.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+165 -443
View File
File diff suppressed because it is too large. Load diff
-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:
+967 -9742
View File
File diff suppressed because it is too large. Load diff
+18 -31
View File
@@ -1,34 +1,34 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "14.2.0",
"version": "11.0.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "2.1.5",
"@expo/apple-utils": "1.7.1",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.156",
"@expo/eas-json": "14.2.0",
"@expo/env": "^0.4.0",
"@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": "2.0.0",
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.6.1",
"@expo/pkcs12": "0.1.3",
"@expo/plist": "0.2.0",
"@expo/multipart-body-parser": "1.1.0",
"@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": "8.0.17",
"@expo/prebuild-config": "7.0.8",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.156",
"@expo/steps": "1.0.130",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -41,7 +41,6 @@
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"dateformat": "4.6.3",
"diff": "7.0.0",
"dotenv": "16.3.1",
"env-paths": "2.2.0",
"envinfo": "7.11.0",
@@ -64,8 +63,7 @@
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.1.2",
"minizlib": "3.0.1",
"nanoid": "3.3.8",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
@@ -80,15 +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",
"yaml": "2.6.0",
"zod": "^3.23.8"
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
@@ -97,7 +92,6 @@
"@graphql-codegen/typescript-operations": "4.0.1",
"@types/cli-progress": "3.11.5",
"@types/dateformat": "3.0.1",
"@types/diff": "6.0.0",
"@types/envinfo": "7.8.3",
"@types/express": "4.17.15",
"@types/fs-extra": "11.0.4",
@@ -111,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",
@@ -179,22 +172,16 @@
"secret": {
"description": "manage project and account secrets"
},
"env": {
"description": "manage project and account environment variables"
},
"update": {
"description": "manage individual updates"
},
"webhook": {
"description": "manage webhooks"
},
"workflow": {
"description": "manage workflows"
}
},
"warn-if-update-available": {
"timeoutInDays": 0.5,
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run:') %>\n<%= chalk.dim(chalk.bold('npm install -g eas-cli')) %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
},
"update": {
"node": {
+18 -53
View File
@@ -1,20 +1,6 @@
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
],
"definitions": {
"apple": {
"AppleKidsAge": {
@@ -337,8 +323,7 @@
"gambling",
"unrestrictedWebAccess",
"kidsAgeBand",
"ageRatingOverride",
"koreaAgeRatingOverride"
"seventeenPlus"
],
"defaultSnippets": [
{
@@ -360,9 +345,7 @@
"gambling": false,
"unrestrictedWebAccess": false,
"kidsAgeBand": null,
"ageRatingOverride": "NONE",
"koreaAgeRatingOverride": "NONE",
"lootBox": false
"seventeenPlus": false
}
},
{
@@ -384,9 +367,7 @@
"unrestrictedWebAccess": false,
"gambling": false,
"kidsAgeBand": null,
"ageRatingOverride": "${13|NONE,SEVENTEEN_PLUS,UNRATED|}",
"koreaAgeRatingOverride": "${14|NONE,FIFTEEN_PLUS,NINETEEN_PLUS|}",
"lootBox": false
"seventeenPlus": false
}
}
],
@@ -461,24 +442,8 @@
},
"seventeenPlus": {
"type": "boolean",
"deprecated": true,
"deprecationMessage": "This property is deprecated, use `ageRatingOverride` instead.",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
},
"ageRatingOverride": {
"$ref": "#/definitions/apple/AppleAgeRatingOverride",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
},
"koreaAgeRatingOverride": {
"$ref": "#/definitions/apple/AppleKoreaAgeRatingOverride",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
},
"lootBox": {
"type": "boolean",
"description": "Does your app contain purchasable loot boxes? These virtual containers provide players with in-game items, including functional cards or cosmetic modifications, based on random chance."
}
}
},
@@ -600,20 +565,6 @@
"maxLength": 4000
}
}
},
"AppleAgeRatingOverride": {
"enum": [
"NONE",
"SEVENTEEN_PLUS",
"UNRATED"
]
},
"AppleKoreaAgeRatingOverride": {
"enum": [
"NONE",
"FIFTEEN_PLUS",
"NINETEEN_PLUS"
]
}
},
"AppleConfig": {
@@ -824,5 +775,19 @@
}
}
}
}
},
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
]
}
@@ -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';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import { ExpoConfig } from '@expo/config-types';
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,
},
};
}
+8 -11
View File
@@ -1,5 +1,4 @@
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { BranchNotFoundError } from './utils';
@@ -9,11 +8,10 @@ import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranchBasicInfoFragment,
UpdateBranch,
UpdateBranchFragment,
} from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import { UpdateBranchBasicInfoFragmentNode } from '../graphql/types/UpdateBranchBasicInfo';
import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
@@ -128,7 +126,7 @@ function renderPageOfBranches(
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<UpdateBranchBasicInfoFragment> {
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
@@ -137,17 +135,15 @@ export async function createUpdateBranchOnAppAsync(
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
...UpdateBranchBasicInfoFragment
name
}
}
}
${print(UpdateBranchBasicInfoFragmentNode)}
`,
{
appId,
name,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
@@ -167,21 +163,22 @@ export async function ensureBranchExistsAsync(
appId: string;
branchName: string;
}
): Promise<{ branch: UpdateBranchBasicInfoFragment; createdBranch: boolean }> {
): Promise<{ branchId: string; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
return { branch: updateBranch, createdBranch: false };
const { id } = updateBranch;
return { branchId: id, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branch: newUpdateBranch, createdBranch: true };
return { branchId: newUpdateBranch.id, createdBranch: true };
} else {
throw error;
}
+4 -2
View File
@@ -1,7 +1,9 @@
import { Client } from '../vcs/vcs';
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string | null> {
return await vcsClient.getBranchNameAsync();
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
return (
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
);
}
export class BranchNotFoundError extends Error {
@@ -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 (
@@ -115,10 +115,6 @@ export async function prepareJobAsync(
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
appId: ctx.projectId,
initiatingUserId: ctx.user.id,
};
return sanitizeBuildJob(job) as Android.Job;
+6 -15
View File
@@ -20,11 +20,7 @@ import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import {
bumpAppVersionAsync,
ensureStaticConfigExists,
getVersionConfigTarget,
} from '../utils/version';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
export enum BumpStrategy {
APP_VERSION,
@@ -57,11 +53,9 @@ export async function bumpVersionAsync({
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.ANDROID });
const version = versionGetter(exp);
await updateNativeVersionsAsync({
projectDir,
version,
version: exp.version,
versionCode: exp.android?.versionCode,
});
Log.log('Synchronized versions with build gradle');
@@ -85,7 +79,7 @@ export async function bumpVersionInAppJsonAsync({
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
await bumpAppVersionAsync({ appVersion, projectDir, exp, platform: Platform.ANDROID });
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = getNextVersionCode(versionCode);
@@ -124,11 +118,9 @@ export async function maybeResolveVersionsAsync(
return {};
}
} else {
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.ANDROID });
const appVersion = versionGetter(exp);
return {
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
appVersion,
appVersion: exp.version,
};
}
}
@@ -210,7 +202,6 @@ export async function resolveRemoteVersionCodeAsync(
applicationId
);
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.ANDROID });
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
@@ -239,7 +230,7 @@ export async function resolveRemoteVersionCodeAsync(
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? versionGetter(exp) ?? '1.0.0',
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
@@ -263,7 +254,7 @@ export async function resolveRemoteVersionCodeAsync(
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? versionGetter(exp) ?? '1.0.0',
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: String(nextBuildVersion),
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
+64 -77
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,12 +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 { createFingerprintAsync } from '../utils/fingerprintCli';
import { printJsonOnlyOutput } from '../utils/json';
import { getTmpDirectory } from '../utils/paths';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -175,9 +176,8 @@ export async function prepareBuildRequestForPlatformAsync<
}
assert(projectArchive);
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync(ctx);
const metadata = await collectMetadataAsync(ctx, runtimeAndFingerprintMetadata);
const runtimeMetadata = await createAndMaybeUploadFingerprintAsync(ctx);
const metadata = await collectMetadataAsync(ctx, runtimeMetadata);
const buildParams = resolveBuildParamsInput(ctx, metadata);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
@@ -186,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, {
@@ -488,8 +488,8 @@ async function handleSingleBuildProgressAsync(
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
formatAccountBillingUrl(accountName)
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
formatAccountSubscriptionsUrl(accountName)
)}.`;
break;
case BuildStatus.InQueue: {
@@ -511,7 +511,7 @@ async function handleSingleBuildProgressAsync(
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountBillingUrl(accountName)
formatAccountSubscriptionsUrl(accountName)
)}`
);
}
@@ -658,46 +658,25 @@ function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
}
}
function formatAccountBillingUrl(accountName: string): string {
return new URL(`/accounts/${accountName}/settings/billing`, getExpoWebsiteBaseUrl()).toString();
function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(
`/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
).toString();
}
async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
}> {
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
if (!runtimeAndFingerprintMetadata?.fingerprintHash) {
const fingerprint = await computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync(ctx);
return {
...runtimeAndFingerprintMetadata,
...fingerprint,
};
} else {
return {
...runtimeAndFingerprintMetadata,
fingerprintHash: runtimeAndFingerprintMetadata.runtimeVersion,
};
}
}
async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Platform>(
async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string;
fingerprintSource?: FingerprintSource;
fingerprintHash?: string;
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,
});
@@ -706,54 +685,62 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
}
/**
* 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,
};
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: resolvedRuntimeVersion.runtimeVersion,
fingerprint: resolvedRuntimeVersion.fingerprint,
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
});
return {
runtimeVersion: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
fingerprintHash: resolvedRuntimeVersion.fingerprintHash ?? undefined,
};
}
await fs.mkdirp(getTmpDirectory());
const fingerprintLocation = path.join(getTmpDirectory(), `${uuidv4()}-runtime-fingerprint.json`);
async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends Platform>(
ctx: BuildContext<T>,
{ debug }: { debug?: boolean } = {}
): Promise<{
fingerprintHash?: string;
fingerprintSource?: FingerprintSource;
}> {
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
workflow: ctx.workflow,
platforms: [ctx.platform],
env: ctx.env,
await fs.writeJSON(fingerprintLocation, {
hash: resolvedRuntimeVersion.runtimeVersion,
sources: resolvedRuntimeVersion.fingerprint.fingerprintSources,
});
if (!fingerprint) {
return {};
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
fingerprintSource: {
type: FingerprintSourceType.PATH,
path: fingerprintLocation,
isDebugFingerprint: resolvedRuntimeVersion.fingerprint.isDebugFingerprintSource,
},
};
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: fingerprint.hash,
fingerprint: {
fingerprintSources: fingerprint.sources,
isDebugFingerprintSource: debug ?? false,
},
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
});
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 {
fingerprintHash: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
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,157 +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() ??
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
if (!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>;
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}
if (buildProfile.env && Object.keys(buildProfile.env).length > 0) {
Log.log(
`Environment variables loaded from the "${buildProfileName}" build profile "env" configuration: ${
buildProfile.env && Object.keys(buildProfile.env).join(', ')
}.`
);
}
if (
buildProfile.env &&
Object.keys(buildProfile.env).length > 0 &&
Object.keys(serverEnvVars).length > 0
) {
const overlappingKeys = Object.keys(serverEnvVars).filter(
key => buildProfile.env && Object.keys(buildProfile.env).includes(key)
);
if (overlappingKeys.length > 0) {
Log.warn(
`The following environment variables are defined in both the "${buildProfileName}" build profile "env" configuration and the "${environment.toLowerCase()}" environment on EAS servers: ${overlappingKeys.join(
', '
)}. The values from the build profile configuration will be used.`
);
}
}
Log.newLine();
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 };
}
}
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
buildProfile: BuildProfile
): EnvironmentVariableEnvironment {
const setEnvironmentMessage =
'Set the "environment" field in the build profile if you want to specify the environment manually.';
if (buildProfile.distribution === 'store') {
Log.log(
`We detected that you are building for the "store" distribution. Resolving the environment for environment variables used during the build to "production". ${setEnvironmentMessage}`
);
return EnvironmentVariableEnvironment.Production;
} else {
if (buildProfile.developmentClient) {
Log.log(
`We detected that you are building the development client. Resolving the environment for environment variables used during the build to "development". ${setEnvironmentMessage}`
);
return EnvironmentVariableEnvironment.Development;
}
Log.log(
`We detected that you are building for the "internal" distribution. Resolving the environment for environment variables used during the build to "preview". ${setEnvironmentMessage}`
);
return EnvironmentVariableEnvironment.Preview;
}
}
+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 {
@@ -300,6 +300,25 @@ describe(readShortVersionAsync, () => {
);
expect(buildNumber).toBe('1.0.0');
});
it('fails when build number is invalid', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
await expect(
readShortVersionAsync(
'/app',
exp,
{
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
},
vcsClient
)
).rejects.toThrowError(
'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'
);
});
});
describe('managed project', () => {
+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 (
@@ -110,10 +110,6 @@ export async function prepareJobAsync(
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
appId: ctx.projectId,
initiatingUserId: ctx.user.id,
};
return sanitizeBuildJob(job) as Ios.Job;
}
+12 -36
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';
@@ -21,13 +21,9 @@ import uniqBy from '../../utils/expodash/uniqBy';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import {
bumpAppVersionAsync,
ensureStaticConfigExists,
getVersionConfigTarget,
} from '../utils/version';
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,
@@ -52,11 +48,9 @@ export async function bumpVersionAsync({
ensureStaticConfigExists(projectDir);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.IOS });
const version = versionGetter(exp);
await updateNativeVersionsAsync({
projectDir,
version,
version: exp.version,
buildNumber: exp.ios?.buildNumber,
targets,
});
@@ -79,7 +73,7 @@ export async function bumpVersionInAppJsonAsync({
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = IOSConfig.Version.getVersion(exp);
await bumpAppVersionAsync({ appVersion, projectDir, exp, platform: Platform.IOS });
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (isValidBuildNumber(buildNumber)) {
@@ -106,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) {
Log.warn(
`The required format for "version" field from app.json/app.config.ts for App Store iOS builds 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 format required by Apple. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
} else {
Log.warn(
`The required format for "CFBundleShortVersionString" in Info.plist for App Store iOS builds 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 format required by Apple. ${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}`
);
}
}
@@ -144,13 +122,11 @@ export async function readShortVersionAsync(
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
validateShortVersion({ shortVersion, workflow });
validateShortVersion(shortVersion);
return shortVersion;
} else {
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.IOS });
const shortVersion = versionGetter(exp);
validateShortVersion({ shortVersion, workflow });
return shortVersion;
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.152';
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;
+3 -5
View File
@@ -16,9 +16,8 @@ import { easCliVersion } from '../utils/easCli';
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>,
runtimeAndFingerprintMetadata: {
runtimeMetadata: {
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
}
): Promise<Metadata> {
@@ -31,9 +30,8 @@ export async function collectMetadataAsync<T extends Platform>(
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion,
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
fingerprintSource: runtimeAndFingerprintMetadata?.fingerprintSource,
runtimeVersion: runtimeMetadata?.runtimeVersion,
fingerprintSource: runtimeMetadata?.fingerprintSource,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelObject,
distribution,
+16 -44
View File
@@ -1,7 +1,8 @@
import { ExpoConfig } from '@expo/config';
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,
@@ -381,10 +371,9 @@ async function prepareAndStartBuildAsync({
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
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;
}
@@ -1,148 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { updateAppJsonConfigAsync } from '../appJson';
import { bumpAppVersionAsync, getVersionConfigTarget } from '../version';
jest.mock('../appJson', () => ({
__esModule: true,
updateAppJsonConfigAsync: jest.fn().mockImplementation(
async (
{
exp,
}: {
projectDir: string;
exp: ExpoConfig;
},
modifyConfig: (config: any) => void
) => {
// a mocked implementation that only mutates the config object without writing to disk
modifyConfig(exp);
}
),
}));
describe(bumpAppVersionAsync, () => {
const name = 'test';
const slug = 'test';
const projectDir = '/app';
const mockUpdateAppJsonConfigAsync = updateAppJsonConfigAsync as jest.MockedFunction<
typeof updateAppJsonConfigAsync
>;
it('should bump expo.version for valid semver', async () => {
const appVersion = '1.0.0';
const exp: ExpoConfig = {
name,
slug,
version: appVersion,
};
await bumpAppVersionAsync({ appVersion, projectDir, exp, platform: Platform.IOS });
expect(mockUpdateAppJsonConfigAsync).toHaveBeenCalled();
expect(exp.version).toBe('1.0.1');
});
it('should bump expo.android.version if the expo.android.version exists', async () => {
const exp: ExpoConfig = {
name,
slug,
version: '0.0.0',
android: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '1.0.0',
},
ios: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '2.0.0',
},
};
await bumpAppVersionAsync({ appVersion: '1.0.0', projectDir, exp, platform: Platform.ANDROID });
expect(mockUpdateAppJsonConfigAsync).toHaveBeenCalled();
expect(exp.version).toBe('0.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.android.version).toBe('1.0.1');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.ios.version).toBe('2.0.0');
});
it('should bump expo.ios.version if the expo.ios.version exists', async () => {
const exp: ExpoConfig = {
name,
slug,
version: '0.0.0',
android: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '1.0.0',
},
ios: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '2.0.0',
},
};
await bumpAppVersionAsync({ appVersion: '2.0.0', projectDir, exp, platform: Platform.IOS });
expect(mockUpdateAppJsonConfigAsync).toHaveBeenCalled();
expect(exp.version).toBe('0.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.android.version).toBe('1.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.ios.version).toBe('2.0.1');
});
});
describe(getVersionConfigTarget, () => {
const exp: ExpoConfig = {
name: 'test',
slug: 'test',
version: '0.0.0',
android: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '1.0.0',
},
ios: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '2.0.0',
},
};
it('should return the correct config target for the android platform', () => {
const { fieldName, versionGetter, versionUpdater } = getVersionConfigTarget({
exp,
platform: Platform.ANDROID,
});
expect(fieldName).toBe('expo.android.version');
expect(versionGetter(exp)).toBe('1.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(versionUpdater(exp, '3.3.3').android.version).toBe('3.3.3');
expect(versionUpdater(exp, '0.0.0').version).toBe('0.0.0');
});
it('should return the correct config target for the ios platform', () => {
const { fieldName, versionGetter, versionUpdater } = getVersionConfigTarget({
exp,
platform: Platform.IOS,
});
expect(fieldName).toBe('expo.ios.version');
expect(versionGetter(exp)).toBe('2.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(versionUpdater(exp, '3.3.3').ios.version).toBe('3.3.3');
expect(versionUpdater(exp, '0.0.0').version).toBe('0.0.0');
});
it('should return the correct config target for common version', () => {
const exp: ExpoConfig = {
name: 'test',
slug: 'test',
version: '0.0.0',
};
const { fieldName, versionGetter, versionUpdater } = getVersionConfigTarget({
exp,
platform: Platform.IOS,
});
expect(fieldName).toBe('expo.version');
expect(versionGetter(exp)).toBe('0.0.0');
expect(versionUpdater(exp, '3.3.3').version).toBe('3.3.3');
});
});
-22
View File
@@ -50,25 +50,3 @@ export function getUpdateGroupUrl(
getExpoWebsiteBaseUrl()
).toString();
}
export function getWorkflowRunUrl(
accountName: string,
projectName: string,
workflowRunId: string
): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/workflows/${workflowRunId}`,
getExpoWebsiteBaseUrl()
).toString();
}
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/github`,
getExpoWebsiteBaseUrl()
).toString();
}
+3 -58
View File
@@ -1,5 +1,4 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
@@ -19,25 +18,21 @@ export async function bumpAppVersionAsync({
appVersion,
projectDir,
exp,
platform,
}: {
appVersion: string;
projectDir: string;
exp: ExpoConfig;
platform: Platform;
}): Promise<void> {
const { fieldName, versionUpdater } = getVersionConfigTarget({ exp, platform });
let bumpedAppVersion: string;
if (semver.valid(appVersion)) {
bumpedAppVersion = nullthrows(semver.inc(appVersion, 'patch'));
Log.log(
`Bumping ${chalk.bold(fieldName)} from ${chalk.bold(appVersion)} to ${chalk.bold(
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(appVersion)} to ${chalk.bold(
bumpedAppVersion
)}`
);
} else {
Log.log(`${chalk.bold(fieldName)} = ${chalk.bold(appVersion)} is not a valid semver`);
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(appVersion)} is not a valid semver`);
bumpedAppVersion = (
await promptAsync({
type: 'text',
@@ -47,56 +42,6 @@ export async function bumpAppVersionAsync({
).bumpedAppVersion;
}
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
versionUpdater(config, bumpedAppVersion);
config.version = bumpedAppVersion;
});
}
/**
* Get the target version field from ExpoConfig based on the platform.
*/
export function getVersionConfigTarget({
exp,
platform,
}: {
exp: ExpoConfig;
platform: Platform;
}): {
fieldName: string;
versionGetter: (config: ExpoConfig) => string | undefined;
versionUpdater: (config: ExpoConfig, version: string) => ExpoConfig;
} {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
if (platform === Platform.ANDROID && typeof exp.android?.version === 'string') {
return {
fieldName: 'expo.android.version',
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
versionGetter: config => config.android?.version,
versionUpdater: (config, version) => {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
config.android = { ...config.android, version };
return config;
},
};
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
} else if (platform === Platform.IOS && typeof exp.ios?.version === 'string') {
return {
fieldName: 'expo.ios.version',
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
versionGetter: config => config.ios?.version,
versionUpdater: (config, version) => {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
config.ios = { ...config.ios, version };
return config;
},
};
}
return {
fieldName: 'expo.version',
versionGetter: config => config.version,
versionUpdater: (config, version) => {
config.version = version;
return config;
},
};
}
+8 -12
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();
}
@@ -27,28 +27,24 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const googleServicesEnvVar =
ctx.platform === Platform.ANDROID
? ctx.env.GOOGLE_SERVICES_JSON
: ctx.env.GOOGLE_SERVICES_INFO_PLIST;
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await ctx.vcsClient.isFileIgnoredAsync(
path.relative(rootDir, absGoogleServicesFilePath)
))) &&
!googleServicesEnvVar
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
);
Log.warn(
`Use EAS file environment variables with secret or sensitive visibility to pass all values that you don't want to include in your version control to build process. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
)}`
);
Log.warn(
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
);
Log.newLine();
}
}
@@ -128,7 +128,6 @@ export const testChannelObject: UpdateChannelObject = {
branchMapping:
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
updateBranches: [testUpdateBranch1, testUpdateBranch2],
isPaused: false,
__typename: 'UpdateChannel',
};
+26 -34
View File
@@ -19,7 +19,7 @@ import {
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
import Log, { learnMore } from '../log';
import Log from '../log';
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
@@ -110,11 +110,7 @@ export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
}
): Promise<void> {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName });
renderChannelHeaderContent({
channelName: channel.name,
channelId: channel.id,
isPaused: channel.isPaused,
});
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
@@ -171,11 +167,7 @@ function renderPageOfChannels(
printJsonOnlyOutput({ currentPage });
} else {
for (const channel of currentPage) {
renderChannelHeaderContent({
channelName: channel.name,
channelId: channel.id,
isPaused: channel.isPaused,
});
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
Log.addNewLineIfNone();
logChannelDetails(channel);
@@ -204,11 +196,9 @@ function renderPageOfBranchesOnChannel(
function renderChannelHeaderContent({
channelName,
channelId,
isPaused,
}: {
channelName: string;
channelId: string;
isPaused: boolean;
}): void {
Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:'));
@@ -216,7 +206,6 @@ function renderChannelHeaderContent({
formatFields([
{ label: 'Name', value: channelName },
{ label: 'ID', value: channelId },
{ label: 'Status', value: isPaused ? 'Paused' : 'Active' },
])
);
Log.addNewLineIfNone();
@@ -303,6 +292,7 @@ export async function doesChannelExistAsync(
}
/**
*
* Creates a channel and links it to a branch with the same name.
*
* @param appId the app ID, also known as the project ID
@@ -318,7 +308,7 @@ export async function createAndLinkChannelAsync(
}: { appId: string; channelName: string; shouldPrintJson?: boolean }
): Promise<ChannelBasicInfo> {
let branchId: string;
let hasCreatedBranch = false;
let branchMessage: string;
try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
@@ -326,6 +316,7 @@ export async function createAndLinkChannelAsync(
name: channelName,
});
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
@@ -333,7 +324,7 @@ export async function createAndLinkChannelAsync(
name: channelName,
});
branchId = newBranch.id;
hasCreatedBranch = true;
branchMessage = `We also went ahead and made a branch with the same name`;
} else {
throw error;
}
@@ -356,25 +347,26 @@ export async function createAndLinkChannelAsync(
if (shouldPrintJson) {
printJsonOnlyOutput(newChannel);
} else {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, appId);
const learnMoreLink = learnMore('https://docs.expo.dev/eas-update/eas-cli/');
Log.addNewLineIfNone();
if (hasCreatedBranch) {
Log.withTick(
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} and branch ${chalk.bold(
`"${newChannel.name}"`
)} on ${chalk.bold(projectDisplayName)} project. ${learnMoreLink}`
);
} else {
Log.withTick(
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} on ${chalk.bold(
projectDisplayName
)} project and connected it with existing ${chalk.bold(
`"${newChannel.name}"`
)} branch. ${learnMoreLink}`
);
}
Log.withTick(
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(graphqlClient, appId)
)}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
}
return newChannel;
}
@@ -6,7 +6,6 @@ import nullthrows from 'nullthrows';
import AnalyticsContextField from './context/AnalyticsContextField';
import ContextField from './context/ContextField';
import DynamicLoggedInContextField from './context/DynamicLoggedInContextField';
import {
DynamicPrivateProjectConfigContextField,
DynamicPublicProjectConfigContextField,
@@ -16,8 +15,6 @@ import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
import { OptionalPrivateProjectConfigContextField } from './context/OptionalPrivateProjectConfigContextField';
import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import { ProjectIdContextField } from './context/ProjectIdContextField';
import { ServerSideEnvironmentVariablesContextField } from './context/ServerSideEnvironmentVariablesContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
import VcsClientContextField from './context/VcsClientContextField';
import { EasCommandError } from './errors';
@@ -26,7 +23,6 @@ import {
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import Log from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
@@ -47,26 +43,8 @@ export type ContextOutput<
[P in keyof T]: T[P];
};
type GetContextType<Type> = {
[Property in keyof Type]: any;
};
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
interface BaseGetContextAsyncArgs {
nonInteractive: boolean;
vcsClientOverride?: Client;
}
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
extends BaseGetContextAsyncArgs {
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
}
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
withServerSideEnvironment?: never;
}
export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = {
/**
@@ -83,16 +61,8 @@ export default abstract class EasCommand extends Command {
MaybeLoggedIn: {
maybeLoggedIn: new MaybeLoggedInContextField(),
},
/**
* Specify this context if the logged-in requirement is only necessary in a particular execution of the command.
*/
DynamicLoggedIn: {
// eslint-disable-next-line async-protect/async-suffix
getDynamicLoggedInAsync: new DynamicLoggedInContextField(),
},
/**
* Specify this context requirement if the command needs to mutate the user session.
* @deprecated Should not be used outside of session management commands, which currently only includes `login` and `logout`.
*/
SessionManagment: {
sessionManager: new SessionManagementContextField(),
@@ -102,7 +72,7 @@ export default abstract class EasCommand extends Command {
* run within a project directory, null otherwise.
*/
OptionalProjectConfig: {
optionalPrivateProjectConfig: new OptionalPrivateProjectConfigContextField(),
privateProjectConfig: new OptionalPrivateProjectConfigContextField(),
},
/**
* Require this command to be run in a project directory. Return the project directory in the context.
@@ -137,16 +107,6 @@ export default abstract class EasCommand extends Command {
Vcs: {
vcsClient: new VcsClientContextField(),
},
ServerSideEnvironmentVariables: {
// eslint-disable-next-line async-protect/async-suffix
getServerSideEnvironmentVariablesAsync: new ServerSideEnvironmentVariablesContextField(),
},
/**
* Require the project to be identified and registered on server. Returns the project ID evaluated from the app config.
*/
ProjectId: {
projectId: new ProjectIdContextField(),
},
};
/**
@@ -181,19 +141,7 @@ export default abstract class EasCommand extends Command {
} = object,
>(
commandClass: { contextDefinition: ContextInput<C> },
{
nonInteractive,
vcsClientOverride,
// if specified and not null, the env vars from the selected environment will be fetched from the server
// to resolve dynamic config (if dynamic config context is used) and enable getServerSideEnvironmentVariablesAsync function (if server side environment variables context is used)
withServerSideEnvironment,
}: C extends
| GetContextType<typeof EasCommand.ContextOptions.ProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.DynamicProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.OptionalProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.ServerSideEnvironmentVariables>
? GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
: GetContextAsyncArgsWithoutServerSideEnvironmentArgument
{ nonInteractive, vcsClientOverride }: { nonInteractive: boolean; vcsClientOverride?: Client }
): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition;
@@ -207,7 +155,6 @@ export default abstract class EasCommand extends Command {
sessionManager: this.sessionManager,
analytics: this.analytics,
vcsClientOverride,
withServerSideEnvironment,
}),
]);
}
+1 -9
View File
@@ -38,12 +38,7 @@ export async function fetchBuildsAsync({
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
filters?: {
statuses?: BuildStatus[];
platform?: RequestedPlatform;
profile?: string;
hasFingerprint?: boolean;
};
filters?: { statuses?: BuildStatus[]; platform?: RequestedPlatform; profile?: string };
}): Promise<BuildFragment[]> {
let builds: BuildFragment[];
const queryFilters: InputMaybe<BuildFilter> = {};
@@ -53,9 +48,6 @@ export async function fetchBuildsAsync({
if (filters?.profile) {
queryFilters['buildProfile'] = filters.profile;
}
if (filters?.hasFingerprint) {
queryFilters['hasFingerprint'] = filters.hasFingerprint;
}
if (!filters?.statuses) {
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
@@ -1,5 +1,4 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import SessionManager from '../../user/SessionManager';
import { Client } from '../../vcs/vcs';
@@ -8,10 +7,6 @@ export interface ContextOptions {
analytics: Analytics;
nonInteractive: boolean;
vcsClientOverride?: Client;
/**
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
*/
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
}
export default abstract class ContextField<T> {
@@ -1,36 +0,0 @@
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
export type DynamicLoggedInContextFn = () => Promise<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
authenticationInfo: LoggedInAuthenticationInfo;
}>;
export default class DynamicLoggedInContextField extends ContextField<DynamicLoggedInContextFn> {
async getValueAsync({
nonInteractive,
sessionManager,
}: ContextOptions): Promise<DynamicLoggedInContextFn> {
return async () => {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
const graphqlClient = createGraphqlClient(authenticationInfo);
return {
actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
authenticationInfo,
};
};
}
}
@@ -1,14 +1,12 @@
import { ExpoConfig } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import {
ExpoConfigOptions,
getPrivateExpoConfigAsync,
getPublicExpoConfigAsync,
getPrivateExpoConfig,
getPublicExpoConfig,
} from '../../project/expoConfig';
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
@@ -21,34 +19,15 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const expBefore = await getPublicExpoConfigAsync(projectDir, options);
const expBefore = getPublicExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
});
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
options = {
...options,
env: {
...options?.env,
...serverSideEnvironmentVariables,
},
};
}
const exp = await getPublicExpoConfigAsync(projectDir, options);
const exp = getPublicExpoConfig(projectDir, options);
return {
exp,
projectDir,
@@ -62,34 +41,15 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const expBefore = await getPrivateExpoConfigAsync(projectDir, options);
const expBefore = getPrivateExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
});
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
options = {
...options,
env: {
...options?.env,
...serverSideEnvironmentVariables,
},
};
}
const exp = await getPrivateExpoConfigAsync(projectDir, options);
const exp = getPrivateExpoConfig(projectDir, options);
return {
exp,
projectDir,
@@ -1,22 +1,19 @@
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
type LoggedInContextType = {
export default class LoggedInContextField extends ContextField<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
authenticationInfo: LoggedInAuthenticationInfo;
};
export default class LoggedInContextField extends ContextField<LoggedInContextType> {
async getValueAsync({
nonInteractive,
sessionManager,
}: ContextOptions): Promise<LoggedInContextType> {
}> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
@@ -28,7 +25,6 @@ export default class LoggedInContextField extends ContextField<LoggedInContextTy
actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
authenticationInfo,
};
}
}
@@ -4,18 +4,16 @@ import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
type MaybeLoggedInContextType = {
export default class MaybeLoggedInContextField extends ContextField<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
authenticationInfo: {
accessToken: string | null;
sessionSecret: string | null;
};
};
export default class MaybeLoggedInContextField extends ContextField<MaybeLoggedInContextType> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<MaybeLoggedInContextType> {
}> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const authenticationInfo = {
accessToken: sessionManager.getAccessToken(),
sessionSecret: sessionManager.getSessionSecret(),
@@ -29,7 +27,6 @@ export default class MaybeLoggedInContextField extends ContextField<MaybeLoggedI
actor: actor ?? null,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
authenticationInfo,
};
}
}
@@ -1,12 +1,10 @@
import { ExpoConfig } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
import { getPrivateExpoConfig } from '../../project/expoConfig';
export class OptionalPrivateProjectConfigContextField extends ContextField<
| {
@@ -16,11 +14,7 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
}
| undefined
> {
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<
| {
projectId: string;
exp: ExpoConfig;
@@ -41,24 +35,11 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
return undefined;
}
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const expBefore = getPrivateExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
let serverSideEnvVars: Record<string, string> | undefined;
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
const exp = getPrivateExpoConfig(projectDir);
return {
exp,
projectDir,
@@ -1,45 +1,26 @@
import { ExpoConfig } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
import { getPrivateExpoConfig } from '../../project/expoConfig';
export class PrivateProjectConfigContextField extends ContextField<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<{
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const expBefore = getPrivateExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
let serverSideEnvVars: Record<string, string> | undefined;
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
const exp = getPrivateExpoConfig(projectDir);
return {
projectId,
@@ -1,15 +0,0 @@
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
export class ProjectIdContextField extends ContextField<string> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<string> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
return projectId;
}
}
@@ -1,42 +0,0 @@
import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPublicExpoConfigAsync } from '../../project/expoConfig';
type GetServerSideEnvironmentVariablesFn = (
maybeEnv?: Record<string, string>
) => Promise<Record<string, string>>;
export class ServerSideEnvironmentVariablesContextField extends ContextField<GetServerSideEnvironmentVariablesFn> {
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<GetServerSideEnvironmentVariablesFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (maybeEnv?: Record<string, string>) => {
if (!withServerSideEnvironment) {
throw new Error(
'withServerSideEnvironment parameter is required to evaluate ServerSideEnvironmentVariablesContextField'
);
}
const exp = await getPublicExpoConfigAsync(projectDir, { env: maybeEnv });
const projectId = await getProjectIdAsync(sessionManager, exp, {
nonInteractive,
env: maybeEnv,
});
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
return serverSideEnvironmentVariables;
};
}
}
@@ -6,7 +6,6 @@ import { Role } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { learnMore } from '../../../../log';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { isExpoInstalled } from '../../../../project/projectUtils';
import SessionManager from '../../../../user/SessionManager';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../getProjectIdAsync';
@@ -22,7 +21,6 @@ jest.mock('../../../../ora', () => ({
}),
}));
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../../../project/projectUtils');
describe(getProjectIdAsync, () => {
let sessionManager: SessionManager;
@@ -73,7 +71,6 @@ describe(getProjectIdAsync, () => {
);
jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
jest.mocked(isExpoInstalled).mockReturnValue(true);
});
it('gets the project ID from app config if exists', async () => {
@@ -225,7 +222,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } },
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
@@ -42,7 +42,7 @@ async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
}
const maybeRepoRoot = PackageManagerUtils.resolveWorkspaceRoot(projectDir) ?? projectDir;
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
@@ -1,4 +1,5 @@
import { ExpoConfig, getProjectConfigDescription } from '@expo/config';
import { getProjectConfigDescription } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
@@ -7,11 +8,9 @@ import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import {
createOrModifyExpoConfigAsync,
getPrivateExpoConfigAsync,
} from '../../../project/expoConfig';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfig } from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import SessionManager from '../../../user/SessionManager';
import { Actor, getActorUsername } from '../../../user/User';
@@ -19,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 = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true, ...options });
const exp = getPrivateExpoConfig(projectDir, options);
const result = await createOrModifyExpoConfigAsync(
projectDir,
{
@@ -196,6 +193,7 @@ export async function validateOrSetProjectIdAsync({
{
accountName: getAccountNameForEASProjectSync(exp, actor),
projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy),
},
{
nonInteractive: options.nonInteractive,
@@ -1,59 +0,0 @@
import { ExpoGraphqlClient } from './createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
const cachedServerSideEnvironmentVariables: Record<
EnvironmentVariableEnvironment,
Record<string, string> | null
> = {
[EnvironmentVariableEnvironment.Development]: null,
[EnvironmentVariableEnvironment.Preview]: null,
[EnvironmentVariableEnvironment.Production]: null,
};
export async function loadServerSideEnvironmentVariablesAsync({
environment,
projectId,
graphqlClient,
}: {
environment: EnvironmentVariableEnvironment;
projectId: string;
graphqlClient: ExpoGraphqlClient;
}): Promise<Record<string, string>> {
// don't load environment variables if they were already loaded while executing a command
const cachedEnvVarsForEnvironment = cachedServerSideEnvironmentVariables[environment];
if (cachedEnvVarsForEnvironment) {
return cachedEnvVarsForEnvironment;
}
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>;
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}
Log.newLine();
cachedServerSideEnvironmentVariables[environment] = serverEnvVars;
return serverEnvVars;
}
+24 -36
View File
@@ -1,6 +1,10 @@
import { Flags } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../graphql/generated';
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
// are used in tandem, it saves on unnecessary typying in commands
@@ -22,24 +26,15 @@ export const EasNonInteractiveAndJsonFlags = {
}),
};
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
};
export const EASEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
};
export const EASMultiEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
multiple: true,
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
}),
};
@@ -52,19 +47,23 @@ export const EASVariableFormatFlag = {
};
export const EASVariableVisibilityFlag = {
visibility: Flags.enum<'plaintext' | 'sensitive' | 'secret'>({
visibility: Flags.enum<EnvironmentVariableVisibility>({
description: 'Visibility of the variable',
options: ['plaintext', 'sensitive', 'secret'],
options: mapToLowercase([
EnvironmentVariableVisibility.Secret,
EnvironmentVariableVisibility.Sensitive,
EnvironmentVariableVisibility.Public,
]),
parse: upperCaseAsync,
}),
};
export type EASEnvironmentVariableScopeFlagValue = 'project' | 'account';
export const EASEnvironmentVariableScopeFlag = {
scope: Flags.enum<EASEnvironmentVariableScopeFlagValue>({
export const EASVariableScopeFlag = {
scope: Flags.enum<EnvironmentVariableScope>({
description: 'Scope for the variable',
options: ['project', 'account'],
default: 'project',
options: mapToLowercase([EnvironmentVariableScope.Shared, EnvironmentVariableScope.Project]),
parse: upperCaseAsync,
default: EnvironmentVariableScope.Project,
}),
};
@@ -79,14 +78,3 @@ export const EasJsonOnlyFlag = {
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
}),
};
export const EasUpdateEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
description:
'Environment to use for the server-side defined EAS environment variables during command execution.',
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
parse: upperCaseAsync,
required: false,
default: null,
}),
};
@@ -12,14 +12,16 @@ export default class AccountView extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.MaybeLoggedIn,
...this.ContextOptions.SessionManagment,
};
async runAsync(): Promise<void> {
const {
maybeLoggedIn: { actor, authenticationInfo },
maybeLoggedIn: { actor },
sessionManager,
} = await this.getContextAsync(AccountView, { nonInteractive: true });
if (actor) {
const loggedInAs = authenticationInfo.accessToken
const loggedInAs = sessionManager.getAccessToken()
? `${getActorDisplayName(actor)} (authenticated using EXPO_TOKEN)`
: getActorDisplayName(actor);
Log.log(chalk.green(loggedInAs));
@@ -25,7 +25,7 @@ export default class BranchCreate extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
@@ -36,7 +36,7 @@ export default class BranchCreate extends EasCommand {
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(BranchCreate);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(BranchCreate, {
@@ -58,7 +58,7 @@ export default class BranchCreate extends EasCommand {
type: 'text',
name: 'name',
message: 'Provide a branch name:',
initial: (await getDefaultBranchNameAsync(vcsClient)) ?? undefined,
initial: await getDefaultBranchNameAsync(vcsClient),
validate: value => (value ? true : validationMessage),
}));
}
@@ -79,7 +79,7 @@ export default class BranchDelete extends EasCommand {
static override description = 'delete a branch';
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -108,7 +108,7 @@ export default class BranchDelete extends EasCommand {
}
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BranchDelete, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
+2 -2
View File
@@ -13,14 +13,14 @@ export default class BranchList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BranchList);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BranchList, {
nonInteractive: flags['non-interactive'],
@@ -63,7 +63,7 @@ export default class BranchRename extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -72,7 +72,7 @@ export default class BranchRename extends EasCommand {
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
} = await this.parse(BranchRename);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BranchRename, {
nonInteractive,
+2 -2
View File
@@ -27,7 +27,7 @@ export default class BranchView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -38,7 +38,7 @@ export default class BranchView extends EasCommand {
} = await this.parse(BranchView);
const { 'non-interactive': nonInteractive } = flags;
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BranchView, {
nonInteractive,
@@ -110,7 +110,7 @@ export default class BuildCancel extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
@@ -128,7 +128,7 @@ export default class BuildCancel extends EasCommand {
}
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildCancel, {
nonInteractive,
@@ -38,7 +38,6 @@ export default class BuildConfigure extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildConfigure, {
nonInteractive: false,
withServerSideEnvironment: null,
});
Log.log(
@@ -94,7 +94,7 @@ export default class BuildDelete extends EasCommand {
}),
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
@@ -112,7 +112,7 @@ export default class BuildDelete extends EasCommand {
}
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildDelete, { nonInteractive });
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
@@ -142,7 +142,6 @@ export default class Build extends EasCommand {
vcsClient,
} = await this.getContextAsync(Build, {
nonInteractive: flags.nonInteractive,
withServerSideEnvironment: null,
});
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
@@ -79,7 +79,6 @@ export default class BuildInspect extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildInspect, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const outputDirectory = path.resolve(process.cwd(), flags.output);
@@ -7,7 +7,6 @@ import EasCommand from '../../commandUtils/EasCommand';
import { RequestedPlatform } from '../../platform';
import { enableJsonOutput } from '../../utils/json';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
import NoVcsClient from '../../vcs/clients/noVcs';
/**
* This command will be run on the EAS Build workers, when building
@@ -64,8 +63,7 @@ export default class BuildInternal extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
withServerSideEnvironment: null,
vcsClientOverride: new GitNoCommitClient(),
});
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
+2 -2
View File
@@ -72,7 +72,7 @@ export default class BuildList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
@@ -99,7 +99,7 @@ export default class BuildList extends EasCommand {
process.exit(1);
}
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildList, {
nonInteractive,
+3 -17
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 {
@@ -120,7 +116,6 @@ export default class BuildResign extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildResign, {
nonInteractive: flags.nonInteractive,
withServerSideEnvironment: null,
});
const maybeBuild = flags.maybeBuildId
@@ -145,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(
{
@@ -174,7 +161,7 @@ export default class BuildResign extends EasCommand {
user: actor,
graphqlClient,
analytics,
env,
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
});
@@ -194,7 +181,7 @@ export default class BuildResign extends EasCommand {
projectDir,
exp,
xcodeBuildContext,
env,
env: buildProfile.env,
vcsClient,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
@@ -261,7 +248,6 @@ export default class BuildResign extends EasCommand {
targetProfile: flags['target-profile'],
maybeBuildId: flags.id,
wait: flags.wait,
environment: flags.environment,
};
}
+3 -2
View File
@@ -79,7 +79,8 @@ export default class Run extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
};
@@ -89,7 +90,7 @@ export default class Run extends EasCommand {
const queryOptions = getPaginatedQueryOptions(flags);
const {
loggedIn: { graphqlClient },
projectId,
privateProjectConfig: { projectId },
} = await this.getContextAsync(Run, {
nonInteractive: false,
});
@@ -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';
@@ -54,7 +53,6 @@ export default class BuildVersionGetView extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildVersionGetView, {
nonInteractive: true,
withServerSideEnvironment: null,
});
if (!flags.platform && flags['non-interactive']) {
@@ -76,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);
@@ -95,7 +89,6 @@ export default class BuildVersionGetView extends EasCommand {
platform,
vcsClient,
nonInteractive: flags['non-interactive'],
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -4,8 +4,6 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import { getVersionConfigTarget } from '../../../build/utils/version';
import EasCommand from '../../../commandUtils/EasCommand';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -55,7 +53,6 @@ export default class BuildVersionSetView extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildVersionSetView, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const platform = await selectPlatformAsync(flags.platform);
@@ -67,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);
@@ -87,7 +78,6 @@ export default class BuildVersionSetView extends EasCommand {
platform,
vcsClient,
nonInteractive: false,
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -112,7 +102,6 @@ export default class BuildVersionSetView extends EasCommand {
: `What version would you like to initialize it with?`;
Log.log(currentStateMessage);
const { versionGetter } = getVersionConfigTarget({ exp, platform });
const { version } = await promptAsync({
type: platform === Platform.ANDROID ? 'number' : 'text',
name: 'version',
@@ -126,7 +115,7 @@ export default class BuildVersionSetView extends EasCommand {
appId: projectId,
platform: toAppPlatform(platform),
applicationIdentifier,
storeVersion: versionGetter(exp) ?? '1.0.0',
storeVersion: exp.version ?? '1.0.0',
buildVersion: String(version),
runtimeVersion:
(await getRuntimeVersionNullableAsync(projectDir, exp, platform)) ?? undefined,
@@ -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';
@@ -73,7 +72,6 @@ export default class BuildVersionSyncView extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildVersionSyncView, {
nonInteractive: true,
withServerSideEnvironment: null,
});
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
@@ -89,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);
@@ -109,7 +103,6 @@ export default class BuildVersionSyncView extends EasCommand {
platform: profileInfo.platform,
vcsClient,
nonInteractive: false,
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
+2 -2
View File
@@ -18,7 +18,7 @@ export default class BuildView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
@@ -29,7 +29,7 @@ export default class BuildView extends EasCommand {
flags,
} = await this.parse(BuildView);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildView, {
nonInteractive: true,
@@ -23,7 +23,7 @@ export default class ChannelCreate extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -33,7 +33,7 @@ export default class ChannelCreate extends EasCommand {
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelCreate);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelCreate, {
nonInteractive,
@@ -31,7 +31,7 @@ export default class ChannelDelete extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -41,7 +41,7 @@ export default class ChannelDelete extends EasCommand {
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelDelete);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelDelete, {
nonInteractive,
@@ -74,7 +74,7 @@ export default class ChannelEdit extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -84,7 +84,7 @@ export default class ChannelEdit extends EasCommand {
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
} = await this.parse(ChannelEdit);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelEdit, {
nonInteractive,
@@ -18,7 +18,7 @@ export default class ChannelList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -27,7 +27,7 @@ export default class ChannelList extends EasCommand {
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelList, {
nonInteractive,
@@ -1,113 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
PauseUpdateChannelMutation,
PauseUpdateChannelMutationVariables,
UpdateChannelBasicInfoFragment,
} from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../../graphql/types/UpdateChannelBasicInfo';
import Log from '../../log';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function pauseUpdateChannelAsync(
graphqlClient: ExpoGraphqlClient,
{ channelId }: PauseUpdateChannelMutationVariables
): Promise<UpdateChannelBasicInfoFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<PauseUpdateChannelMutation, PauseUpdateChannelMutationVariables>(
gql`
mutation PauseUpdateChannel($channelId: ID!) {
updateChannel {
pauseUpdateChannel(channelId: $channelId) {
id
...UpdateChannelBasicInfoFragment
}
}
}
${print(UpdateChannelBasicInfoFragmentNode)}
`,
{ channelId }
)
.toPromise()
);
const channel = data.updateChannel.pauseUpdateChannel;
if (!channel) {
throw new Error(`Could not find a channel with id: ${channelId}`);
}
return channel;
}
export default class ChannelPause extends EasCommand {
static override description = 'pause a channel to stop it from sending updates';
static override args = [
{
name: 'name',
required: false,
description: 'Name of the channel to edit',
},
];
static override flags = {
branch: Flags.string({
description: 'Name of the branch to point to',
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const {
args,
flags: { json, 'non-interactive': nonInteractive },
} = await this.parse(ChannelPause);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelPause, {
nonInteractive,
});
if (json) {
enableJsonOutput();
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
: await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to edit',
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
});
const channel = await pauseUpdateChannelAsync(graphqlClient, {
channelId: existingChannel.id,
});
if (json) {
printJsonOnlyOutput(channel);
} else {
Log.withTick(chalk`Channel {bold ${channel.name}} is now paused.\n`);
Log.addNewLineIfNone();
Log.log(
chalk`Users with builds on channel {bold ${channel.name}} will no longer receive updates.`
);
}
}
}
@@ -1,111 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
ResumeUpdateChannelMutation,
ResumeUpdateChannelMutationVariables,
UpdateChannelBasicInfoFragment,
} from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../../graphql/types/UpdateChannelBasicInfo';
import Log from '../../log';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function resumeUpdateChannelAsync(
graphqlClient: ExpoGraphqlClient,
{ channelId }: ResumeUpdateChannelMutationVariables
): Promise<UpdateChannelBasicInfoFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<ResumeUpdateChannelMutation, ResumeUpdateChannelMutationVariables>(
gql`
mutation ResumeUpdateChannel($channelId: ID!) {
updateChannel {
resumeUpdateChannel(channelId: $channelId) {
id
...UpdateChannelBasicInfoFragment
}
}
}
${print(UpdateChannelBasicInfoFragmentNode)}
`,
{ channelId }
)
.toPromise()
);
const channel = data.updateChannel.resumeUpdateChannel;
if (!channel) {
throw new Error(`Could not find a channel with id: ${channelId}`);
}
return channel;
}
export default class ChannelResume extends EasCommand {
static override description = 'resume a channel to start sending updates';
static override args = [
{
name: 'name',
required: false,
description: 'Name of the channel to edit',
},
];
static override flags = {
branch: Flags.string({
description: 'Name of the branch to point to',
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const {
args,
flags: { json, 'non-interactive': nonInteractive },
} = await this.parse(ChannelResume);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelResume, {
nonInteractive,
});
if (json) {
enableJsonOutput();
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
: await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to edit',
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
});
const channel = await resumeUpdateChannelAsync(graphqlClient, {
channelId: existingChannel.id,
});
if (json) {
printJsonOnlyOutput(channel);
} else {
Log.withTick(chalk`Channel {bold ${channel.name}} is now active.\n`);
Log.addNewLineIfNone();
Log.log(chalk`Users with builds on channel {bold ${channel.name}} will now receive updates.`);
}
}
}
@@ -138,7 +138,6 @@ export default class ChannelRollout extends EasCommand {
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelRollout, {
nonInteractive: argsAndFlags.nonInteractive,
withServerSideEnvironment: null,
});
if (argsAndFlags.json) {
enableJsonOutput();
@@ -26,7 +26,7 @@ export default class ChannelView extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -38,7 +38,7 @@ export default class ChannelView extends EasCommand {
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelView, {
nonInteractive,
+6 -19
View File
@@ -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 { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import { toAppPlatform } from '../graphql/types/AppPlatform';
@@ -34,7 +33,6 @@ export default class Config extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.DynamicLoggedIn,
};
async runAsync(): Promise<void> {
@@ -42,16 +40,10 @@ export default class Config extends EasCommand {
if (flags.json) {
enableJsonOutput();
}
const {
platform: maybePlatform,
profile: maybeProfile,
'non-interactive': nonInteractive,
} = flags;
const { getDynamicPublicProjectConfigAsync, projectDir, getDynamicLoggedInAsync } =
await this.getContextAsync(Config, {
nonInteractive,
withServerSideEnvironment: null,
});
const { platform: maybePlatform, profile: maybeProfile } = flags;
const { getDynamicPublicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
nonInteractive: false,
});
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
const profileName =
@@ -88,13 +80,8 @@ export default class Config extends EasCommand {
Log.log(JSON.stringify(profile, null, 2));
}
} else {
const { graphqlClient } = await getDynamicLoggedInAsync();
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) {
@@ -39,7 +39,6 @@ export default class InitializeBuildCredentials extends EasCommand {
vcsClient,
} = await this.getContextAsync(InitializeBuildCredentials, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const platform = await selectPlatformAsync(flags.platform);
@@ -22,13 +22,12 @@ export default class Credentials extends EasCommand {
const { flags } = await this.parse(Credentials);
const {
loggedIn: { actor, graphqlClient },
optionalPrivateProjectConfig: privateProjectConfig,
privateProjectConfig,
getDynamicPrivateProjectConfigAsync,
analytics,
vcsClient,
} = await this.getContextAsync(Credentials, {
nonInteractive: false,
withServerSideEnvironment: null,
});
await new SelectPlatform(
@@ -15,10 +15,9 @@ export default class DeviceCreate extends EasCommand {
// this command is interactive by design
const {
loggedIn: { actor, graphqlClient },
optionalPrivateProjectConfig: privateProjectConfig,
privateProjectConfig,
} = await this.getContextAsync(DeviceCreate, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const ctx = await createContextAsync({
@@ -34,7 +34,7 @@ export default class DeviceDelete extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -43,7 +43,7 @@ export default class DeviceDelete extends EasCommand {
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
let { 'apple-team-id': appleTeamIdentifier, udid } = flags;
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(DeviceDelete, {
nonInteractive: paginatedQueryOptions.nonInteractive,
+2 -2
View File
@@ -21,7 +21,7 @@ export default class BuildList extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -29,7 +29,7 @@ export default class BuildList extends EasCommand {
const { flags } = await this.parse(BuildList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildList, {
nonInteractive: paginatedQueryOptions.nonInteractive,
@@ -35,7 +35,7 @@ export default class DeviceRename extends EasCommand {
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -44,7 +44,7 @@ export default class DeviceRename extends EasCommand {
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
let { 'apple-team-id': appleTeamIdentifier, udid, name } = flags;
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(DeviceRename, {
nonInteractive: paginatedQueryOptions.nonInteractive,
+2 -2
View File
@@ -11,7 +11,7 @@ export default class DeviceView extends EasCommand {
static override args = [{ name: 'UDID' }];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
@@ -32,7 +32,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
throw new Error('Device UDID is missing');
}
const {
projectId,
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(DeviceView, {
nonInteractive: true,
@@ -1,385 +0,0 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import {
parseVisibility,
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableTypeAsync,
promptVariableValueAsync,
} from '../../../utils/prompts';
import EnvCreate from '../create';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvCreate, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const variableId = 'testId';
const testAccountId = 'test-account-id';
beforeEach(() => {
jest.resetAllMocks();
jest
.mocked(parseVisibility)
.mockImplementation(jest.requireActual('../../../utils/prompts').parseVisibility);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(EnvironmentVariableMutation.createForAppAsync)
.mockImplementation(async (_client, input, _appId) => ({
...input,
id: variableId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
}));
jest
.mocked(EnvironmentVariableMutation.createSharedVariableAsync)
.mockImplementation(async (_client, input, _appId) => ({
...input,
id: variableId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
}));
jest
.mocked(EnvironmentVariableMutation.updateAsync)
.mockImplementation(async (_client, input) => ({
...input,
id: variableId,
name: 'VarName',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
type: EnvironmentSecretType.String,
}));
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockImplementation(async () => []);
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockImplementation(async () => []);
});
describe('in interactive mode', () => {
it('creates a project variable', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--visibility',
'secret',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Secret,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
it('updates an existing variable in the same environment', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--visibility',
'secret',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
const otherVariableId = 'otherId';
jest
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Project,
},
]);
// @ts-expect-error
jest.spyOn(command, 'promptForOverwriteAsync').mockResolvedValue(true);
jest
.mocked(EnvironmentVariableMutation.updateAsync)
// @ts-expect-error
.mockImplementation(async (_client, input) => ({
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
}));
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
filterNames: ['VarName'],
});
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Secret,
});
});
it('creates an account-wide variable', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--scope',
'account',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
isGlobal: true,
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
'test-account-id'
);
});
it('throws if an account-wide variable already exists', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--scope',
'account',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest
.mocked(EnvironmentVariablesQuery.sharedAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: 'otherId',
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
await expect(command.runAsync()).rejects.toThrow();
});
it('updates if an account-wide variable already exists and --force flag is set', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--force',
'--scope',
'account',
],
mockConfig
);
const otherVariableId = 'otherId';
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest
.mocked(EnvironmentVariablesQuery.sharedAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
it('creates an account-wide variable and links it', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--environment',
'development',
'--scope',
'account',
'--link',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [
EnvironmentVariableEnvironment.Production,
EnvironmentVariableEnvironment.Development,
],
visibility: EnvironmentVariableVisibility.Public,
isGlobal: true,
type: EnvironmentSecretType.String,
},
testAccountId
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Development
);
});
it('prompts for missing arguments', async () => {
const command = new EnvCreate([], mockConfig);
jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
jest
.mocked(promptVariableTypeAsync)
.mockImplementation(async () => EnvironmentSecretType.String);
jest
.mocked(promptVariableEnvironmentAsync)
// @ts-expect-error
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(promptVariableNameAsync).toHaveBeenCalled();
expect(promptVariableValueAsync).toHaveBeenCalled();
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
});
});
@@ -1,124 +0,0 @@
import { Config } from '@oclif/core';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import EnvDelete from '../delete';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../log');
describe(EnvDelete, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
beforeEach(() => {
jest.resetAllMocks();
});
it('deletes a variable by name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvDelete(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'production',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
});
it('prompts for variable selection when name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvDelete([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
});
it('throws an error when variable name is not found', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvDelete(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow('Variable "NON_EXISTENT_VARIABLE" not found.');
});
it('throws an error when multiple variables with the same name are found', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
{ id: '2', name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
'Multiple variables with name "TEST_VARIABLE" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.'
);
});
it('cancels deletion when user does not confirm', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(false);
const command = new EnvDelete(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrowErrorMatchingSnapshot();
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
});
});
@@ -1,114 +0,0 @@
import { Config } from '@oclif/core';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../../utils/prompts';
import EnvGet from '../get';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvGet, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const mockVariables: EnvironmentVariableFragment[] = [
{
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
];
it('retrieves environment variables successfully', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: undefined,
filterNames: ['TEST_VAR_1'],
includeFileContent: true,
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
});
it('handles errors during retrieval', async () => {
const errorMessage =
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag";
const command = new EnvGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await expect(command.runAsync()).rejects.toThrow(errorMessage);
});
it('prompts for variable name and environment if the name is ambigous', async () => {
jest
.mocked(promptVariableEnvironmentAsync)
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
const command = new EnvGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).mockResolvedValueOnce([
...mockVariables,
{
id: 'var2',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
]);
await command.runAsync();
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(promptVariableNameAsync).toHaveBeenCalled();
});
});
@@ -1,271 +0,0 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvLink from '../link';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvLink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
projectId,
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Linked variable ${chalk.bold('TEST_VARIABLE')} to project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('links an account-wide variable to the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvLink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('links an account-wide variable to the current project to a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvLink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('uses --variable-environment to select the variable with ambigous name', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockImplementation(
(_client, { environment }) => {
return mockVariables.filter(v => v.environments.includes(environment));
}
);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvLink(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'other-id',
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -1,167 +0,0 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import EnvList from '../list';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
const mockVariables: EnvironmentVariableFragment[] = [
{
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
type: EnvironmentSecretType.String,
},
{
id: 'var2',
name: 'TEST_VAR_2',
value: 'value2',
environments: [EnvironmentVariableEnvironment.Development],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
type: EnvironmentSecretType.String,
},
];
describe(EnvList, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('lists project environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvList([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists project environment variables in specified environments', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvList(['--environment', 'production'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists project environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvList(['--include-sensitive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: undefined,
includeFileContent: false,
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists account-wide environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvList(['--scope', 'account'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists account-wide environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvList(['--include-sensitive', '--scope', 'account'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
});
@@ -1,226 +0,0 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvUnlink from '../unlink';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvUnlink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
projectId,
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Unlinked variable ${chalk.bold('TEST_VARIABLE')} from project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('unlinks an account-wide variable from the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvUnlink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('unlinks an account-wide variable from the current project in a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvUnlink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff. Show more