mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
135
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7dbe4ee71 | ||
|
|
34ecd38304 | ||
|
|
c4adf7056e | ||
|
|
0e7f326de2 | ||
|
|
bbc8b1ac76 | ||
|
|
3db2a69183 | ||
|
|
51155bb2b4 | ||
|
|
30ec5f967b | ||
|
|
89dda20149 | ||
|
|
b591698c31 | ||
|
|
66543b658b | ||
|
|
4769048578 | ||
|
|
64c1e47674 | ||
|
|
be3a86abda | ||
|
|
eb2ca117fc | ||
|
|
5043385077 | ||
|
|
bb4c08a52a | ||
|
|
8b75057fa1 | ||
|
|
80cc849137 | ||
|
|
2d407ea9d4 | ||
|
|
91456d1c65 | ||
|
|
757a0ccad7 | ||
|
|
4b7cac0392 | ||
|
|
8ff8022b3f | ||
|
|
a2acea9fe9 | ||
|
|
9abdfcfdbe | ||
|
|
696f9dde13 | ||
|
|
1e73c8c96f | ||
|
|
1b02a67d88 | ||
|
|
d0f158a546 | ||
|
|
16a2dc681b | ||
|
|
73800252ae | ||
|
|
e7c4ba1f54 | ||
|
|
84cddaf0e1 | ||
|
|
896f7f0385 | ||
|
|
7f31ea6a2d | ||
|
|
3ef397f96c | ||
|
|
680a684a8e | ||
|
|
1eae0c75ee | ||
|
|
483e0f2dca | ||
|
|
6ee843e41c | ||
|
|
5402ebdc5c | ||
|
|
1b5119d818 | ||
|
|
8a82365680 | ||
|
|
9c55621719 | ||
|
|
1afc3093ec | ||
|
|
790c3d776e | ||
|
|
be84ffd62a | ||
|
|
fa812f448f | ||
|
|
d09161b745 | ||
|
|
653f26257b | ||
|
|
b1e874966c | ||
|
|
315abf0a03 | ||
|
|
89a9255929 | ||
|
|
cae0ba21eb | ||
|
|
ed52e291cb | ||
|
|
6db7ac617b | ||
|
|
1dd9614dbe | ||
|
|
67c58a75d9 | ||
|
|
5b3e4cb3e1 | ||
|
|
caf257ce9c | ||
|
|
31122623fa | ||
|
|
eb36dd0fef | ||
|
|
bb0723354f | ||
|
|
1b9feed040 | ||
|
|
0220ca17a6 | ||
|
|
74cec12c23 | ||
|
|
e807fc105d | ||
|
|
ae30c4c7db | ||
|
|
35b9fda122 | ||
|
|
8d010ad7e8 | ||
|
|
d7181478c8 | ||
|
|
70ed1a482a | ||
|
|
920e5ee8a2 | ||
|
|
f334f2c019 | ||
|
|
b145a7d98c | ||
|
|
3c750b60c0 | ||
|
|
b3a9f1af70 | ||
|
|
39d8ab65d1 | ||
|
|
1ff2274d8d | ||
|
|
5ed4b65133 | ||
|
|
10aca5208f | ||
|
|
b2da80d5ea | ||
|
|
b9457d71f9 | ||
|
|
5fd3149a95 | ||
|
|
5f83724833 | ||
|
|
7df558d713 | ||
|
|
543e8ce26d | ||
|
|
355fada383 | ||
|
|
b6625a4cba | ||
|
|
c1ef20afab | ||
|
|
d8321e1f06 | ||
|
|
d8906e085e | ||
|
|
3b823865d3 | ||
|
|
a29ed19653 | ||
|
|
e70ecaa9b5 | ||
|
|
9886fcb3ef | ||
|
|
4f5739b2dc | ||
|
|
c7a40b06a5 | ||
|
|
3f88dc8690 | ||
|
|
874db1c125 | ||
|
|
91a147b40b | ||
|
|
cc4553b318 | ||
|
|
dd2303dbb2 | ||
|
|
967b5d9985 | ||
|
|
8439cd2141 | ||
|
|
f46063cca5 | ||
|
|
c0cfa93b69 | ||
|
|
7d6f620f91 | ||
|
|
c973a1f603 | ||
|
|
1b8f60fad4 | ||
|
|
7a30de9ad7 | ||
|
|
4247b65e09 | ||
|
|
8c388d5173 | ||
|
|
fce945679f | ||
|
|
7772e199b4 | ||
|
|
755e105252 | ||
|
|
611d0c98ee | ||
|
|
949a90187b | ||
|
|
bb14d9255a | ||
|
|
98e35926ff | ||
|
|
cd433405cb | ||
|
|
3ad54b0fc9 | ||
|
|
5184117ed6 | ||
|
|
868df87146 | ||
|
|
2f0cc2361b | ||
|
|
4be6c51418 | ||
|
|
99c4c618c9 | ||
|
|
742a1a05e5 | ||
|
|
6f806cabb1 | ||
|
|
4c84794a31 | ||
|
|
a1ab5a4fb5 | ||
|
|
dc11c1c487 | ||
|
|
805865356c | ||
|
|
5fc0757fb0 |
No files matched your search
@@ -26,10 +26,6 @@ module.exports = {
|
||||
name: 'lodash',
|
||||
message: "Don't use lodash, it's heavy!",
|
||||
},
|
||||
{
|
||||
name: 'node-fetch',
|
||||
message: 'Use got instead, node-fetch is used only for the GraphQL client.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "\U0001F41B Bug Report in EAS CLI"
|
||||
labels: 'bug'
|
||||
labels: 'needs review'
|
||||
description: 'You want to report a reproducible bug or regression in EAS CLI.'
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -14,6 +14,11 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Build/Submit details page URL
|
||||
description: If you're reporting an issue with EAS Build and/or EAS Submit, please provide the URL to the details page URL. The page can be only viewed by the Expo team members. Providing the URL is optional but it makes debugging the issue easier.
|
||||
placeholder: https://expo.dev/accounts/ACCOUNT_NAME/projects/PROJECT_NAME/builds/BUILD_ID
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
@@ -21,19 +26,18 @@ body:
|
||||
label: Summary
|
||||
description: Describe the issue in 1 or 2 sentences
|
||||
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
|
||||
- type: textarea
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Managed or bare?
|
||||
description: See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
|
||||
placeholder: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project.
|
||||
description: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project. See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
placeholder: Run `eas diagnostics` and paste the output here
|
||||
description: Run `npx expo-env-info` and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement'
|
||||
labels: 'enhancement,needs review'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
@@ -15,4 +15,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: bash bin/install.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: eas --version
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
ascending: false
|
||||
operations-per-run: 300
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
enable-statistics: true
|
||||
exempt-issue-labels: "needs review"
|
||||
exempt-all-assignees: true
|
||||
+193
@@ -10,6 +10,199 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Update dependencies. ([#1095](https://github.com/expo/eas-cli/pull/1095) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support target specific entitlements. ([#1078](https://github.com/expo/eas-cli/pull/1078) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.51.0](https://github.com/expo/eas-cli/releases/tag/v0.51.0) - 2022-04-19
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add non-interactive flag to eas update command. ([#1066](https://github.com/expo/eas-cli/pull/1066) by [@wschurman](https://github.com/wschurman))
|
||||
- Handle new EAS Submit common error - expired Apple's certificates. ([#1068](https://github.com/expo/eas-cli/pull/1068) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Enable full bundling logging for eas update when debugging. ([#1070](https://github.com/expo/eas-cli/pull/1070) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `@expo/[email protected]` (now with license). ([#1064](https://github.com/expo/eas-cli/pull/1064) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Unify Google Service Account key prompts. ([#1063](https://github.com/expo/eas-cli/pull/1063) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.50.0](https://github.com/expo/eas-cli/releases/tag/v0.50.0) - 2022-04-11
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Provide queue position progress and estimated wait time. ([#1049](https://github.com/expo/eas-cli/pull/1049), [#1058](https://github.com/expo/eas-cli/pull/1058) by [@dsokal](https://github.com/dsokal))
|
||||
- Enable App Store authentication for ASC Api Keys with environment variables. ([#1051](https://github.com/expo/eas-cli/pull/1051) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Prompt for user/password authentication where required. ([#1055](https://github.com/expo/eas-cli/pull/1055) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Require private-key-path to be specified for updating when code signing configured. ([#1059](https://github.com/expo/eas-cli/pull/1059) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [0.49.0](https://github.com/expo/eas-cli/releases/tag/v0.49.0) - 2022-04-05
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added experimental App Store Connect API provisioning profile regeneration. ([#1038](https://github.com/expo/eas-cli/pull/1038) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Experimental support for iOS App Extensions in managed projects. ([#1039](https://github.com/expo/eas-cli/pull/1039) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix proxy support. ([#1032](https://github.com/expo/eas-cli/pull/1032) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add annotations regarding App Store Connect Token session support. ([#1029](https://github.com/expo/eas-cli/pull/1029) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Upgrade dependencies. ([#1043](https://github.com/expo/eas-cli/pull/1043) by [@dsokal](https://github.com/dsokal))
|
||||
- Make default deprecation message more generic. ([#1047](https://github.com/expo/eas-cli/pull/1047) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.48.2](https://github.com/expo/eas-cli/releases/tag/v0.48.2) - 2022-03-21
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add proxy support. ([#1005](https://github.com/expo/eas-cli/pull/1005) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Do not retry build requests. ([#1024](https://github.com/expo/eas-cli/pull/1024) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.48.1](https://github.com/expo/eas-cli/releases/tag/v0.48.1) - 2022-03-15
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix code signing error when not yet configured. ([#1018](https://github.com/expo/eas-cli/pull/1018) by [@wschurman](https://github.com/wschurman))
|
||||
- Fix link to build details page to use `project.slug`. ([#1021](https://github.com/expo/eas-cli/pull/1021) by [@giautm](https://github.com/giautm))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Improve apple login prompt for internal distribution builds. ([#1016](https://github.com/expo/eas-cli/pull/1016) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.48.0](https://github.com/expo/eas-cli/releases/tag/v0.48.0) - 2022-03-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add code signing. ([#964](https://github.com/expo/eas-cli/pull/964) by [@wschurman](https://github.com/wschurman))
|
||||
- Install expo-updates when running update:configure. ([#977](https://github.com/expo/eas-cli/pull/977) by [@jkhales](https://github.com/jkhales))
|
||||
- Make `update:configure` work on native files. ([#978](https://github.com/expo/eas-cli/pull/978) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `"Unknown status"` error when a build is canceled. ([#1012](https://github.com/expo/eas-cli/pull/1012) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add error message when package.json is outside git repository. ([#971](https://github.com/expo/eas-cli/pull/971) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Make runtime version policy warning on the update command more descriptive. ([#979](https://github.com/expo/eas-cli/pull/979) by [@kbrandwijk](https://github.com/kbrandwijk))
|
||||
- Remove unused flag. ([#995](https://github.com/expo/eas-cli/pull/995) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Replace `got` with `node-fetch`. ([#1000](https://github.com/expo/eas-cli/pull/1000) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Upgrade dependencies. ([#1015](https://github.com/expo/eas-cli/pull/1015) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.47.0](https://github.com/expo/eas-cli/releases/tag/v0.47.0) - 2022-02-08
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Return informative error when running `eas channel:rollout` with a channel that does not exist. ([#930](https://github.com/expo/eas-cli/pull/930) by [@jkhales](https://github.com/jkhales))
|
||||
- Check if `expo-updates` is installed before publish. ([#953](https://github.com/expo/eas-cli/pull/953) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade dependencies. ([#946](https://github.com/expo/eas-cli/pull/946) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.46.0](https://github.com/expo/eas-cli/releases/tag/v0.46.0) - 2022-01-26
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for building iOS builds on macOS Catalina. ([#939](https://github.com/expo/eas-cli/pull/939) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix URL placeholder for iOS submits. ([#941](https://github.com/expo/eas-cli/pull/941) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Unify EAS command descriptions style. ([#925](https://github.com/expo/eas-cli/pull/925) by [@dsokal](https://github.com/dsokal))
|
||||
- Upgrade dependencies. ([#939](https://github.com/expo/eas-cli/pull/939) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.45.1](https://github.com/expo/eas-cli/releases/tag/v0.45.1) - 2022-01-19
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add json output for started builds when using `--no-wait`. ([#921](https://github.com/expo/eas-cli/pull/921) by [@kbrandwijk](https://github.com/kbrandwijk))
|
||||
- Add support to `eas build` for specifying a custom `prebuildCommand` in `eas.json`. ([#919](https://github.com/expo/eas-cli/pull/919) by [@kbrandwijk](https://github.com/kbrandwijk))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add macOS Monterey image. ([#922](https://github.com/expo/eas-cli/pull/922) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.45.0](https://github.com/expo/eas-cli/releases/tag/v0.45.0) - 2022-01-18
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas device:delete`. ([#890](https://github.com/expo/eas-cli/pull/890) by [@kbrandwijk](https://github.com/kbrandwijk))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade dependencies. ([#872](https://github.com/expo/eas-cli/pull/872) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.44.1](https://github.com/expo/eas-cli/releases/tag/v0.44.1) - 2022-01-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix build:configure expo-updates configuration to only run on generic projects. ([#904](https://github.com/expo/eas-cli/pull/904) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
|
||||
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- No longer enable APNS (iOS Push Notifications) capability by default anymore. ([#797](https://github.com/expo/eas-cli/pull/797) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Don't configure Android projects locally. Clean up old Gradle signing config. Streamline `expo-updates` configuration. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `update:list` command. ([#884](https://github.com/expo/eas-cli/pull/884) by [@jkhales](https://github.com/jkhales))
|
||||
- Improve error message for outdated Apple PLA error. ([#889](https://github.com/expo/eas-cli/pull/889) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `--output` flag to the build command. ([#885](https://github.com/expo/eas-cli/pull/885) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix creating project archive with symlink cycle. ([#891](https://github.com/expo/eas-cli/pull/891) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Sign Android debug builds with the correct keystore. Previously, all debug builds would be signed with a default debug keystore. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.43.0](https://github.com/expo/eas-cli/releases/tag/v0.43.0) - 2022-01-03
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas channel:delete`. ([#846](https://github.com/expo/eas-cli/pull/846), [#869](https://github.com/expo/eas-cli/pull/869) by [@jkhales](https://github.com/jkhales))
|
||||
- Add `eas autocomplete`. ([#870](https://github.com/expo/eas-cli/pull/870) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix saving `build:inspect` results to project subdirectory. ([#863](https://github.com/expo/eas-cli/pull/863) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.42.4](https://github.com/expo/eas-cli/releases/tag/v0.42.4) - 2021-12-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Add missing dependency - `@oclif/plugin-help`. ([#865](https://github.com/expo/eas-cli/pull/865) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.42.3](https://github.com/expo/eas-cli/releases/tag/v0.42.3) - 2021-12-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Allow select eas commands to be run outside of a project. ([#851](https://github.com/expo/eas-cli/pull/851) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
+4
-5
@@ -11,7 +11,7 @@
|
||||
|
||||
prefix="${EAS_PREFIX:-/usr/local}"
|
||||
temp_dir="$(mktemp -d)"
|
||||
|
||||
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
|
||||
then
|
||||
# Use colors when stdout is a terminal and NO_COLOR isn't set.
|
||||
@@ -67,7 +67,7 @@
|
||||
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
|
||||
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
|
||||
fi
|
||||
|
||||
|
||||
hardware="$(uname -m)"
|
||||
case "$hardware" in
|
||||
x86_64)
|
||||
@@ -95,13 +95,12 @@
|
||||
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
|
||||
;;
|
||||
esac
|
||||
|
||||
url="https://github.com/expo/eas-cli/releases/latest/download/eas-$platform-$arch.tar.gz"
|
||||
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
|
||||
echo "Installing EAS CLI from $url"
|
||||
|
||||
mkdir -p "$temp_dir"
|
||||
cd "$temp_dir"
|
||||
|
||||
|
||||
echo
|
||||
echo "Downloading..."
|
||||
if [ "$(command -v curl)" ]
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.42.3"
|
||||
"version": "0.52.0"
|
||||
}
|
||||
+10
-10
@@ -17,19 +17,19 @@
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/node": "16.7.8",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-universe": "8.0.0",
|
||||
"@types/jest": "27.5.0",
|
||||
"@types/node": "17.0.32",
|
||||
"eslint": "8.15.0",
|
||||
"eslint-config-universe": "11.0.0",
|
||||
"eslint-plugin-async-protect": "1.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "27.1.0",
|
||||
"jest-watch-typeahead": "0.6.4",
|
||||
"jest": "28.1.0",
|
||||
"jest-watch-typeahead": "1.1.0",
|
||||
"lerna": "4.0.0",
|
||||
"oclif": "2.0.1",
|
||||
"prettier": "2.4.1",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.5.2"
|
||||
"oclif": "3.0.1",
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "28.0.2",
|
||||
"typescript": "4.6.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.10.0",
|
||||
|
||||
+282
-118
@@ -37,6 +37,7 @@ eas --help COMMAND
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
|
||||
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
|
||||
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
|
||||
* [`eas branch:list`](#eas-branchlist)
|
||||
@@ -47,6 +48,7 @@ eas --help COMMAND
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:inspect`](#eas-buildinspect)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:submit`](#eas-buildsubmit)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
@@ -55,10 +57,14 @@ eas --help COMMAND
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:delete`](#eas-devicedelete)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas diagnostics`](#eas-diagnostics)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas init`](#eas-init)
|
||||
* [`eas login`](#eas-login)
|
||||
* [`eas logout`](#eas-logout)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
@@ -68,12 +74,14 @@ eas --help COMMAND
|
||||
* [`eas update`](#eas-update)
|
||||
* [`eas update:configure`](#eas-updateconfigure)
|
||||
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
* [`eas update:list`](#eas-updatelist)
|
||||
* [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
* [`eas webhook:update`](#eas-webhookupdate)
|
||||
* [`eas webhook:view ID`](#eas-webhookview-id)
|
||||
* [`eas whoami`](#eas-whoami)
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
@@ -90,7 +98,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -107,7 +115,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -124,25 +132,54 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
view or change analytics settings
|
||||
display or change analytics settings
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas analytics [STATUS]
|
||||
|
||||
DESCRIPTION
|
||||
view or change analytics settings
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
display autocomplete installation instructions
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas autocomplete [SHELL] [-r]
|
||||
|
||||
ARGUMENTS
|
||||
SHELL shell type
|
||||
|
||||
FLAGS
|
||||
-r, --refresh-cache Refresh cache (ignores displaying instructions)
|
||||
|
||||
DESCRIPTION
|
||||
display autocomplete installation instructions
|
||||
|
||||
EXAMPLES
|
||||
$ eas autocomplete
|
||||
|
||||
$ eas autocomplete bash
|
||||
|
||||
$ eas autocomplete zsh
|
||||
|
||||
$ eas autocomplete --refresh-cache
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.2.0/src/commands/autocomplete/index.ts)_
|
||||
|
||||
## `eas branch:create [NAME]`
|
||||
|
||||
Create a branch on the current project.
|
||||
create a branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -155,14 +192,14 @@ FLAGS
|
||||
--json return a json with the new branch ID and name.
|
||||
|
||||
DESCRIPTION
|
||||
Create a branch on the current project.
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
Delete a branch on the current project
|
||||
delete a branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -175,14 +212,14 @@ FLAGS
|
||||
--json return JSON with the edited branch's ID and name.
|
||||
|
||||
DESCRIPTION
|
||||
Delete a branch on the current project
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
List all branches on this project.
|
||||
list all branches
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -192,14 +229,14 @@ FLAGS
|
||||
--json return output as JSON
|
||||
|
||||
DESCRIPTION
|
||||
List all branches on this project.
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
Rename a branch.
|
||||
rename a branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -211,14 +248,14 @@ FLAGS
|
||||
--to=<value> new name of the branch.
|
||||
|
||||
DESCRIPTION
|
||||
Rename a branch.
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
View a branch.
|
||||
view a branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -231,19 +268,19 @@ FLAGS
|
||||
--json return a json with the branch's ID name and recent update groups.
|
||||
|
||||
DESCRIPTION
|
||||
View a branch.
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
Start a build
|
||||
start a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build [-p android|ios|all] [--json] [--skip-project-configuration] [--profile <value>]
|
||||
[--non-interactive] [--local] [--wait] [--clear-cache] [--auto-submit | --auto-submit-with-profile <value>]
|
||||
$ eas build [-p android|ios|all] [--json] [--profile <value>] [--non-interactive] [--local] [--output
|
||||
<value>] [--wait] [--clear-cache] [--auto-submit | --auto-submit-with-profile <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
@@ -254,34 +291,34 @@ FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--output=<value> Output path for local build
|
||||
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
|
||||
in eas.json.
|
||||
--skip-project-configuration Skip project configuration
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
|
||||
DESCRIPTION
|
||||
Start a build
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
Cancel a build.
|
||||
cancel a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
|
||||
DESCRIPTION
|
||||
Cancel a build.
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
Configure the project to support EAS Build.
|
||||
configure the project to support EAS Build
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -291,21 +328,23 @@ FLAGS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
|
||||
DESCRIPTION
|
||||
Configure the project to support EAS Build.
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
Inspect the state of the project at specific build stages. Useful for troubleshooting.
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:inspect -p android|ios -s archive|pre-build|post-build --output <value> [--profile <value>] [--force]
|
||||
[--verbose]
|
||||
$ eas build:inspect -p android|ios -s archive|pre-build|post-build -o <value> [--profile <value>] [--force] [-v]
|
||||
|
||||
FLAGS
|
||||
-o, --output=OUTPUT_DIRECTORY
|
||||
(required) Output directory.
|
||||
|
||||
-p, --platform=(android|ios)
|
||||
(required)
|
||||
|
||||
@@ -315,22 +354,19 @@ FLAGS
|
||||
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
|
||||
post-build - builds the native project and leaves the output directory for inspection
|
||||
|
||||
-v, --verbose
|
||||
|
||||
--force
|
||||
Delete OUTPUT_DIRECTORY if it already exists.
|
||||
|
||||
--output=OUTPUT_DIRECTORY
|
||||
(required) Output directory.
|
||||
|
||||
--profile=PROFILE_NAME
|
||||
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
--verbose
|
||||
|
||||
DESCRIPTION
|
||||
Inspect the state of the project at specific build stages. Useful for troubleshooting.
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -363,7 +399,35 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
submit app binary to App Store and/or Play Store
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
|
||||
<value>] [--verbose] [--wait] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the build to submit
|
||||
--latest Submit the latest build for specified platform
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=<value> Path to the .apk/.aab/.ipa file
|
||||
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
|
||||
eas.json.
|
||||
--url=<value> App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
submit app binary to App Store and/or Play Store
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -380,11 +444,11 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
Create a channel on the current project.
|
||||
create a channel
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -397,14 +461,14 @@ FLAGS
|
||||
--json print output as a JSON object with the new channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
Create a channel on the current project.
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
Point a channel at a new branch.
|
||||
point a channel at a new branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -415,17 +479,17 @@ ARGUMENTS
|
||||
|
||||
FLAGS
|
||||
--branch=<value> Name of the branch to point to
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
--json Print output as a JSON object with the channel ID, name and branch mapping
|
||||
|
||||
DESCRIPTION
|
||||
Point a channel at a new branch.
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
List all channels on the current project.
|
||||
list all channels
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -435,14 +499,14 @@ FLAGS
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
List all channels on the current project.
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
View a channel on the current project.
|
||||
view a channel
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -455,14 +519,14 @@ FLAGS
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
View a channel on the current project.
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
show the eas.json config
|
||||
display project configuration (app.json + eas.json)
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -473,24 +537,24 @@ FLAGS
|
||||
--profile=<value>
|
||||
|
||||
DESCRIPTION
|
||||
show the eas.json config
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
manage your credentials
|
||||
manage credentials
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas credentials
|
||||
|
||||
DESCRIPTION
|
||||
manage your credentials
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -504,7 +568,25 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
remove a registered device from your account
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas device:delete [--apple-team-id <value>] [--udid <value>]
|
||||
|
||||
FLAGS
|
||||
--apple-team-id=<value>
|
||||
--udid=<value>...
|
||||
|
||||
DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -521,7 +603,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -535,21 +617,21 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
log environment info to the console
|
||||
display environment info
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas diagnostics
|
||||
|
||||
DESCRIPTION
|
||||
log environment info to the console
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -569,7 +651,52 @@ DESCRIPTION
|
||||
Display help for eas.
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.9/src/commands/help.ts)_
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_
|
||||
|
||||
## `eas init`
|
||||
|
||||
create or link an EAS project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas init
|
||||
|
||||
DESCRIPTION
|
||||
create or link an EAS project
|
||||
|
||||
ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
## `eas login`
|
||||
|
||||
log in with your Expo account
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas login
|
||||
|
||||
DESCRIPTION
|
||||
log in with your Expo account
|
||||
|
||||
ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
## `eas logout`
|
||||
|
||||
log out
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas logout
|
||||
|
||||
DESCRIPTION
|
||||
log out
|
||||
|
||||
ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -583,7 +710,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -600,11 +727,11 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
create an environment secret on the current project or owner account
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -617,14 +744,14 @@ FLAGS
|
||||
--value=<value> Value of the secret
|
||||
|
||||
DESCRIPTION
|
||||
Create an environment secret on the current project or owner account.
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
Delete an environment secret by ID.
|
||||
delete an environment secret by ID
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -634,30 +761,28 @@ FLAGS
|
||||
--id=<value> ID of the secret to delete
|
||||
|
||||
DESCRIPTION
|
||||
Delete an environment secret by ID.
|
||||
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
list environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:list
|
||||
|
||||
DESCRIPTION
|
||||
Lists environment secrets available for your current app
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
Submit build archive to App Store Connect
|
||||
submit app binary to App Store and/or Play Store
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -677,24 +802,22 @@ FLAGS
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
Submit build archive to App Store Connect
|
||||
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
submit app binary to App Store and/or Play Store
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
Publish an update group.
|
||||
publish an update group
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
|
||||
[--group <value> | | ] [-p android|ios|all] [--json] [--auto]
|
||||
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
@@ -704,32 +827,39 @@ FLAGS
|
||||
--input-dir=<value> [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--message=<value> A short message describing the update
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
|
||||
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
|
||||
certificate's directory.
|
||||
--republish Republish an update group
|
||||
--skip-bundler Skip running Expo CLI to bundle the app before publishing
|
||||
|
||||
DESCRIPTION
|
||||
Publish an update group.
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
Configure the project to support EAS Update.
|
||||
configure the project to support EAS Update
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:configure
|
||||
$ eas update:configure [-p android|ios|all]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all] Platform to configure
|
||||
|
||||
DESCRIPTION
|
||||
Configure the project to support EAS Update.
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
Delete all the updates in an update Group.
|
||||
delete all the updates in an update group
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -742,14 +872,33 @@ FLAGS
|
||||
--json Return a json with the group ID of the deleted updates.
|
||||
|
||||
DESCRIPTION
|
||||
Delete all the updates in an update Group.
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
view the recent updates for a branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:list [--branch <value> | --all] [--json]
|
||||
|
||||
FLAGS
|
||||
--all List all updates associated with this project
|
||||
--branch=<value> List all updates on this branch
|
||||
--json Return a json with all of the recent update groups.
|
||||
|
||||
DESCRIPTION
|
||||
view the recent updates for a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
Update group details.
|
||||
update group details
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -762,14 +911,14 @@ FLAGS
|
||||
--json Return a json with the updates belonging to the group.
|
||||
|
||||
DESCRIPTION
|
||||
Update group details.
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
Create a webhook on the current project.
|
||||
create a webhook
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -782,14 +931,14 @@ FLAGS
|
||||
--url=<value> Webhook URL
|
||||
|
||||
DESCRIPTION
|
||||
Create a webhook on the current project.
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
Delete a webhook on the current project.
|
||||
delete a webhook
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -799,14 +948,14 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
|
||||
DESCRIPTION
|
||||
Delete a webhook on the current project.
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
List webhooks on the current project.
|
||||
list webhooks
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -816,14 +965,14 @@ FLAGS
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
|
||||
DESCRIPTION
|
||||
List webhooks on the current project.
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
Create a webhook on the current project.
|
||||
update a webhook
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -837,14 +986,14 @@ FLAGS
|
||||
--url=<value> Webhook URL
|
||||
|
||||
DESCRIPTION
|
||||
Create a webhook on the current project.
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
View a webhook on the current project.
|
||||
view a webhook
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -854,8 +1003,23 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
|
||||
DESCRIPTION
|
||||
View a webhook on the current project.
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.42.3/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
show the username you are logged in as
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas whoami
|
||||
|
||||
DESCRIPTION
|
||||
show the username you are logged in as
|
||||
|
||||
ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
<!-- commandsstop -->
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core');
|
||||
const path = require('path');
|
||||
|
||||
const project = path.join(__dirname, '..', 'tsconfig.json');
|
||||
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
require('ts-node').register({ project });
|
||||
|
||||
// In dev mode, always show stack traces
|
||||
oclif.settings.disableJsonFlag = true
|
||||
|
||||
// Start the CLI
|
||||
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core')
|
||||
|
||||
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
|
||||
require('@oclif/core')
|
||||
.run()
|
||||
.then(require('@oclif/core/flush'))
|
||||
.catch(require('@oclif/core/handle'));
|
||||
Generated
+22043
-20544
File diff suppressed because it is too large.
Load diff
@@ -1,99 +1,107 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.42.3",
|
||||
"version": "0.52.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.26",
|
||||
"@expo/config": "6.0.11",
|
||||
"@expo/config-plugins": "4.0.11",
|
||||
"@expo/eas-build-job": "0.2.61",
|
||||
"@expo/eas-json": "0.42.3",
|
||||
"@expo/json-file": "8.2.34",
|
||||
"@expo/pkcs12": "0.0.6",
|
||||
"@expo/plist": "0.0.16",
|
||||
"@expo/plugin-warn-if-update-available": "2.0.1",
|
||||
"@expo/prebuild-config": "3.0.11",
|
||||
"@expo/apple-utils": "0.0.0-alpha.31",
|
||||
"@expo/code-signing-certificates": "0.0.2",
|
||||
"@expo/config": "6.0.23",
|
||||
"@expo/config-plugins": "4.1.4",
|
||||
"@expo/config-types": "45.0.0",
|
||||
"@expo/eas-build-job": "0.2.72",
|
||||
"@expo/eas-json": "0.52.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.18",
|
||||
"@expo/plugin-warn-if-update-available": "2.2.0",
|
||||
"@expo/prebuild-config": "3.1.6",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@expo/spawn-async": "1.6.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.0.10",
|
||||
"@urql/core": "2.3.1",
|
||||
"@urql/exchange-retry": "0.3.0",
|
||||
"@oclif/core": "1.8.1",
|
||||
"@oclif/plugin-autocomplete": "1.3.0",
|
||||
"@oclif/plugin-help": "5.1.12",
|
||||
"@urql/core": "2.4.4",
|
||||
"@urql/exchange-retry": "0.3.3",
|
||||
"chalk": "4.1.2",
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "4.5.1",
|
||||
"cli-progress": "3.11.0",
|
||||
"cli-table3": "0.6.2",
|
||||
"dateformat": "4.6.3",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-glob": "3.2.7",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.2.11",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "3.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"getenv": "1.0.0",
|
||||
"got": "11.8.2",
|
||||
"gradle-to-js": "2.0.0",
|
||||
"graphql": "15.5.1",
|
||||
"graphql-tag": "2.12.5",
|
||||
"ignore": "5.1.8",
|
||||
"gradle-to-js": "2.0.1",
|
||||
"graphql": "16.5.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"ignore": "5.2.0",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.1",
|
||||
"joi": "17.4.2",
|
||||
"jks-js": "1.0.2",
|
||||
"joi": "17.6.0",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "2.5.2",
|
||||
"minimatch": "3.0.4",
|
||||
"nanoid": "3.1.25",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-forge": "0.10.0",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "5.0.1",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"prompts": "2.3.2",
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.5",
|
||||
"semver": "7.3.7",
|
||||
"slash": "3.0.0",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.3.1",
|
||||
"tslib": "2.4.0",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^1.19.2",
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@graphql-codegen/cli": "2.6.2",
|
||||
"@graphql-codegen/introspection": "2.1.1",
|
||||
"@graphql-codegen/typescript": "2.4.11",
|
||||
"@graphql-codegen/typescript-operations": "2.4.0",
|
||||
"@types/cli-progress": "3.9.2",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/semver": "7.3.8",
|
||||
"@types/tar": "4.0.5",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/node-forge": "1.0.2",
|
||||
"@types/prompts": "2.0.14",
|
||||
"@types/semver": "7.3.9",
|
||||
"@types/tar": "6.1.1",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.21.4",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"memfs": "3.2.2",
|
||||
"mockdate": "^3.0.2",
|
||||
"nock": "^13.0.5",
|
||||
"form-data": "4.0.0",
|
||||
"memfs": "3.4.1",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.4",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "1.0.8",
|
||||
"typescript": "4.5.2"
|
||||
"ts-deepmerge": "2.0.1",
|
||||
"typescript": "4.6.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -114,12 +122,16 @@
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-autocomplete",
|
||||
"@oclif/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
],
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage your account"
|
||||
"description": "manage account"
|
||||
},
|
||||
"branch": {
|
||||
"description": "manage update branches"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
@@ -128,10 +140,10 @@
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage your Apple devices for internal distribution"
|
||||
"description": "manage Apple devices for Internal Distribution"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage your project"
|
||||
"description": "manage project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
import { HTTPError, RequestError } from 'got/dist/source';
|
||||
|
||||
export default class ApiV2Error extends RequestError {
|
||||
readonly name = 'ApiV2Error';
|
||||
readonly expoApiV2ErrorCode: string;
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
|
||||
constructor(
|
||||
originalError: HTTPError,
|
||||
response: {
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
}
|
||||
) {
|
||||
super(response.message, originalError, originalError.request);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import { RequestError } from 'got/dist/source';
|
||||
import nock from 'nock';
|
||||
|
||||
import ApiV2Error from '../ApiV2Error';
|
||||
import { apiClient, getExpoApiBaseUrl } from '../api';
|
||||
import { ApiV2Error, api, getExpoApiBaseUrl } from '../api';
|
||||
|
||||
describe('apiClient', () => {
|
||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||
@@ -23,7 +21,7 @@ describe('apiClient', () => {
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
await api.postAsync('test', { body: {} });
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
@@ -43,11 +41,11 @@ describe('apiClient', () => {
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
await api.postAsync('test', { body: {} });
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeInstanceOf(RequestError);
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).not.toBeInstanceOf(ApiV2Error);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export function asMock(fn: any): jest.Mock {
|
||||
return fn as jest.Mock;
|
||||
}
|
||||
+87
-35
@@ -1,42 +1,94 @@
|
||||
import got, { HTTPError, NormalizedOptions, RequestError } from 'got';
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
|
||||
import ApiV2Error from './ApiV2Error';
|
||||
import fetch, { RequestError, RequestInit } from './fetch';
|
||||
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
|
||||
|
||||
export const apiClient = got.extend({
|
||||
prefixUrl: getExpoApiBaseUrl() + '/v2/',
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
(options: NormalizedOptions) => {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
options.headers.authorization = `Bearer ${token}`;
|
||||
return;
|
||||
}
|
||||
const sessionSecret = getSessionSecret();
|
||||
if (sessionSecret) {
|
||||
options.headers['expo-session'] = sessionSecret;
|
||||
}
|
||||
},
|
||||
],
|
||||
beforeError: [
|
||||
(error: RequestError): RequestError => {
|
||||
if (error instanceof HTTPError) {
|
||||
let result: { [key: string]: any };
|
||||
try {
|
||||
result = JSON.parse(error.response.body as string);
|
||||
} catch (e2) {
|
||||
return error;
|
||||
}
|
||||
if (result.errors?.length) {
|
||||
return new ApiV2Error(error, result.errors[0]);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
},
|
||||
],
|
||||
export class ApiV2Error extends Error {
|
||||
readonly name = 'ApiV2Error';
|
||||
readonly expoApiV2ErrorCode: string;
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
|
||||
constructor(response: {
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
}) {
|
||||
super(response.message);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
body: JSONValue;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async putAsync(path: string, options: RequestOptions): Promise<any> {
|
||||
return await requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
|
||||
},
|
||||
});
|
||||
|
||||
async postAsync(path: string, options: RequestOptions): Promise<any> {
|
||||
return await requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
|
||||
},
|
||||
|
||||
async deleteAsync(path: string): Promise<any> {
|
||||
return await requestAsync(path, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async getAsync(path: string): Promise<any> {
|
||||
return await requestAsync(path, { method: 'GET' });
|
||||
},
|
||||
};
|
||||
|
||||
async function requestAsync(path: string, options: RequestInit): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
});
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
await handleApiErrorAsync(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApiErrorAsync(err: any): Promise<void> {
|
||||
if (err instanceof RequestError) {
|
||||
let result: { [key: string]: any };
|
||||
try {
|
||||
result = (await err.response.json()) as { [key: string]: any };
|
||||
} catch {
|
||||
throw new Error(`Malformed api response: ${await err.response.text()}`);
|
||||
}
|
||||
if (result.errors?.length) {
|
||||
throw new ApiV2Error(result.errors[0]);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
return { authorization: `Bearer ${token}` };
|
||||
}
|
||||
const sessionSecret = getSessionSecret();
|
||||
if (sessionSecret) {
|
||||
return { 'expo-session': sessionSecret };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getExpoApiBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
|
||||
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir);
|
||||
} catch (error) {
|
||||
Log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
let androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
|
||||
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, androidManifest);
|
||||
}
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
Log.warn(
|
||||
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
|
||||
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.RELEASE_CHANNEL
|
||||
);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
const stringifiedRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
if (!stringifiedRequestHeaders) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAndroidManifestAsync(projectDir: string): Promise<AndroidManifest> {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
return AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
@@ -71,8 +71,8 @@ export async function prepareAndroidBuildAsync(
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
|
||||
return await ensureAndroidCredentialsAsync(ctx);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile: ctx.buildProfile,
|
||||
|
||||
@@ -18,6 +18,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
username: job.username,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
developmentClient: job.developmentClient,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ export async function prepareJobAsync(
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
buildType,
|
||||
username,
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
|
||||
+26
-33
@@ -2,39 +2,17 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
|
||||
} from '../../project/android/applicationId';
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await getVcsClient().trackFileAsync(easGradlePath);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
Log.withTick('Android project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync({
|
||||
export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
@@ -48,11 +26,7 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
|
||||
throw new Error(
|
||||
'Project is not configured. Please run "eas build:configure" to configure the project.'
|
||||
);
|
||||
}
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
@@ -73,3 +47,22 @@ function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement):
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this after a few months
|
||||
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
|
||||
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
|
||||
if (await fs.pathExists(easBuildGradlePath)) {
|
||||
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
|
||||
await fs.remove(easBuildGradlePath);
|
||||
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
|
||||
const buildGradleContentsWithoutApply = buildGradleContents.replace(
|
||||
/apply from: ["'].\/eas-build.gradle["']\n/,
|
||||
''
|
||||
);
|
||||
if (buildGradleContentsWithoutApply !== buildGradleContents) {
|
||||
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export async function maybeResolveVersionsAsync(
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
|
||||
+287
-137
@@ -1,16 +1,29 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import cliProgress from 'cli-progress';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { BuildEvent } from '../analytics/events';
|
||||
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildPriority,
|
||||
BuildStatus,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { requestedPlatformDisplayNames } from '../platform';
|
||||
import { Ora, ora } from '../ora';
|
||||
import {
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
@@ -38,7 +51,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
@@ -60,14 +73,13 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!ctx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
|
||||
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -101,43 +113,43 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error: any) {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
`You have already reached the maximum number of pending ${
|
||||
requestedPlatformDisplayNames[job.platform]
|
||||
} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<string> {
|
||||
@@ -209,104 +221,242 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
);
|
||||
}
|
||||
|
||||
type MaybeBuildFragment = BuildFragment | null;
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log(
|
||||
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
|
||||
);
|
||||
const spinner = ora().start();
|
||||
let time = new Date().getTime();
|
||||
const endTime = time + timeoutSec * 1000;
|
||||
while (time <= endTime) {
|
||||
const builds: (BuildFragment | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
Log.debug('Failed to fetch the build status', err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
if (build !== null) {
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
case BuildStatus.New:
|
||||
spinner.text = 'Build created';
|
||||
break;
|
||||
case BuildStatus.InQueue:
|
||||
spinner.text = 'Build queued...';
|
||||
break;
|
||||
case BuildStatus.Canceled:
|
||||
spinner.text = 'Build canceled';
|
||||
spinner.stopAndPersist();
|
||||
return builds;
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return builds;
|
||||
} else {
|
||||
throw new Error(`Standalone build failed!`);
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status');
|
||||
throw new Error(`Unknown build status: ${build.status} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (!spinner.text) {
|
||||
spinner.text = 'Could not fetch the build status. Check your network connection.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
|
||||
spinner.succeed('All builds have finished');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status
|
||||
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
|
||||
build.status
|
||||
)
|
||||
: false
|
||||
).length === builds.length
|
||||
) {
|
||||
spinner.fail('Some of the builds were canceled or failed.');
|
||||
return builds;
|
||||
} else {
|
||||
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
|
||||
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
|
||||
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
|
||||
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
|
||||
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
|
||||
const unknown = builds.length - newBuilds - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
newBuilds && `Builds created: ${newBuilds}`,
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
canceled && `Builds canceled: ${canceled}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
||||
{
|
||||
// 2 hours (max build time limit) + 10 minutes (possible queue time)
|
||||
timeoutSec = 2 * 60 * 60 + 10 * 60,
|
||||
intervalSec = 10,
|
||||
} = {}
|
||||
): Promise<MaybeBuildFragment[]> {
|
||||
const b = `build${buildIds.length > 1 ? 's' : ''}`;
|
||||
Log.log(`Waiting for ${b} to complete. You can press Ctrl+C to exit.`);
|
||||
const spinner = ora(`Waiting for ${b} to complete.`).start();
|
||||
const endTime = new Date().getTime() + timeoutSec * 1000;
|
||||
while (new Date().getTime() <= endTime) {
|
||||
const builds = await getBuildsSafelyAsync(buildIds);
|
||||
const { refetch } =
|
||||
builds.length === 1
|
||||
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
|
||||
: await handleMultipleBuildsProgressAsync({ builds }, { spinner });
|
||||
if (!refetch) {
|
||||
return builds;
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleepAsync(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
spinner.fail('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
|
||||
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
|
||||
const promises = buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
Log.debug('Failed to fetch the build status', err);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
interface BuildProgressResult {
|
||||
refetch: boolean;
|
||||
}
|
||||
|
||||
let queueProgressBarStarted = false;
|
||||
const queueProgressBar = new cliProgress.SingleBar(
|
||||
{ format: '|{bar}| {estimatedWaitTime}' },
|
||||
cliProgress.Presets.rect
|
||||
);
|
||||
|
||||
async function handleSingleBuildProgressAsync(
|
||||
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
|
||||
{ spinner }: { spinner: Ora }
|
||||
): Promise<BuildProgressResult> {
|
||||
if (build === null) {
|
||||
spinner.text = 'Could not fetch the build status. Check your network connection.';
|
||||
return { refetch: true };
|
||||
}
|
||||
|
||||
if (queueProgressBarStarted && build?.status && build.status !== BuildStatus.InQueue) {
|
||||
if (build.status === BuildStatus.InProgress) {
|
||||
queueProgressBar.update(queueProgressBar.getTotal(), {
|
||||
estimatedWaitTime: '',
|
||||
});
|
||||
}
|
||||
queueProgressBar.stop();
|
||||
Log.newLine();
|
||||
queueProgressBarStarted = false;
|
||||
spinner.start('Build is about to start');
|
||||
}
|
||||
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return { refetch: false };
|
||||
case BuildStatus.New:
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}.`;
|
||||
break;
|
||||
case BuildStatus.InQueue: {
|
||||
spinner.text = 'Build queued...';
|
||||
const progressBarPayload =
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
? { estimatedWaitTime: formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds) }
|
||||
: { estimatedWaitTime: '' };
|
||||
|
||||
if (
|
||||
!queueProgressBarStarted &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
) {
|
||||
spinner.stopAndPersist();
|
||||
if (build.priority !== BuildPriority.High) {
|
||||
Log.newLine();
|
||||
Log.log('Start builds sooner in the priority queue.');
|
||||
Log.log(
|
||||
`Sign up for EAS Production or Enterprise at ${chalk.underline(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
Log.log('Waiting in queue');
|
||||
queueProgressBar.start(
|
||||
build.initialQueuePosition + 1,
|
||||
build.initialQueuePosition - build.queuePosition + 1,
|
||||
progressBarPayload
|
||||
);
|
||||
queueProgressBarStarted = true;
|
||||
}
|
||||
if (typeof build.queuePosition === 'number') {
|
||||
queueProgressBar.update(build.queuePosition, progressBarPayload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BuildStatus.Canceled:
|
||||
spinner.fail('Build canceled');
|
||||
return { refetch: false };
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return { refetch: false };
|
||||
} else {
|
||||
throw new Error('Standalone build failed!');
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status');
|
||||
throw new Error(`Unknown build status: ${build.status} - aborting!`);
|
||||
}
|
||||
return { refetch: true };
|
||||
}
|
||||
|
||||
const statusToDisplayName: Record<BuildStatus, string> = {
|
||||
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
|
||||
[BuildStatus.InQueue]: 'in queue',
|
||||
[BuildStatus.InProgress]: 'in progress',
|
||||
[BuildStatus.Canceled]: 'canceled',
|
||||
[BuildStatus.Finished]: 'finished',
|
||||
[BuildStatus.Errored]: 'failed',
|
||||
};
|
||||
|
||||
const platforms = [AppPlatform.Android, AppPlatform.Ios];
|
||||
|
||||
async function handleMultipleBuildsProgressAsync(
|
||||
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
|
||||
{ spinner }: { spinner: Ora }
|
||||
): Promise<BuildProgressResult> {
|
||||
const buildCount = maybeBuilds.length;
|
||||
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
|
||||
|
||||
const allFinished =
|
||||
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
|
||||
const allSettled =
|
||||
builds.filter(build =>
|
||||
[BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(build.status)
|
||||
).length === buildCount;
|
||||
|
||||
if (allSettled) {
|
||||
if (allFinished) {
|
||||
spinner.succeed(formatSettledBuildsText(builds));
|
||||
} else {
|
||||
spinner.fail(formatSettledBuildsText(builds));
|
||||
}
|
||||
return { refetch: false };
|
||||
} else {
|
||||
spinner.text = formatPendingBuildsText(builds);
|
||||
return { refetch: true };
|
||||
}
|
||||
}
|
||||
|
||||
function formatSettledBuildsText(builds: BuildFragment[]): string {
|
||||
return platforms
|
||||
.map(platform => {
|
||||
const build = nullthrows(
|
||||
builds.find(build => build.platform === platform),
|
||||
`Build for platform ${platform} must be defined in this context`
|
||||
);
|
||||
return `${appPlatformEmojis[platform]} ${
|
||||
appPlatformDisplayNames[platform]
|
||||
} build - status: ${chalk.bold(statusToDisplayName[build.status])}`;
|
||||
})
|
||||
.join('\n ');
|
||||
}
|
||||
|
||||
function formatPendingBuildsText(builds: BuildFragment[]): string {
|
||||
return platforms
|
||||
.map(platform => {
|
||||
const build = builds.find(build => build.platform === platform);
|
||||
const status = build ? statusToDisplayName[build.status] : 'unknown';
|
||||
let extraInfo = '';
|
||||
if (
|
||||
build?.status === BuildStatus.InQueue &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
) {
|
||||
const percent = Math.floor(
|
||||
((build.initialQueuePosition - build.queuePosition + 1) /
|
||||
(build.initialQueuePosition + 1)) *
|
||||
100
|
||||
);
|
||||
const estimatedWaitTime =
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
? ` - ${formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds)}`
|
||||
: '';
|
||||
extraInfo = ` - queue progress: ${chalk.bold(`${percent}%`)}${estimatedWaitTime}`;
|
||||
}
|
||||
return `${appPlatformEmojis[platform]} ${
|
||||
appPlatformDisplayNames[platform]
|
||||
} build - status: ${chalk.bold(status)}${extraInfo}`;
|
||||
})
|
||||
.join('\n ');
|
||||
}
|
||||
|
||||
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
|
||||
return maybeBuild !== null;
|
||||
}
|
||||
|
||||
function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
|
||||
if (estimatedWaitTimeLeftSeconds < 5 * 60) {
|
||||
return 'starting soon...';
|
||||
} else {
|
||||
const n = Math.floor(estimatedWaitTimeLeftSeconds / (10 * 60)) + 1;
|
||||
return `starting in about ${n}0 minutes...`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/settings/subscriptions`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -1,97 +1,47 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJson, EasJsonReader } from '@expo/eas-json';
|
||||
import { Errors } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import Log from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
projectDir: string,
|
||||
requestedPlatform: RequestedPlatform
|
||||
): Promise<void> {
|
||||
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = 'This project is not configured to build with EAS. Set it up now?';
|
||||
const confirm = await confirmAsync({ message });
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: requestedPlatform,
|
||||
});
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Errors.error(
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Errors.error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.dev/build/eas-json'
|
||||
)})`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
interface ConfigureParams {
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
/**
|
||||
* Creates eas.json if it does not exist.
|
||||
*
|
||||
* Returns:
|
||||
* - false - if eas.json already exists
|
||||
* - true - if eas.json was created by the function
|
||||
*/
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
configureParams: ConfigureParams
|
||||
): Promise<boolean> {
|
||||
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await configureAsync(configureParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
const ctx: ConfigureContext = {
|
||||
user: await ensureLoggedInAsync(),
|
||||
projectDir: options.projectDir,
|
||||
exp,
|
||||
requestedPlatform: options.platform,
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
|
||||
hasAndroidNativeProject:
|
||||
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
|
||||
hasIosNativeProject:
|
||||
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
|
||||
};
|
||||
|
||||
Log.newLine();
|
||||
await ensureEasJsonExistsAsync(ctx);
|
||||
if (ctx.shouldConfigureAndroid) {
|
||||
await configureAndroidAsync(ctx);
|
||||
}
|
||||
if (ctx.shouldConfigureIos) {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
await createEasJsonAsync(projectDir);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
|
||||
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
|
||||
Log.newLine();
|
||||
Log.withTick('No changes were necessary, the project is already configured correctly.');
|
||||
await reviewAndCommitChangesAsync('Configure EAS Build', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,58 +88,19 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
},
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
const reader = new EasJsonReader(ctx.projectDir);
|
||||
await reader.readAsync();
|
||||
|
||||
Log.withTick('Validated eas.json');
|
||||
return;
|
||||
}
|
||||
async function createEasJsonAsync(projectDir: string): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const easJson =
|
||||
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
|
||||
hasAndroidNativeProject || hasIosNativeProject
|
||||
? EAS_JSON_BARE_DEFAULT
|
||||
: EAS_JSON_MANAGED_DEFAULT;
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
Log.withTick('Generated eas.json');
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
Skip,
|
||||
}
|
||||
|
||||
async function reviewAndCommitChangesAsync(
|
||||
initialCommitMessage: string,
|
||||
askedFirstTime: boolean = true
|
||||
): Promise<void> {
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: 'Can we commit these changes to git for you?',
|
||||
choices: [
|
||||
{ title: 'Yes', value: ShouldCommitChanges.Yes },
|
||||
...(askedFirstTime
|
||||
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
|
||||
: []),
|
||||
{
|
||||
title: 'Skip committing changes, I will do it later on my own',
|
||||
value: ShouldCommitChanges.Skip,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await getVcsClient().showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
|
||||
}
|
||||
@@ -6,23 +6,11 @@ import type { XCBuildConfiguration } from 'xcode';
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||
import { Actor } from '../user/User';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
requestedPlatform: RequestedPlatform;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
hasAndroidNativeProject: boolean;
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
@@ -51,7 +39,6 @@ export interface BuildContext<T extends Platform> {
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
|
||||
@@ -25,7 +25,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -34,7 +33,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
@@ -81,7 +79,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
|
||||
expoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
// TODO: ensure ExpoPlist in pbxproj
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir);
|
||||
} catch (error) {
|
||||
Log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistVersionConfigurationSynced(exp, expoPlist)) {
|
||||
expoPlist = IOSConfig.Updates.setVersionsConfig(exp, expoPlist);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
|
||||
Log.warn(
|
||||
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
|
||||
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
|
||||
throw new Error('Missing values in Expo.plist');
|
||||
}
|
||||
}
|
||||
|
||||
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
projectDir: string,
|
||||
expoPlist: IOSConfig.ExpoPlist
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await getVcsClient().trackFileAsync(expoPlistPath);
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
|
||||
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
return updatesRequestHeaders['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
@@ -36,13 +36,12 @@ export async function createIosContextAsync(
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
const targets = await resolveTargetsAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
xcodeBuildContext,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
|
||||
|
||||
@@ -63,8 +62,8 @@ export async function prepareIosBuildAsync(
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile: ctx.buildProfile,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
@@ -19,13 +18,6 @@ export async function ensureIosCredentialsAsync(
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
buildCtx.projectDir,
|
||||
buildCtx.workflow,
|
||||
buildCtx.buildProfile.env ?? {}
|
||||
),
|
||||
},
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
username: job.username,
|
||||
developmentClient: job.developmentClient,
|
||||
simulator: job.simulator,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ export async function prepareJobAsync(
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
username,
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
+3
-23
@@ -3,32 +3,12 @@ import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
Log.withTick('iOS project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync({
|
||||
export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
@@ -6,13 +6,14 @@ import semver from 'semver';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.57';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.78';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
skipCleanup?: boolean;
|
||||
skipNativeBuild?: boolean;
|
||||
artifactsDir?: string;
|
||||
artifactPath?: string;
|
||||
workingdir?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
@@ -41,6 +42,7 @@ export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions):
|
||||
: {}),
|
||||
...(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;
|
||||
|
||||
@@ -5,34 +5,21 @@ import fs from 'fs-extra';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import Log from '../log';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from './android/UpdatesModule';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
} from '../update/android/UpdatesModule';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from './ios/UpdatesModule';
|
||||
} from '../update/ios/UpdatesModule';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
|
||||
function getRuntimeVersionNullable(
|
||||
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
|
||||
): string | null {
|
||||
try {
|
||||
return Updates.getRuntimeVersion(config, platform);
|
||||
} catch (e) {
|
||||
Log.debug(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
@@ -49,7 +36,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx),
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
@@ -38,7 +39,6 @@ let sdkVersionChecked = false;
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
skipProjectConfiguration: boolean;
|
||||
profile?: string;
|
||||
nonInteractive: boolean;
|
||||
wait: boolean;
|
||||
@@ -53,7 +53,10 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir, flags.requestedPlatform);
|
||||
await ensureProjectConfiguredAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
@@ -128,10 +131,17 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
|
||||
const { accountName } = Object.values(buildCtxByPlatform)[0];
|
||||
const builds = await waitForBuildEndAsync({
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
accountName,
|
||||
});
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
@@ -163,7 +173,6 @@ async function prepareAndStartBuildAsync({
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
localBuildOptions: flags.localBuildOptions,
|
||||
});
|
||||
|
||||
|
||||
@@ -103,10 +103,7 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
|
||||
Log.warn("Changes won't affect your project config.");
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn(
|
||||
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
|
||||
);
|
||||
Log.warn('This command is using API that soon will be deprecated.');
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else {
|
||||
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ExpoConfig, getPackageJson } from '@expo/config';
|
||||
|
||||
export function isExpoUpdatesInstalled(projectDir: string): boolean {
|
||||
const packageJson = getPackageJson(projectDir);
|
||||
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
|
||||
}
|
||||
|
||||
export function ensureValidVersions(exp: ExpoConfig): void {
|
||||
if (!exp.runtimeVersion && !exp.sdkVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find either 'runtimeVersion' or 'sdkVersion' to configure 'expo-updates'. Please specify at least one of these properties under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,24 @@ import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
|
||||
return new URL(
|
||||
`/accounts/${accountName}/projects/${projectName}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
if (project.__typename === 'App') {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
|
||||
} else {
|
||||
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
|
||||
}
|
||||
const url =
|
||||
project.__typename !== 'App'
|
||||
? `/builds/${build.id}`
|
||||
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
|
||||
|
||||
return new URL(url, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
|
||||
return new URL(`/artifacts/${artifactId}`, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
|
||||
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
|
||||
import { getUserAsync } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand from '../EasCommand';
|
||||
import TestEasCommand from './TestEasCommand';
|
||||
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
|
||||
@@ -32,17 +28,30 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(getUserAsync)
|
||||
jest
|
||||
.mocked(getUserAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(ensureLoggedInAsync)
|
||||
.mockImplementation(async () => mockJester);
|
||||
jest
|
||||
.mocked(ensureLoggedInAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(initAsync).mockReset();
|
||||
asMock(flushAsync).mockReset();
|
||||
asMock(logEvent).mockReset();
|
||||
.mockImplementation(async () => mockJester);
|
||||
jest.mocked(initAsync).mockReset();
|
||||
jest.mocked(flushAsync).mockReset();
|
||||
jest.mocked(logEvent).mockReset();
|
||||
});
|
||||
|
||||
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
|
||||
class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = authValue;
|
||||
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
return TestEasCommand;
|
||||
};
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
describe('without exceptions', () => {
|
||||
// The first test in this suite should have an increased timeout
|
||||
@@ -53,32 +62,36 @@ describe(EasCommand.name, () => {
|
||||
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
|
||||
// and look for "Config.load"
|
||||
it('ensures the user is logged in', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(ensureLoggedInAsync).toHaveBeenCalled();
|
||||
}, 15_000);
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
|
||||
|
||||
const TestEasCommand = createTestEasCommand(false);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(getUserAsync).toHaveReturnedWith(mockJester);
|
||||
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
|
||||
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
@@ -89,11 +102,12 @@ describe(EasCommand.name, () => {
|
||||
|
||||
describe('after exceptions', () => {
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand(true);
|
||||
try {
|
||||
await TestEasCommand.run().then(() => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
export default class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = this.authValue();
|
||||
|
||||
authValue(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
@@ -3,7 +3,7 @@ import Log from '../log';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
|
||||
export default class AnalyticsView extends EasCommand {
|
||||
static description = 'view or change analytics settings';
|
||||
static description = 'display or change analytics settings';
|
||||
|
||||
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
@@ -51,7 +52,7 @@ export async function createUpdateBranchOnAppAsync({
|
||||
}
|
||||
|
||||
export default class BranchCreate extends EasCommand {
|
||||
static description = 'Create a branch on the current project.';
|
||||
static description = 'create a branch';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -73,6 +74,9 @@ export default class BranchCreate extends EasCommand {
|
||||
args: { name },
|
||||
flags,
|
||||
} = await this.parse(BranchCreate);
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -98,12 +102,11 @@ export default class BranchCreate extends EasCommand {
|
||||
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, name });
|
||||
|
||||
if (flags.json) {
|
||||
Log.log(newBranch);
|
||||
return;
|
||||
printJsonOnlyOutput(newBranch);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
async function getBranchInfoAsync({
|
||||
appId,
|
||||
@@ -76,7 +77,7 @@ async function deleteBranchOnAppAsync({
|
||||
}
|
||||
|
||||
export default class BranchDelete extends EasCommand {
|
||||
static description = 'Delete a branch on the current project';
|
||||
static description = 'delete a branch';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -97,6 +98,9 @@ export default class BranchDelete extends EasCommand {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(BranchDelete);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -141,11 +145,11 @@ export default class BranchDelete extends EasCommand {
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(deletionResult));
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { UpdateBranchFragmentNode } from '../../graphql/types/UpdateBranch';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
const BRANCHES_LIMIT = 10_000;
|
||||
|
||||
@@ -54,7 +55,7 @@ export async function listBranchesAsync({
|
||||
}
|
||||
|
||||
export default class BranchList extends EasCommand {
|
||||
static description = 'List all branches on this project.';
|
||||
static description = 'list all branches';
|
||||
|
||||
static flags = {
|
||||
json: Flags.boolean({
|
||||
@@ -65,13 +66,16 @@ export default class BranchList extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BranchList);
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
if (flags.json) {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
printJsonOnlyOutput(branches);
|
||||
} else {
|
||||
const table = new CliTable({
|
||||
head: ['Branch', ...UPDATE_COLUMNS],
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
async function renameUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -51,7 +52,7 @@ async function renameUpdateBranchOnAppAsync({
|
||||
}
|
||||
|
||||
export default class BranchRename extends EasCommand {
|
||||
static description = 'Rename a branch.';
|
||||
static description = 'rename a branch';
|
||||
|
||||
static flags = {
|
||||
from: Flags.string({
|
||||
@@ -72,6 +73,9 @@ export default class BranchRename extends EasCommand {
|
||||
let {
|
||||
flags: { json: jsonFlag, from: currentName, to: newName },
|
||||
} = await this.parse(BranchRename);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -111,14 +115,13 @@ export default class BranchRename extends EasCommand {
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(editedBranch));
|
||||
return;
|
||||
printJsonOnlyOutput(editedBranch);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Renamed branch from ${currentName} to ${chalk.bold(
|
||||
editedBranch.name
|
||||
)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Renamed branch from ${currentName} to ${chalk.bold(
|
||||
editedBranch.name
|
||||
)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,72 +2,19 @@ import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { ViewBranchQuery, ViewBranchQueryVariables } from '../../graphql/generated';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
import groupBy from '../../utils/expodash/groupBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
|
||||
export async function viewUpdateBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<ViewBranchQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<ViewBranchQuery, ViewBranchQueryVariables>(
|
||||
gql`
|
||||
query ViewBranch($appId: String!, $name: String!, $limit: Int!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateBranchByName(name: $name) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: $limit) {
|
||||
id
|
||||
group
|
||||
message
|
||||
createdAt
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestFragment
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
limit: PAGE_LIMIT,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data;
|
||||
}
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class BranchView extends EasCommand {
|
||||
static description = 'View a branch.';
|
||||
static description = 'view a branch';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -89,6 +36,9 @@ export default class BranchView extends EasCommand {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(BranchView);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -107,7 +57,7 @@ export default class BranchView extends EasCommand {
|
||||
}));
|
||||
}
|
||||
|
||||
const { app } = await viewUpdateBranchAsync({
|
||||
const { app } = await UpdateQuery.viewBranchAsync({
|
||||
appId: projectId,
|
||||
name,
|
||||
});
|
||||
@@ -121,37 +71,36 @@ export default class BranchView extends EasCommand {
|
||||
);
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify({ ...UpdateBranch, updates }));
|
||||
return;
|
||||
printJsonOnlyOutput({ ...UpdateBranch, updates });
|
||||
} else {
|
||||
const groupTable = new Table({
|
||||
head: UPDATE_COLUMNS,
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
groupTable.push([
|
||||
formatUpdate(update),
|
||||
update.runtimeVersion,
|
||||
update.group,
|
||||
getPlatformsForGroup({
|
||||
updates: UpdateBranch.updates,
|
||||
group: update.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branch:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: UpdateBranch.name },
|
||||
{ label: 'ID', value: UpdateBranch.id },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Recently published update groups:'));
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: UPDATE_COLUMNS,
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
groupTable.push([
|
||||
formatUpdate(update),
|
||||
update.runtimeVersion,
|
||||
update.group,
|
||||
getPlatformsForGroup({
|
||||
updates: UpdateBranch.updates,
|
||||
group: update.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branch:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: UpdateBranch.name },
|
||||
{ label: 'ID', value: UpdateBranch.id },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Recently published update groups:'));
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
@@ -104,13 +104,13 @@ async function selectBuildToCancelAsync(
|
||||
async function ensureBuildExistsAsync(buildId: string): Promise<void> {
|
||||
try {
|
||||
await BuildQuery.byIdAsync(buildId);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
throw new Error(`Couldn't find a build matching the id ${buildId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class BuildCancel extends EasCommand {
|
||||
static description = 'Cancel a build.';
|
||||
static description = 'cancel a build';
|
||||
|
||||
static args = [{ name: 'BUILD_ID' }];
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export default class BuildConfigure extends EasCommand {
|
||||
static description = 'Configure the project to support EAS Build.';
|
||||
static description = 'configure the project to support EAS Build';
|
||||
|
||||
static flags = {
|
||||
platform: Flags.enum({
|
||||
@@ -26,45 +32,60 @@ export default class BuildConfigure extends EasCommand {
|
||||
Log.log(
|
||||
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const expoUpdatesIsInstalled = isExpoUpdatesInstalled(projectDir);
|
||||
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
|
||||
await configureAsync({
|
||||
platform,
|
||||
projectDir: await findProjectRootAsync(),
|
||||
// clean up old Android configuration
|
||||
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
}
|
||||
|
||||
// ensure eas.json exists
|
||||
Log.newLine();
|
||||
await ensureProjectConfiguredAsync({
|
||||
projectDir,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
Log.newLine();
|
||||
logSuccess(platform);
|
||||
}
|
||||
}
|
||||
// configure expo-updates
|
||||
if (expoUpdatesIsInstalled) {
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
function logSuccess(platform: RequestedPlatform): void {
|
||||
let platformsText = 'iOS and Android projects are';
|
||||
let storesText = 'the Apple App Store or Google Play Store';
|
||||
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'android') {
|
||||
platformsText = 'Android project is';
|
||||
storesText = 'the Google Play Store';
|
||||
} else if (platform === 'ios') {
|
||||
platformsText = 'iOS project is';
|
||||
storesText = 'the Apple App Store Connect';
|
||||
}
|
||||
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await syncIosUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.log(`🎉 Your ${platformsText} ready to build.
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
Log.log(`🎉 Your project is ready to build.
|
||||
|
||||
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
|
||||
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
|
||||
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to app stores.
|
||||
- ${learnMore('https://docs.expo.dev/build/introduction', {
|
||||
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
|
||||
})}`);
|
||||
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
|
||||
})}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
Log.addNewLineIfNone();
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Which platforms would you like to configure for EAS Build?',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
|
||||
@@ -23,6 +24,7 @@ interface RawBuildFlags {
|
||||
profile?: string;
|
||||
'non-interactive': boolean;
|
||||
local: boolean;
|
||||
output?: string;
|
||||
wait: boolean;
|
||||
'clear-cache': boolean;
|
||||
json: boolean;
|
||||
@@ -31,7 +33,7 @@ interface RawBuildFlags {
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'Start a build';
|
||||
static description = 'start a build';
|
||||
|
||||
static flags = {
|
||||
platform: Flags.enum({
|
||||
@@ -48,7 +50,7 @@ export default class Build extends EasCommand {
|
||||
}),
|
||||
'skip-project-configuration': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Skip project configuration',
|
||||
hidden: true,
|
||||
}),
|
||||
profile: Flags.string({
|
||||
description:
|
||||
@@ -63,6 +65,9 @@ export default class Build extends EasCommand {
|
||||
default: false,
|
||||
description: 'Run build locally [experimental]',
|
||||
}),
|
||||
output: Flags.string({
|
||||
description: 'Output path for local build',
|
||||
}),
|
||||
wait: Flags.boolean({
|
||||
default: true,
|
||||
allowNo: true,
|
||||
@@ -100,6 +105,9 @@ export default class Build extends EasCommand {
|
||||
|
||||
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.local && flags.output) {
|
||||
Errors.error('--output is allowed only for local builds', { exit: 1 });
|
||||
}
|
||||
if (!flags.platform && nonInteractive) {
|
||||
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
@@ -130,15 +138,25 @@ export default class Build extends EasCommand {
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
if (flags['skip-project-configuration']) {
|
||||
Log.warnDeprecatedFlag(
|
||||
'skip-project-configuration',
|
||||
'Automatic configuration of native code is no longer optional.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const profile = flags['profile'];
|
||||
return {
|
||||
requestedPlatform,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
profile,
|
||||
nonInteractive,
|
||||
localBuildOptions: flags['local']
|
||||
? { enable: true, verbose: true }
|
||||
? {
|
||||
enable: true,
|
||||
verbose: true,
|
||||
artifactPath: flags.output && path.resolve(process.cwd(), flags.output),
|
||||
}
|
||||
: {
|
||||
enable: false,
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ const STAGE_DESCRIPTION = `Stage of the build you want to inspect.
|
||||
|
||||
export default class BuildInspect extends EasCommand {
|
||||
static description =
|
||||
'Inspect the state of the project at specific build stages. Useful for troubleshooting.';
|
||||
'inspect the state of the project at specific build stages, useful for troubleshooting';
|
||||
|
||||
static flags = {
|
||||
platform: Flags.enum({
|
||||
@@ -46,6 +46,7 @@ export default class BuildInspect extends EasCommand {
|
||||
required: true,
|
||||
}),
|
||||
output: Flags.string({
|
||||
char: 'o',
|
||||
description: 'Output directory.',
|
||||
required: true,
|
||||
helpValue: 'OUTPUT_DIRECTORY',
|
||||
@@ -55,6 +56,7 @@ export default class BuildInspect extends EasCommand {
|
||||
default: false,
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
char: 'v',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
@@ -62,18 +64,25 @@ export default class BuildInspect extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildInspect);
|
||||
const outputDirectory = path.resolve(process.cwd(), flags.output);
|
||||
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
|
||||
|
||||
if (flags.force && process.cwd().startsWith(outputDirectory)) {
|
||||
throw new Error(
|
||||
`This operation is not allowed, it would delete all files in ${outputDirectory} including the current project.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.prepareOutputDirAsync(outputDirectory, flags.force);
|
||||
|
||||
if (flags.stage === InspectStage.ARCHIVE) {
|
||||
const vcs = getVcsClient();
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await vcs.makeShallowCopyAsync(outputDirectory);
|
||||
Log.withTick(`Project saved to ${outputDirectory}`);
|
||||
await vcs.makeShallowCopyAsync(tmpWorkingdir);
|
||||
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
|
||||
} else {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
|
||||
try {
|
||||
await runBuildAndSubmitAsync(projectDir, {
|
||||
skipProjectConfiguration: false,
|
||||
nonInteractive: false,
|
||||
wait: true,
|
||||
clearCache: false,
|
||||
@@ -93,24 +102,13 @@ export default class BuildInspect extends EasCommand {
|
||||
if (!flags.verbose) {
|
||||
Log.log(chalk.green('Build successful'));
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
if (!flags.verbose) {
|
||||
Log.error('Build failed');
|
||||
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
|
||||
}
|
||||
} finally {
|
||||
const spinner = ora().start(`Copying project build directory to ${outputDirectory}`);
|
||||
try {
|
||||
const tmpBuildDirectory = path.join(tmpWorkingdir, 'build');
|
||||
if (await fs.pathExists(tmpBuildDirectory)) {
|
||||
await fs.copy(tmpBuildDirectory, outputDirectory);
|
||||
}
|
||||
await fs.remove(tmpWorkingdir);
|
||||
spinner.succeed(`Project build directory saved to ${outputDirectory}`);
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
await this.copyToOutputDirAsync(path.join(tmpWorkingdir, 'build'), outputDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,4 +123,18 @@ export default class BuildInspect extends EasCommand {
|
||||
}
|
||||
await fs.mkdirp(outputDir);
|
||||
}
|
||||
|
||||
private async copyToOutputDirAsync(src: string, dst: string): Promise<void> {
|
||||
const spinner = ora().start(`Copying project directory to ${dst}`);
|
||||
try {
|
||||
if (await fs.pathExists(src)) {
|
||||
await fs.copy(src, dst);
|
||||
}
|
||||
await fs.remove(src);
|
||||
spinner.succeed(`Project directory saved to ${dst}`);
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,16 @@ import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
|
||||
export async function createUpdateChannelOnAppAsync({
|
||||
@@ -59,7 +60,7 @@ export async function createUpdateChannelOnAppAsync({
|
||||
}
|
||||
|
||||
export default class ChannelCreate extends EasCommand {
|
||||
static description = 'Create a channel on the current project.';
|
||||
static description = 'create a channel';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -82,6 +83,9 @@ export default class ChannelCreate extends EasCommand {
|
||||
args: { name: channelName },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(ChannelCreate);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -102,14 +106,15 @@ export default class ChannelCreate extends EasCommand {
|
||||
|
||||
let branchId: string;
|
||||
let branchMessage: string;
|
||||
try {
|
||||
const existingBranch = await getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: channelName,
|
||||
});
|
||||
const existingBranch = await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: channelName,
|
||||
});
|
||||
|
||||
if (existingBranch) {
|
||||
branchId = existingBranch.id;
|
||||
branchMessage = `We found a branch with the same name`;
|
||||
} catch (e) {
|
||||
} else {
|
||||
const newBranch = await createUpdateBranchOnAppAsync({
|
||||
appId: projectId,
|
||||
name: channelName,
|
||||
@@ -133,30 +138,29 @@ export default class ChannelCreate extends EasCommand {
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newChannel));
|
||||
return;
|
||||
printJsonOnlyOutput(newChannel);
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.withTick(
|
||||
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
|
||||
);
|
||||
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 },
|
||||
])
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('You can now update your app by publishing!'));
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.withTick(
|
||||
`Created a new channel on project ${chalk.bold(await getProjectFullNameAsync(exp))}`
|
||||
);
|
||||
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 },
|
||||
])
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('You can now update your app by publishing!'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateChannelMutation,
|
||||
DeleteUpdateChannelMutationVariables,
|
||||
DeleteUpdateChannelResult,
|
||||
GetChannelInfoQuery,
|
||||
GetChannelInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class ChannelDelete extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Delete a channel';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the channel to delete',
|
||||
},
|
||||
];
|
||||
static flags = {
|
||||
json: Flags.boolean({
|
||||
description: 'print output as a JSON object',
|
||||
default: false,
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { name: nameArg },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractiveFlag },
|
||||
} = await this.parse(ChannelDelete);
|
||||
if (jsonFlag && !nonInteractiveFlag) {
|
||||
throw new Error('--json is allowed only in non-interactive mode');
|
||||
}
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
let name;
|
||||
if (nameArg) {
|
||||
name = nameArg;
|
||||
} else {
|
||||
if (nonInteractiveFlag) {
|
||||
throw new Error('Channel name must be set when running in non-interactive mode');
|
||||
}
|
||||
name = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the channel to delete:',
|
||||
validate: (value: any) => (value ? true : 'Channel name may not be empty.'),
|
||||
})
|
||||
).name;
|
||||
}
|
||||
|
||||
const data = await getChannelInfoAsync({ appId: projectId, name });
|
||||
const channelId = data.app?.byId.updateChannelByName?.id;
|
||||
if (!channelId) {
|
||||
throw new Error(`Could not find channel ${name} on ${fullName}`);
|
||||
}
|
||||
|
||||
if (!nonInteractiveFlag) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permamently delete channel: "${name}".\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of channel: "${name}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const deletionResult = await deleteChannelOnAppAsync({
|
||||
channelId,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
} else {
|
||||
Log.withTick(`️Deleted channel "${name}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getChannelInfoAsync({
|
||||
appId,
|
||||
name,
|
||||
}: GetChannelInfoQueryVariables): Promise<GetChannelInfoQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetChannelInfoQuery, GetChannelInfoQueryVariables>(
|
||||
gql`
|
||||
query GetChannelInfo($appId: String!, $name: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannelByName(name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateChannel'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function deleteChannelOnAppAsync({
|
||||
channelId,
|
||||
}: DeleteUpdateChannelMutationVariables): Promise<DeleteUpdateChannelResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateChannelMutation, DeleteUpdateChannelMutationVariables>(
|
||||
gql`
|
||||
mutation DeleteUpdateChannel($channelId: ID!) {
|
||||
updateChannel {
|
||||
deleteUpdateChannel(channelId: $channelId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
channelId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateChannel.deleteUpdateChannel;
|
||||
}
|
||||
@@ -13,13 +13,11 @@ import {
|
||||
UpdateChannelBranchMappingMutation,
|
||||
UpdateChannelBranchMappingMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
async function getChannelByNameForAppAsync({
|
||||
appId,
|
||||
@@ -95,7 +93,7 @@ export async function updateChannelBranchMappingAsync({
|
||||
}
|
||||
|
||||
export default class ChannelEdit extends EasCommand {
|
||||
static description = 'Point a channel at a new branch.';
|
||||
static description = 'point a channel at a new branch';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -110,33 +108,22 @@ export default class ChannelEdit extends EasCommand {
|
||||
description: 'Name of the branch to point to',
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
|
||||
description: 'Print output as a JSON object with the channel ID, name and branch mapping',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name: channelName },
|
||||
flags: { branch: branchName, json: jsonFlag },
|
||||
} = await this.parse(ChannelEdit);
|
||||
const { args, flags } = await this.parse(ChannelEdit);
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to edit a specific channel.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: channelName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the channel to edit:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
const channelName = args.name ?? (await promptForChannelAsync());
|
||||
|
||||
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
|
||||
if (existingChannel.updateBranches.length > 1) {
|
||||
@@ -145,20 +132,19 @@ export default class ChannelEdit extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: chalk`What branch should it change to?`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
const branchName = flags.branch ?? (await promptForBranchAsync());
|
||||
|
||||
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
|
||||
const branch = await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
const channel = await updateChannelBranchMappingAsync({
|
||||
channelId: existingChannel.id,
|
||||
// todo: move branch mapping logic to utility
|
||||
@@ -168,17 +154,38 @@ export default class ChannelEdit extends EasCommand {
|
||||
}),
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(channel));
|
||||
return;
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(channel);
|
||||
} else {
|
||||
Log.withTick(
|
||||
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForChannelAsync(): Promise<string> {
|
||||
Log.addNewLineIfNone();
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the channel to edit:',
|
||||
validate: value => (value ? true : 'The channel name may not be empty.'),
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
async function promptForBranchAsync(): Promise<string> {
|
||||
Log.addNewLineIfNone();
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `To which branch should the channel link?`,
|
||||
validate: value => (value ? true : 'The branch name may not be empty.'),
|
||||
});
|
||||
return name;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { logChannelDetails } from './view';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
@@ -66,7 +67,7 @@ async function getAllUpdateChannelForAppAsync({
|
||||
}
|
||||
|
||||
export default class ChannelList extends EasCommand {
|
||||
static description = 'List all channels on the current project.';
|
||||
static description = 'list all channels';
|
||||
|
||||
static flags = {
|
||||
json: Flags.boolean({
|
||||
@@ -79,6 +80,9 @@ export default class ChannelList extends EasCommand {
|
||||
const {
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(ChannelList);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -93,19 +97,18 @@ export default class ChannelList extends EasCommand {
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(channels));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const channel of channels) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
printJsonOnlyOutput(channels);
|
||||
} else {
|
||||
for (const channel of channels) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { updateChannelBranchMappingAsync } from './edit';
|
||||
import { BranchMapping, getBranchMapping, getUpdateChannelByNameForAppAsync } from './view';
|
||||
import { BranchMapping, getBranchMapping } from './view';
|
||||
|
||||
async function promptForRolloutPercentAsync({
|
||||
promptMessage,
|
||||
@@ -38,28 +40,20 @@ async function promptForRolloutPercentAsync({
|
||||
return rolloutPercent;
|
||||
}
|
||||
|
||||
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
|
||||
function getRolloutInfo(
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>
|
||||
): {
|
||||
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
} {
|
||||
const { branchMapping } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
const { branchMapping } = getBranchMapping(channel.branchMapping);
|
||||
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
|
||||
const newBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
const newBranch = channel.updateBranches.filter(branch => branch.id === newBranchId)[0];
|
||||
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
|
||||
|
||||
if (!newBranch || !oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.name}".`
|
||||
);
|
||||
throw new Error(`Branch mapping rollout is missing a branch for channel "${channel.name}".`);
|
||||
}
|
||||
|
||||
const currentPercent = 100 * branchMapping.data[0].branchMappingLogic.operand;
|
||||
@@ -74,16 +68,16 @@ async function startRolloutAsync({
|
||||
projectId,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
channel,
|
||||
}: {
|
||||
channelName?: string;
|
||||
branchName?: string;
|
||||
branchName: string;
|
||||
percent?: number;
|
||||
jsonFlag: boolean;
|
||||
projectId: string;
|
||||
fullName: string;
|
||||
currentBranchMapping: BranchMapping;
|
||||
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery;
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
|
||||
}): Promise<{
|
||||
newChannelInfo: {
|
||||
id: string;
|
||||
@@ -92,19 +86,18 @@ async function startRolloutAsync({
|
||||
};
|
||||
logMessage: string;
|
||||
}> {
|
||||
if (!branchName) {
|
||||
const validationMessage = 'A branch must be specified.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `Select a branch to rollout onto ${channelName}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
const branch = await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
|
||||
|
||||
const oldBranchId = currentBranchMapping.data[0].branchId;
|
||||
if (branch.id === oldBranchId) {
|
||||
throw new Error(
|
||||
@@ -137,14 +130,11 @@ async function startRolloutAsync({
|
||||
],
|
||||
};
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
channelId: channel.id,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const oldBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
|
||||
if (!oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
|
||||
@@ -167,13 +157,13 @@ async function editRolloutAsync({
|
||||
percent,
|
||||
jsonFlag,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
channel,
|
||||
}: {
|
||||
channelName?: string;
|
||||
percent?: number;
|
||||
jsonFlag: boolean;
|
||||
currentBranchMapping: BranchMapping;
|
||||
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery;
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
|
||||
}): Promise<{
|
||||
newChannelInfo: {
|
||||
id: string;
|
||||
@@ -182,9 +172,7 @@ async function editRolloutAsync({
|
||||
};
|
||||
logMessage: string;
|
||||
}> {
|
||||
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(
|
||||
getUpdateChannelByNameForAppResult
|
||||
);
|
||||
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
|
||||
|
||||
if (percent == null) {
|
||||
if (jsonFlag) {
|
||||
@@ -204,7 +192,7 @@ async function editRolloutAsync({
|
||||
newBranchMapping.data[0].branchMappingLogic.operand = percent / 100;
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
channelId: channel.id,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
@@ -224,13 +212,13 @@ async function endRolloutAsync({
|
||||
branchName,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
channel,
|
||||
}: {
|
||||
channelName?: string;
|
||||
branchName?: string;
|
||||
jsonFlag: boolean;
|
||||
projectId: string;
|
||||
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery;
|
||||
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
|
||||
}): Promise<{
|
||||
newChannelInfo: {
|
||||
id: string;
|
||||
@@ -240,13 +228,22 @@ async function endRolloutAsync({
|
||||
logMessage: string;
|
||||
}> {
|
||||
// end rollout
|
||||
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(
|
||||
getUpdateChannelByNameForAppResult
|
||||
);
|
||||
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
|
||||
|
||||
let endOnNewBranch;
|
||||
if (branchName) {
|
||||
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
|
||||
const branch = await BranchQuery.getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
switch (branch.id) {
|
||||
case newBranch.id:
|
||||
endOnNewBranch = true;
|
||||
@@ -294,7 +291,7 @@ async function endRolloutAsync({
|
||||
};
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
channelId: channel.id,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
const logMessage = `️Rollout on channel ${chalk.bold(
|
||||
@@ -342,18 +339,29 @@ export default class ChannelRollout extends EasCommand {
|
||||
args: { channel: channelName },
|
||||
flags: { json: jsonFlag, end: endFlag, branch: branchName, percent },
|
||||
} = await this.parse(ChannelRollout);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const getUpdateChannelByNameForAppResult = await getUpdateChannelByNameForAppAsync({
|
||||
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
channelName: channelName!,
|
||||
});
|
||||
if (!channel) {
|
||||
throw new Error(
|
||||
`Could not find a channel named "${channelName}". Please check what channels exist on this project with ${chalk.bold(
|
||||
'eas channel:list'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
const { branchMapping: currentBranchMapping, isRollout } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
channel.branchMapping
|
||||
);
|
||||
|
||||
if (currentBranchMapping.data.length === 0) {
|
||||
@@ -387,13 +395,13 @@ export default class ChannelRollout extends EasCommand {
|
||||
if (!isRollout) {
|
||||
rolloutMutationResult = await startRolloutAsync({
|
||||
channelName,
|
||||
branchName,
|
||||
branchName: branchName ?? (await promptForBranchNameAsync(channelName)),
|
||||
percent,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
channel,
|
||||
});
|
||||
} else if (endFlag) {
|
||||
rolloutMutationResult = await endRolloutAsync({
|
||||
@@ -401,7 +409,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
branchName,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
channel,
|
||||
});
|
||||
} else {
|
||||
rolloutMutationResult = await editRolloutAsync({
|
||||
@@ -409,7 +417,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
percent,
|
||||
jsonFlag,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
channel,
|
||||
});
|
||||
}
|
||||
if (!rolloutMutationResult) {
|
||||
@@ -417,9 +425,19 @@ export default class ChannelRollout extends EasCommand {
|
||||
}
|
||||
const { newChannelInfo, logMessage } = rolloutMutationResult;
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newChannelInfo));
|
||||
return;
|
||||
printJsonOnlyOutput(newChannelInfo);
|
||||
} else {
|
||||
Log.withTick(logMessage);
|
||||
}
|
||||
Log.withTick(logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForBranchNameAsync(channelName: string): Promise<string> {
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `Select a branch to rollout onto ${channelName}`,
|
||||
validate: value => (value ? true : 'A branch must be specified.'),
|
||||
});
|
||||
return name;
|
||||
}
|
||||
@@ -3,14 +3,9 @@ import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetChannelByNameForAppQuery,
|
||||
GetChannelByNameForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -21,6 +16,7 @@ import {
|
||||
getPlatformsForGroup,
|
||||
} from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -49,7 +45,7 @@ export function getBranchMapping(branchMappingString?: string): {
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
@@ -90,56 +86,6 @@ export function getBranchMapping(branchMappingString?: string): {
|
||||
return { branchMapping, isRollout, rolloutPercent };
|
||||
}
|
||||
|
||||
export async function getUpdateChannelByNameForAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
}: GetChannelByNameForAppQueryVariables): Promise<GetChannelByNameForAppQuery> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetChannelByNameForAppQuery, GetChannelByNameForAppQueryVariables>(
|
||||
gql`
|
||||
query GetChannelByNameForApp($appId: String!, $channelName: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannelByName(name: $channelName) {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
branchMapping
|
||||
updateBranches(offset: 0, limit: 1000) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
platform
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, channelName },
|
||||
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: {
|
||||
branchMapping: string;
|
||||
updateBranches: {
|
||||
@@ -192,7 +138,7 @@ export function logChannelDetails(channel: {
|
||||
}
|
||||
|
||||
export default class ChannelView extends EasCommand {
|
||||
static description = 'View a channel on the current project.';
|
||||
static description = 'view a channel';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -214,6 +160,9 @@ export default class ChannelView extends EasCommand {
|
||||
args: { name: channelName },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(ChannelView);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -232,30 +181,28 @@ export default class ChannelView extends EasCommand {
|
||||
}));
|
||||
}
|
||||
|
||||
const getUpdateChannelByNameForAppresult = await getUpdateChannelByNameForAppAsync({
|
||||
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
channelName,
|
||||
});
|
||||
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not find a channel with name: ${channelName}`);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(channel));
|
||||
return;
|
||||
printJsonOnlyOutput(channel);
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,19 @@ import { getProjectConfigDescription } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { appPlatformEmojis } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { selectAsync } from '../prompts';
|
||||
import { handleDeprecatedEasJsonAsync } from './build';
|
||||
|
||||
export default class Config extends EasCommand {
|
||||
static description = 'show the eas.json config';
|
||||
static description = 'display project configuration (app.json + eas.json)';
|
||||
|
||||
static flags = {
|
||||
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
@@ -58,12 +61,15 @@ export default class Config extends EasCommand {
|
||||
const profile = await reader.getBuildProfileAsync(platform, profileName);
|
||||
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
|
||||
|
||||
Log.log(getProjectConfigDescription(projectDir));
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(config, null, 2));
|
||||
Log.newLine();
|
||||
Log.newLine();
|
||||
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
const platformEmoji = appPlatformEmojis[appPlatform];
|
||||
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
|
||||
export default class Credentials extends EasCommand {
|
||||
static description = 'manage your credentials';
|
||||
static description = 'manage credentials';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await new SelectPlatform().runAsync();
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import { Device, DeviceStatus } from '@expo/apple-utils';
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
|
||||
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
|
||||
import {
|
||||
AppleDeviceQuery,
|
||||
AppleDeviceQueryResult,
|
||||
AppleDevicesByTeamIdentifierQueryResult,
|
||||
} from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
|
||||
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
|
||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||
import formatDevice from '../../devices/utils/formatDevice';
|
||||
import { AppleDevice, Maybe } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class DeviceDelete extends EasCommand {
|
||||
static description = 'remove a registered device from your account';
|
||||
|
||||
static flags = {
|
||||
'apple-team-id': Flags.string(),
|
||||
udid: Flags.string({ multiple: true }),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
|
||||
} = await this.parse(DeviceDelete);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!appleTeamIdentifier) {
|
||||
appleTeamIdentifier = await this.askForAppleTeamAsync(accountName);
|
||||
}
|
||||
|
||||
assert(appleTeamIdentifier, 'No team identifier is specified');
|
||||
|
||||
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
|
||||
|
||||
if (!appleDevicesResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { appleTeamName, appleDevices } = appleDevicesResult;
|
||||
|
||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.newLine();
|
||||
Log.warn('No devices were chosen to be removed.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||
|
||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
|
||||
|
||||
if (!hasRemoved) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
|
||||
}
|
||||
|
||||
async askAndDisableOnAppleAsync(
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
appleTeamIdentifier: string
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
const deleteOnApple = await toggleConfirmAsync({
|
||||
message: 'Do you want to disable these devices on your Apple account as well?',
|
||||
});
|
||||
|
||||
if (!deleteOnApple) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
|
||||
const context = getRequestContext(ctx);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
const removeAppleSpinner = ora('Disabling devices on Apple').start();
|
||||
try {
|
||||
let realDevices = await Device.getAllIOSProfileDevicesAsync(context);
|
||||
realDevices = realDevices.filter(d =>
|
||||
chosenDevices.map(cd => cd.identifier).includes(d.attributes.udid)
|
||||
);
|
||||
|
||||
for (const device of realDevices) {
|
||||
await device.updateAsync({ status: DeviceStatus.DISABLED });
|
||||
}
|
||||
|
||||
removeAppleSpinner.succeed('Disabled devices on Apple');
|
||||
} catch (err) {
|
||||
removeAppleSpinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async askAndRemoveFromExpoAsync(
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
|
||||
): Promise<boolean> {
|
||||
Log.warn(
|
||||
`You are about to remove the Apple device${
|
||||
chosenDevices.length > 1 ? 's' : ''
|
||||
} listed above from your Expo account.`
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
|
||||
try {
|
||||
for (const chosenDevice of chosenDevices) {
|
||||
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
|
||||
}
|
||||
removalSpinner.succeed('Removed Apple devices from Expo');
|
||||
} catch (err) {
|
||||
removalSpinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
logChosenDevices(
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
appleTeamName: Maybe<string> | undefined,
|
||||
appleTeamIdentifier: string
|
||||
): void {
|
||||
Log.addNewLineIfNone();
|
||||
chosenDevices.forEach(device => {
|
||||
Log.log(
|
||||
formatDevice(device, {
|
||||
appleTeamName,
|
||||
appleTeamIdentifier: appleTeamIdentifier!,
|
||||
})
|
||||
);
|
||||
Log.newLine();
|
||||
});
|
||||
}
|
||||
|
||||
async chooseDevicesToDeleteAsync(
|
||||
appleDevices: AppleDeviceQueryResult[],
|
||||
udids: string[]
|
||||
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
|
||||
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
|
||||
Log.newLine();
|
||||
if (udids) {
|
||||
udids.forEach(udid => {
|
||||
const foundDevice = appleDevices.find(device => device.identifier === udid);
|
||||
if (foundDevice) {
|
||||
chosenDevices.push(foundDevice);
|
||||
} else {
|
||||
Log.warn(`No device found with UDID ${udid}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.addNewLineIfNone();
|
||||
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
return chosenDevices;
|
||||
}
|
||||
|
||||
async getDevicesForTeamAsync(
|
||||
accountName: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
|
||||
const devicesSpinner = ora().start('Fetching the list of devices for the team…');
|
||||
|
||||
try {
|
||||
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
|
||||
accountName,
|
||||
appleTeamIdentifier
|
||||
);
|
||||
|
||||
if (result?.appleDevices.length) {
|
||||
const { appleTeamName, appleDevices } = result;
|
||||
|
||||
devicesSpinner.succeed(
|
||||
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
|
||||
);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
devicesSpinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
devicesSpinner.fail(`Something went wrong and we couldn't fetch the device list`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
|
||||
const teamSpinner = ora().start('Fetching the list of teams for the project…');
|
||||
|
||||
try {
|
||||
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
|
||||
|
||||
if (teams.length > 0) {
|
||||
teamSpinner.succeed('Fetched the list of teams for the project');
|
||||
|
||||
if (teams.length === 1) {
|
||||
return teams[0].appleTeamIdentifier;
|
||||
}
|
||||
|
||||
const result = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'appleTeamIdentifier',
|
||||
message: 'What Apple Team would you like to list devices for?',
|
||||
choices: teams.map(team => ({
|
||||
title: team.appleTeamName
|
||||
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
|
||||
: team.appleTeamIdentifier,
|
||||
value: team.appleTeamIdentifier,
|
||||
})),
|
||||
});
|
||||
|
||||
return result.appleTeamIdentifier;
|
||||
} else {
|
||||
teamSpinner.fail(`Couldn't find any teams for the account ${accountName}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
teamSpinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export default class BuildList extends EasCommand {
|
||||
try {
|
||||
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
|
||||
|
||||
if (teams.length) {
|
||||
if (teams.length > 0) {
|
||||
spinner.succeed();
|
||||
|
||||
if (teams.length === 1) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
export default class Diagnostics extends EasCommand {
|
||||
static description = 'log environment info to the console';
|
||||
static description = 'display environment info';
|
||||
|
||||
protected requiresAuthentication = false;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getActorDisplayName } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class EnvironmentSecretCreate extends EasCommand {
|
||||
static description = 'Create an environment secret on the current project or owner account.';
|
||||
static description = 'create an environment secret on the current project or owner account';
|
||||
|
||||
static flags = {
|
||||
scope: Flags.enum({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
@@ -18,8 +17,7 @@ import {
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class EnvironmentSecretDelete extends EasCommand {
|
||||
static description = `Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
|
||||
static description = 'delete an environment secret by ID';
|
||||
|
||||
static flags = {
|
||||
id: Flags.string({
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '../../project/projectUtils';
|
||||
|
||||
export default class EnvironmentSecretList extends EasCommand {
|
||||
static description = 'Lists environment secrets available for your current app';
|
||||
static description = 'list environment secrets available for your current app';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
|
||||
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log, { link } from '../log';
|
||||
import Log from '../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
@@ -41,8 +41,7 @@ interface CommandFlags {
|
||||
}
|
||||
|
||||
export default class Submit extends EasCommand {
|
||||
static description = `Submit build archive to App Store Connect
|
||||
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
|
||||
static description = 'submit app binary to App Store and/or Play Store';
|
||||
static aliases = ['build:submit'];
|
||||
|
||||
static flags = {
|
||||
|
||||
@@ -1,32 +1,180 @@
|
||||
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
|
||||
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
|
||||
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0';
|
||||
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
|
||||
|
||||
async function configureProjectForEASUpdateAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
isBare: boolean
|
||||
): Promise<void> {
|
||||
export default class UpdateConfigure extends EasCommand {
|
||||
static description = 'configure the project to support EAS Update';
|
||||
|
||||
static flags = {
|
||||
platform: Flags.enum({
|
||||
description: 'Platform to configure',
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
default: 'all',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
const { flags } = await this.parse(UpdateConfigure);
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
});
|
||||
|
||||
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
}
|
||||
|
||||
const [androidWorkflow, iosWorkflow] = await Promise.all([
|
||||
resolveWorkflowAsync(projectDir, Platform.ANDROID),
|
||||
resolveWorkflowAsync(projectDir, Platform.IOS),
|
||||
]);
|
||||
|
||||
const updatedExp = await configureAppJSONForEASUpdateAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
platform,
|
||||
workflows: {
|
||||
android: androidWorkflow,
|
||||
ios: iosWorkflow,
|
||||
},
|
||||
});
|
||||
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
|
||||
|
||||
// configure native files for EAS Update
|
||||
if (
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
|
||||
androidWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp);
|
||||
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
|
||||
}
|
||||
if (
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
|
||||
iosWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp);
|
||||
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Your app is configured to run EAS Update!`);
|
||||
}
|
||||
}
|
||||
|
||||
async function configureAppJSONForEASUpdateAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
platform,
|
||||
workflows,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
platform: RequestedPlatform;
|
||||
workflows: {
|
||||
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
|
||||
};
|
||||
}): Promise<ExpoConfig> {
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const easUpdateURL = getEASUpdateURL(projectId);
|
||||
const preexistingRuntimeVersion = exp.runtimeVersion;
|
||||
const defaultRuntimeVersion = isBare
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
runtimeVersion: preexistingRuntimeVersion ?? defaultRuntimeVersion,
|
||||
updates: { ...exp.updates, url: easUpdateURL },
|
||||
});
|
||||
const updates = { ...exp.updates, url: easUpdateURL };
|
||||
|
||||
const androidDefaultRuntimeVersion =
|
||||
workflows['android'] === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
const iosDefaultRuntimeVersion =
|
||||
workflows['ios'] === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
|
||||
const newAndroidRuntimeVersion =
|
||||
exp.android?.runtimeVersion ?? exp.runtimeVersion ?? androidDefaultRuntimeVersion;
|
||||
const newIosRuntimeVersion =
|
||||
exp.ios?.runtimeVersion ?? exp.runtimeVersion ?? iosDefaultRuntimeVersion;
|
||||
|
||||
let newConfig: Partial<ExpoConfig>;
|
||||
switch (platform) {
|
||||
case RequestedPlatform.All: {
|
||||
if (isRuntimeEqual(newAndroidRuntimeVersion, newIosRuntimeVersion)) {
|
||||
newConfig = {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: undefined,
|
||||
},
|
||||
ios: { ...exp.ios, runtimeVersion: undefined },
|
||||
updates,
|
||||
};
|
||||
} else {
|
||||
newConfig = {
|
||||
runtimeVersion: undefined, // top level runtime is redundant if it is specified in both android and ios
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
ios: {
|
||||
...exp.ios,
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestedPlatform.Android: {
|
||||
newConfig = {
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case RequestedPlatform.Ios: {
|
||||
newConfig = {
|
||||
ios: {
|
||||
...exp.ios,
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await modifyConfigAsync(projectDir, newConfig);
|
||||
|
||||
const prexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
|
||||
const prexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
if (exp.updates?.url) {
|
||||
@@ -38,12 +186,27 @@ async function configureProjectForEASUpdateAsync(
|
||||
} else {
|
||||
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
|
||||
}
|
||||
if (!preexistingRuntimeVersion) {
|
||||
|
||||
if (
|
||||
!prexistingAndroidRuntimeVersion &&
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform)
|
||||
) {
|
||||
Log.withTick(
|
||||
`Set runtimeVersion to "${JSON.stringify(defaultRuntimeVersion)}" in app.json`
|
||||
`Set ${appPlatformDisplayNames[AppPlatform.Android]} runtimeVersion to "${JSON.stringify(
|
||||
newConfig.android?.runtimeVersion ?? newConfig.runtimeVersion
|
||||
)}" in app.json`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!prexistingIosRuntimeVersion &&
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)
|
||||
) {
|
||||
Log.withTick(
|
||||
`Set ${appPlatformDisplayNames[AppPlatform.Ios]} runtimeVersion to "${JSON.stringify(
|
||||
newConfig.ios?.runtimeVersion ?? newConfig.runtimeVersion
|
||||
)}" in app.json`
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -57,11 +220,15 @@ async function configureProjectForEASUpdateAsync(
|
||||
'https://expo.fyi/eas-update-config.md'
|
||||
)}\n`
|
||||
);
|
||||
Log.log(
|
||||
chalk.bold(
|
||||
`{\n updates": {\n "url": "${easUpdateURL}"\n },\n "runtimeVersion": {\n "policy": "sdkVersion"\n }\n}`
|
||||
)
|
||||
);
|
||||
Log.log(chalk.bold(JSON.stringify(newConfig, null, 2)));
|
||||
Log.addNewLineIfNone();
|
||||
if (workflows['android'] === Workflow.GENERIC || workflows['ios'] === Workflow.GENERIC) {
|
||||
Log.warn(
|
||||
`You will also have to manually edit the projects ${chalk.bold(
|
||||
'Expo.plist/AndroidManifest.xml'
|
||||
)}. ${learnMore('https://expo.fyi/eas-update-config.md#native-configuration')}`
|
||||
);
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
@@ -70,40 +237,20 @@ async function configureProjectForEASUpdateAsync(
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
assert(result.config, 'A successful result should have a config');
|
||||
|
||||
return result.config.expo;
|
||||
}
|
||||
|
||||
export default class UpdateConfigure extends EasCommand {
|
||||
static description = 'Configure the project to support EAS Update.';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
});
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const isBare = hasAndroidNativeProject || hasIosNativeProject;
|
||||
|
||||
await configureProjectForEASUpdateAsync(projectDir, exp, isBare);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
if (isBare) {
|
||||
Log.log(
|
||||
`🧐 It seems you are on the bare workflow! Please also update your native files. You can do this by either running ${chalk.bold(
|
||||
'eas build:configure'
|
||||
)} or manually editing Expo.plist/AndroidManifest.xml. ${learnMore(
|
||||
'https://expo.fyi/eas-update-config.md#native-configuration'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
Log.log(`🎉 Your app is configured to run EAS Update!`);
|
||||
}
|
||||
function isRuntimeEqual(
|
||||
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' },
|
||||
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' }
|
||||
): boolean {
|
||||
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
|
||||
return runtimeVersionA === runtimeVersionB;
|
||||
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
|
||||
return runtimeVersionA.policy === runtimeVersionB.policy;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
async function deleteUpdateGroupAsync({
|
||||
group,
|
||||
@@ -35,7 +36,7 @@ async function deleteUpdateGroupAsync({
|
||||
}
|
||||
|
||||
export default class UpdateDelete extends EasCommand {
|
||||
static description = 'Delete all the updates in an update Group.';
|
||||
static description = 'delete all the updates in an update group';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -58,15 +59,17 @@ export default class UpdateDelete extends EasCommand {
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(UpdateDelete);
|
||||
|
||||
if (!jsonFlag) {
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
} else {
|
||||
const shouldAbort = await confirmAsync({
|
||||
message:
|
||||
`🚨${chalk.red('CAUTION')}🚨\n\n` +
|
||||
`${chalk.yellow(`This will delete all of the updates in group "${group}".`)} ${chalk.red(
|
||||
'This is a permanent operation.'
|
||||
)}\n\n` +
|
||||
`If you want to revert to a previous publish, you should use 'branch:publish --republish' targeted at the last working update group instead.\n\n` +
|
||||
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'branch:publish --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
|
||||
`If you want to revert to a previous publish, you should use 'update --republish' targeted at the last working update group instead.\n\n` +
|
||||
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'update --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
|
||||
`Would you like to abort?`,
|
||||
});
|
||||
|
||||
@@ -79,10 +82,9 @@ export default class UpdateDelete extends EasCommand {
|
||||
await deleteUpdateGroupAsync({ group });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify({ group }));
|
||||
return;
|
||||
printJsonOnlyOutput({ group });
|
||||
} else {
|
||||
Log.withTick(`Deleted update group ${group}`);
|
||||
}
|
||||
|
||||
Log.withTick(`Deleted update group ${group}`);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,38 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import fetch from '../../fetch';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetUpdateGroupAsyncQuery,
|
||||
PublishUpdateGroupInput,
|
||||
Robot,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
UpdatePublishMutation,
|
||||
User,
|
||||
ViewBranchQuery,
|
||||
ViewBranchUpdatesQuery,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
@@ -31,14 +41,20 @@ import {
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getManifestBodyAsync,
|
||||
signManifestBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
import { listBranchesAsync } from '../branch/list';
|
||||
import { viewUpdateBranchAsync } from '../branch/view';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
@@ -109,11 +125,11 @@ async function ensureBranchExistsAsync({
|
||||
}): Promise<{
|
||||
id: string;
|
||||
updates: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'];
|
||||
}> {
|
||||
const { app } = await viewUpdateBranchAsync({
|
||||
const { app } = await UpdateQuery.viewBranchAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
@@ -131,7 +147,7 @@ async function ensureBranchExistsAsync({
|
||||
}
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
static description = 'Publish an update group.';
|
||||
static description = 'publish an update group';
|
||||
|
||||
static flags = {
|
||||
branch: Flags.string({
|
||||
@@ -174,6 +190,14 @@ export default class UpdatePublish extends EasCommand {
|
||||
'Use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
'private-key-path': Flags.string({
|
||||
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
|
||||
required: false,
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -188,8 +212,14 @@ export default class UpdatePublish extends EasCommand {
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
platform,
|
||||
'private-key-path': privateKeyPath,
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
} = await this.parse(UpdatePublish);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const platformFlag = platform as PlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
@@ -200,6 +230,39 @@ export default class UpdatePublish extends EasCommand {
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const { exp: expPrivate } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: false,
|
||||
});
|
||||
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
|
||||
if (!hasExpoUpdates && nonInteractive) {
|
||||
Errors.error(
|
||||
`${chalk.bold(
|
||||
'expo-updates'
|
||||
)} must already be installed when executing in non-interactive mode`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasExpoUpdates) {
|
||||
const install = await confirmAsync({
|
||||
message: `You are creating an update which requires ${chalk.bold(
|
||||
'expo-updates'
|
||||
)} to be installed in your app.\n Do you want EAS CLI to install it for you?`,
|
||||
instructions: 'The command will abort unless you agree.',
|
||||
});
|
||||
if (install) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
} else {
|
||||
Errors.error(`Install ${chalk.bold('expo-updates')} manually and come back later.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
await checkEASUpdateURLIsSetAsync(exp);
|
||||
@@ -211,6 +274,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
@@ -260,6 +327,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
if (group) {
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
|
||||
// Drop into interactive mode if the user has not specified an update group to republish.
|
||||
if (jsonFlag) {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
@@ -324,6 +395,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
|
||||
if (!message) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
@@ -342,6 +417,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
@@ -388,30 +467,76 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
|
||||
])
|
||||
);
|
||||
|
||||
if (republish && !oldRuntimeVersion) {
|
||||
throw new Error(
|
||||
'Can not find the runtime version of the update group that is being republished.'
|
||||
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
|
||||
([runtime, platforms]) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
|
||||
])
|
||||
);
|
||||
|
||||
if (republish && !oldRuntimeVersion) {
|
||||
throw new Error(
|
||||
'Cannot find the runtime version of the update group that is being republished.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
message,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
return {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
message,
|
||||
};
|
||||
});
|
||||
let newUpdates;
|
||||
);
|
||||
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
|
||||
|
||||
if (codeSigningInfo) {
|
||||
Log.log('🔒 Signing updates');
|
||||
|
||||
const updatesTemp = [...newUpdates];
|
||||
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
|
||||
const newUpdates = updatesTemp.splice(0, Object.keys(updateGroup.updateInfoGroup).length);
|
||||
return {
|
||||
updateGroup,
|
||||
newUpdates,
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
updateGroupsAndTheirUpdates.map(async ({ updateGroup, newUpdates }) => {
|
||||
await Promise.all(
|
||||
newUpdates.map(async newUpdate => {
|
||||
const response = await fetch(newUpdate.manifestPermalink, {
|
||||
method: 'GET',
|
||||
headers: { accept: 'multipart/mixed' },
|
||||
});
|
||||
const manifestBody = nullthrows(await getManifestBodyAsync(response));
|
||||
|
||||
checkManifestBodyAgainstUpdateInfoGroup(
|
||||
manifestBody,
|
||||
nullthrows(
|
||||
updateGroup.updateInfoGroup[newUpdate.platform as keyof UpdateInfoGroup]
|
||||
)
|
||||
);
|
||||
|
||||
const manifestSignature = signManifestBody(manifestBody, codeSigningInfo);
|
||||
|
||||
await PublishMutation.setCodeSigningInfoAsync(newUpdate.id, {
|
||||
alg: codeSigningInfo.codeSigningMetadata.alg,
|
||||
keyid: codeSigningInfo.codeSigningMetadata.keyid,
|
||||
sig: manifestSignature,
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to published updates');
|
||||
@@ -419,7 +544,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newUpdates));
|
||||
printJsonOnlyOutput(newUpdates);
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -464,19 +589,27 @@ async function getRuntimeVersionObjectAsync(
|
||||
if (isPolicy) {
|
||||
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error('Runtime version policies are only supported in the managed workflow.');
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${platform}`
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
update: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'][number]
|
||||
): string {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ViewAllUpdatesQuery } from '../../graphql/generated';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { FormatUpdateParameter, UPDATE_COLUMNS, formatUpdate } from '../../update/utils';
|
||||
import groupBy from '../../utils/expodash/groupBy';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
type UpdateGroupDescription = FormatUpdateParameter & {
|
||||
branch: string;
|
||||
group: string;
|
||||
platforms: string;
|
||||
runtimeVersion: string;
|
||||
};
|
||||
|
||||
export default class BranchView extends EasCommand {
|
||||
static description = 'view the recent updates for a branch';
|
||||
|
||||
static flags = {
|
||||
branch: Flags.string({
|
||||
description: 'List all updates on this branch',
|
||||
exclusive: ['all'],
|
||||
}),
|
||||
all: Flags.boolean({
|
||||
description: 'List all updates associated with this project',
|
||||
exclusive: ['branch'],
|
||||
default: false,
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: `Return a json with all of the recent update groups.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { branch: branchFlag, all, json: jsonFlag },
|
||||
} = await this.parse(BranchView);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
let updateGroupDescriptions: UpdateGroupDescription[];
|
||||
if (all) {
|
||||
const branchesAndUpdates = await UpdateQuery.viewAllAsync({ appId: projectId });
|
||||
updateGroupDescriptions = getUpdateGroupDescriptions(
|
||||
branchesAndUpdates.app.byId.updateBranches
|
||||
);
|
||||
} else {
|
||||
let branchInteractive: string | undefined;
|
||||
if (!branchFlag) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: branchInteractive } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the branch whose updates you wish to view:',
|
||||
initial: (await getVcsClient().getBranchNameAsync()) ?? undefined,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
const branch = branchFlag ?? branchInteractive;
|
||||
assert(branch, 'Branch name may not be empty.');
|
||||
|
||||
const branchesAndUpdates = await UpdateQuery.viewBranchAsync({
|
||||
appId: projectId,
|
||||
name: branch,
|
||||
});
|
||||
const UpdateBranch = branchesAndUpdates.app?.byId.updateBranchByName;
|
||||
if (!UpdateBranch) {
|
||||
throw new Error(`Could not find branch "${branch}"`);
|
||||
}
|
||||
updateGroupDescriptions = getUpdateGroupDescriptions([UpdateBranch]);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(updateGroupDescriptions);
|
||||
} else {
|
||||
logAsTable(updateGroupDescriptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdateGroupDescriptions(
|
||||
branchesAndUpdates: ViewAllUpdatesQuery['app']['byId']['updateBranches']
|
||||
): UpdateGroupDescription[] {
|
||||
const flattenedBranchesAndUpdates = branchesAndUpdates.flatMap(branch =>
|
||||
branch.updates.map(update => {
|
||||
return { branch: branch.name, ...update };
|
||||
})
|
||||
);
|
||||
const updateGroupDescriptions = Object.values(
|
||||
groupBy(flattenedBranchesAndUpdates, update => update.group)
|
||||
).map(updateGroup => {
|
||||
const platforms = updateGroup
|
||||
.map(update => update.platform)
|
||||
.sort()
|
||||
.join(', ');
|
||||
return { ...updateGroup[0], platforms };
|
||||
});
|
||||
updateGroupDescriptions.sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
return updateGroupDescriptions;
|
||||
}
|
||||
|
||||
function logAsTable(updateGroupDescriptions: UpdateGroupDescription[]): void {
|
||||
const table = new CliTable({
|
||||
head: ['Branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
table.push(
|
||||
...updateGroupDescriptions.map(updateGroupDescription => [
|
||||
updateGroupDescription.branch,
|
||||
formatUpdate(updateGroupDescription),
|
||||
updateGroupDescription.runtimeVersion,
|
||||
updateGroupDescription.group,
|
||||
updateGroupDescription.platforms,
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Recently published update groups:'));
|
||||
Log.log(table.toString());
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export async function viewUpdateAsync({
|
||||
groupId,
|
||||
@@ -50,7 +51,7 @@ export async function viewUpdateAsync({
|
||||
return data;
|
||||
}
|
||||
export default class UpdateView extends EasCommand {
|
||||
static description = 'Update group details.';
|
||||
static description = 'update group details';
|
||||
|
||||
static args = [
|
||||
{
|
||||
@@ -72,30 +73,32 @@ export default class UpdateView extends EasCommand {
|
||||
args: { groupId },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(UpdateView);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const { updatesByGroup } = await viewUpdateAsync({ groupId });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(updatesByGroup));
|
||||
return;
|
||||
printJsonOnlyOutput(updatesByGroup);
|
||||
} else {
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
getPlatformsForGroup({
|
||||
updates: updatesByGroup,
|
||||
group: updatesByGroup[0]?.group,
|
||||
}),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
getPlatformsForGroup({
|
||||
updates: updatesByGroup,
|
||||
group: updatesByGroup[0]?.group,
|
||||
}),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
|
||||
import { prepareInputParamsAsync } from '../../webhooks/input';
|
||||
|
||||
export default class WebhookCreate extends EasCommand {
|
||||
static description = 'Create a webhook on the current project.';
|
||||
static description = 'create a webhook';
|
||||
|
||||
static flags = {
|
||||
event: Flags.enum({
|
||||
|
||||
@@ -14,7 +14,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookDelete extends EasCommand {
|
||||
static description = 'Delete a webhook on the current project.';
|
||||
static description = 'delete a webhook';
|
||||
|
||||
static args = [
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookList extends EasCommand {
|
||||
static description = 'List webhooks on the current project.';
|
||||
static description = 'list webhooks';
|
||||
|
||||
static flags = {
|
||||
event: Flags.enum({
|
||||
|
||||
@@ -9,7 +9,7 @@ import pick from '../../utils/expodash/pick';
|
||||
import { prepareInputParamsAsync } from '../../webhooks/input';
|
||||
|
||||
export default class WebhookUpdate extends EasCommand {
|
||||
static description = 'Create a webhook on the current project.';
|
||||
static description = 'update a webhook';
|
||||
|
||||
static flags = {
|
||||
id: Flags.string({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ora } from '../../ora';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookView extends EasCommand {
|
||||
static description = 'View a webhook on the current project.';
|
||||
static description = 'view a webhook';
|
||||
|
||||
static args = [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AppStoreApi from '../ios/appstore/AppStoreApi';
|
||||
import { AuthCtx } from '../ios/appstore/authenticate';
|
||||
import { AuthCtx } from '../ios/appstore/authenticateTypes';
|
||||
|
||||
export const testAuthCtx: AuthCtx = {
|
||||
appleId: 'test-apple-id',
|
||||
|
||||
@@ -76,7 +76,9 @@ export const testPushKey: ApplePushKeyFragment = {
|
||||
iosAppCredentialsList: [],
|
||||
};
|
||||
|
||||
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
|
||||
export const testTargets: Target[] = [
|
||||
{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com', entitlements: {} },
|
||||
];
|
||||
|
||||
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
|
||||
id: 'test-prov-prof-id-1',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { CredentialsContext } from '../../context';
|
||||
@@ -41,14 +44,33 @@ export class CreateGoogleServiceAccountKey {
|
||||
return detectedPath;
|
||||
}
|
||||
|
||||
const { keyJsonPath } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'keyJsonPath',
|
||||
message: 'Path to Google Service Account Key JSON file:',
|
||||
validate: (value: string) => value.length > 0 || "Path can't be empty",
|
||||
Log.log(
|
||||
`${chalk.bold(
|
||||
'A Google Service Account JSON key is required to upload your app to Google Play Store'
|
||||
)}.\n` +
|
||||
`If you're not sure what this is or how to create one, ${learnMore(
|
||||
'https://expo.fyi/creating-google-service-account',
|
||||
{ learnMoreMessage: 'learn more' }
|
||||
)}`
|
||||
);
|
||||
const { filePath } = await promptAsync({
|
||||
name: 'filePath',
|
||||
message: 'Path to Google Service Account file:',
|
||||
initial: 'api-0000000000000000000-111111-aaaaaabbbbbb.json',
|
||||
type: 'text',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async (filePath: string) => {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isFile()) {
|
||||
return true;
|
||||
}
|
||||
return 'Input is not a file.';
|
||||
} catch {
|
||||
return 'File does not exist.';
|
||||
}
|
||||
},
|
||||
]);
|
||||
return keyJsonPath;
|
||||
});
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
|
||||
describe('BuildCredentialsUtils', () => {
|
||||
describe(canCopyLegacyCredentialsAsync, () => {
|
||||
@@ -84,7 +83,9 @@ describe('BuildCredentialsUtils', () => {
|
||||
});
|
||||
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
|
||||
it('copies all legacy credentials to EAS if the user is eligible', async () => {
|
||||
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockImplementation(async () => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateFcm } from '../CreateFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
|
||||
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
|
||||
|
||||
describe(CreateFcm, () => {
|
||||
it('creates an fcm api key in Interactive Mode', async () => {
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
@@ -8,8 +7,8 @@ import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey'
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('fs');
|
||||
asMock(promptAsync).mockImplementation(() => ({
|
||||
keyJsonPath: '/google-service-account-key.json',
|
||||
jest.mocked(promptAsync).mockImplementation(async () => ({
|
||||
filePath: '/google-service-account-key.json',
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
@@ -15,7 +14,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
beforeEach(() => {
|
||||
asMock(generateRandomKeystoreAsync).mockReset();
|
||||
jest.mocked(generateRandomKeystoreAsync).mockReset();
|
||||
});
|
||||
it('creates a keystore in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
@@ -28,7 +27,7 @@ describe('CreateKeystore', () => {
|
||||
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
|
||||
});
|
||||
it('creates a keystore by uploading', async () => {
|
||||
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
|
||||
jest.mocked(askForUserProvidedAsync).mockImplementationOnce(async () => testKeystore);
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { RemoveFcm } from '../RemoveFcm';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
|
||||
describe(RemoveFcm, () => {
|
||||
it('removes an FCM API Key', async () => {
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -8,7 +7,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { RemoveGoogleServiceAccountKey } from '../RemoveGoogleServiceAccountKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
|
||||
describe(RemoveGoogleServiceAccountKey, () => {
|
||||
it('removes an Google Service Account Key', async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
@@ -7,7 +6,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
|
||||
describe(RemoveKeystore, () => {
|
||||
it('removes a keystore', async () => {
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
@@ -19,7 +18,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
|
||||
|
||||
describe('SetUpBuildCredentials', () => {
|
||||
beforeEach(() => {
|
||||
asMock(generateRandomKeystoreAsync).mockReset();
|
||||
jest.mocked(generateRandomKeystoreAsync).mockReset();
|
||||
});
|
||||
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
|
||||
+6
-6
@@ -1,6 +1,5 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { AndroidKeystoreType } from '../../../../graphql/generated';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import {
|
||||
@@ -13,13 +12,14 @@ import {
|
||||
SelectAndroidBuildCredentials,
|
||||
SelectAndroidBuildCredentialsResultType,
|
||||
} from '../../../manager/SelectAndroidBuildCredentials';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(confirmAsync).mockImplementation(() => true);
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
jest.mock('../../../manager/SelectAndroidBuildCredentials');
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -28,7 +28,7 @@ beforeEach(() => {
|
||||
|
||||
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
|
||||
it('sets up a new build configuration from credentials.json upon user request', async () => {
|
||||
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
|
||||
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
|
||||
return {
|
||||
runAsync: () => {
|
||||
return {
|
||||
@@ -37,7 +37,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
@@ -78,7 +78,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
|
||||
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('uses an existing build configuration upon user request', async () => {
|
||||
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
|
||||
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
|
||||
return {
|
||||
runAsync: () => {
|
||||
return {
|
||||
@@ -87,7 +87,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -14,8 +13,8 @@ import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('fs');
|
||||
asMock(promptAsync).mockImplementation(() => ({
|
||||
keyJsonPath: '/google-service-account-key.json',
|
||||
jest.mocked(promptAsync).mockImplementation(async () => ({
|
||||
filePath: '/google-service-account-key.json',
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
+3
-2
@@ -1,4 +1,3 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
@@ -9,7 +8,9 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockImplementation(async () => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
|
||||
|
||||
describe(UseExistingGoogleServiceAccountKey, () => {
|
||||
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
|
||||
|
||||
+6
-7
@@ -1,6 +1,5 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import { detectGoogleServiceAccountKeyPathAsync } from '../googleServiceAccountKey';
|
||||
|
||||
@@ -25,13 +24,13 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
asMock(promptAsync).mockClear();
|
||||
asMock(confirmAsync).mockClear();
|
||||
jest.mocked(promptAsync).mockClear();
|
||||
jest.mocked(confirmAsync).mockClear();
|
||||
});
|
||||
|
||||
describe('Google Service Account Key path detection', () => {
|
||||
it('detects a single google-services file and prompts for confirmation', async () => {
|
||||
asMock(confirmAsync).mockResolvedValueOnce(true);
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
|
||||
expect(confirmAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: 'Would you like to use this file?' })
|
||||
@@ -46,7 +45,7 @@ describe('Google Service Account Key path detection', () => {
|
||||
});
|
||||
|
||||
it('returns null, when user rejects to use detected file', async () => {
|
||||
asMock(confirmAsync).mockResolvedValueOnce(false);
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
expect(confirmAsync).toHaveBeenCalledWith(
|
||||
@@ -56,7 +55,7 @@ describe('Google Service Account Key path detection', () => {
|
||||
});
|
||||
|
||||
it('displays a chooser, when multiple files are found', async () => {
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
jest.mocked(promptAsync).mockResolvedValueOnce({
|
||||
selectedPath: '/project_dir/another-service-account.json',
|
||||
});
|
||||
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
|
||||
@@ -76,7 +75,7 @@ describe('Google Service Account Key path detection', () => {
|
||||
});
|
||||
|
||||
it('returns null, when user selects the "None of above"', async () => {
|
||||
asMock(promptAsync).mockResolvedValueOnce({
|
||||
jest.mocked(promptAsync).mockResolvedValueOnce({
|
||||
selectedPath: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import Log from '../../../../log';
|
||||
import {
|
||||
testAndroidAppCredentialsFragment,
|
||||
@@ -25,7 +24,9 @@ describe('print credentials', () => {
|
||||
appLookupParams,
|
||||
appCredentials: testAndroidAppCredentialsFragment,
|
||||
});
|
||||
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
|
||||
const loggedSoFar = jest
|
||||
.mocked(Log.log)
|
||||
.mock.calls.reduce((acc, mockValue) => acc + mockValue.toString(), '');
|
||||
expect(loggedSoFar).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ function fileIsServiceAccountKey(keyJsonPath: string): boolean {
|
||||
try {
|
||||
readAndValidateServiceAccountKey(keyJsonPath);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,11 @@ import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
// (dsokal) We actually want to use node-fetch but the change is in progress.
|
||||
// See https://github.com/expo/eas-cli/issues/32 for context.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import fetch from 'node-fetch';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Analytics, BuildEvent } from '../../../analytics/events';
|
||||
import fetch from '../../../fetch';
|
||||
import { AndroidKeystoreType } from '../../../graphql/generated';
|
||||
import { KeystoreGenerationUrlMutation } from '../../../graphql/mutations/KeystoreGenerationUrlMutation';
|
||||
import Log from '../../../log';
|
||||
@@ -27,7 +24,7 @@ export async function keytoolCommandExistsAsync(): Promise<boolean> {
|
||||
try {
|
||||
await spawnAsync('keytool');
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -149,7 +146,7 @@ async function createKeystoreInCloudAsync(
|
||||
body: JSON.stringify(keystoreParams),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const result: KeystoreServiceResult = await response.json();
|
||||
const result = (await response.json()) as KeystoreServiceResult;
|
||||
spinner.succeed();
|
||||
return {
|
||||
type: AndroidKeystoreType.Jks,
|
||||
|
||||
@@ -24,7 +24,7 @@ function isPKCSKeystore(keystore: Keystore): boolean {
|
||||
try {
|
||||
parsePKCS12(keystore.keystore, keystore.keystorePassword);
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ function isJKSKeystore(keystore: Keystore): boolean {
|
||||
try {
|
||||
jks.parseJks(Buffer.from(keystore.keystore, 'base64'), keystore.keystorePassword);
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Actor, getActorDisplayName } from '../user/User';
|
||||
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
|
||||
import * as IosGraphqlClient from './ios/api/GraphqlClient';
|
||||
import AppStoreApi from './ios/appstore/AppStoreApi';
|
||||
import { AuthenticationMode } from './ios/appstore/authenticateTypes';
|
||||
|
||||
export class CredentialsContext {
|
||||
public readonly android = AndroidGraphqlClient;
|
||||
@@ -49,7 +50,7 @@ export class CredentialsContext {
|
||||
): ExpoConfig | null {
|
||||
try {
|
||||
return getExpoConfig(projectDir, { env });
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// ignore error, context might be created outside of expo project
|
||||
return null;
|
||||
}
|
||||
@@ -99,6 +100,11 @@ export class CredentialsContext {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.appStore.defaultAuthenticationMode === AuthenticationMode.API_KEY) {
|
||||
await this.appStore.ensureAuthenticatedAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'If you provide your Apple account credentials we will be able to generate all necessary build credentials and fully validate them.'
|
||||
@@ -106,7 +112,7 @@ export class CredentialsContext {
|
||||
);
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'This is optional, but without Apple account access you will need to provide all the values manually and we can only run minimal validation on them.'
|
||||
'This is optional, but without Apple account access you will need to provide all the missing values manually and we can only run minimal validation on them.'
|
||||
)
|
||||
);
|
||||
const confirm = await confirmAsync({
|
||||
@@ -117,7 +123,7 @@ export class CredentialsContext {
|
||||
} else {
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'No problem! 👌 If you select an action that requires those credentials we will ask you again about it.'
|
||||
'No problem! 👌 If any of the next steps will require Apple account access we will ask you again about it.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { IosDistributionType } from '../../../graphql/generated';
|
||||
import {
|
||||
testKeystore,
|
||||
@@ -25,8 +24,8 @@ jest.mock('prompts');
|
||||
describe('update credentials.json', () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
jest.mocked(prompts).mockReset();
|
||||
jest.mocked(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
@@ -176,6 +175,7 @@ describe('update credentials.json', () => {
|
||||
{
|
||||
targetName: 'testapp',
|
||||
bundleIdentifier: 'com.bundle.id',
|
||||
entitlements: {},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Loaded 100 of 230 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user