mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc9b8442d7 | ||
|
|
a84762b00d | ||
|
|
2d117e1d7e | ||
|
|
986674942d | ||
|
|
13c224589b | ||
|
|
20b091fbfa | ||
|
|
168b6afdfa | ||
|
|
3c6320b929 | ||
|
|
4f1dc582bd | ||
|
|
daf2ef1e30 | ||
|
|
f703bd8b5a | ||
|
|
997cd7058a | ||
|
|
e896c2f6c5 | ||
|
|
3d723ca426 | ||
|
|
c17e1b3fa2 | ||
|
|
d70fc36485 | ||
|
|
47784ca6d3 | ||
|
|
3e5a3aca27 | ||
|
|
89b0227594 | ||
|
|
b98a30e7c6 | ||
|
|
5c7862a685 | ||
|
|
82833a9eb4 | ||
|
|
66171c3c4b | ||
|
|
179def91c7 | ||
|
|
9f63ad4527 | ||
|
|
878028271a | ||
|
|
d4e15bbcbf | ||
|
|
13bb056df1 | ||
|
|
14d56904d2 | ||
|
|
c2ffd05763 | ||
|
|
46e394fbef | ||
|
|
2613f0c2f9 | ||
|
|
e5fd7c01f4 | ||
|
|
73a0dd6e23 | ||
|
|
a9dc7f9eeb | ||
|
|
c3836f23d1 | ||
|
|
bfd73cbb19 | ||
|
|
6a1753edaa | ||
|
|
558a73086d | ||
|
|
15966fbd26 | ||
|
|
1e8b55383e | ||
|
|
6920ebb0ba | ||
|
|
55f35949e1 | ||
|
|
ccb998a221 | ||
|
|
30bc27b4c4 | ||
|
|
da3c95ccaf | ||
|
|
bbcbe1dceb | ||
|
|
f15beea454 | ||
|
|
16e384be76 | ||
|
|
e74e6d8fc5 | ||
|
|
b6f4603107 | ||
|
|
0e7d7f765a | ||
|
|
33b4d8d719 | ||
|
|
0d765748e3 | ||
|
|
93953f1424 | ||
|
|
e9b67db05c | ||
|
|
09bf912b9e | ||
|
|
e701bacdaa | ||
|
|
cc22542031 | ||
|
|
6c53262b5b | ||
|
|
7d7d4995d8 | ||
|
|
977546c8c9 | ||
|
|
53dcd4cb43 | ||
|
|
9e18e738eb | ||
|
|
252c7187d5 | ||
|
|
90852b9557 | ||
|
|
0df16236a2 | ||
|
|
ecaf5a4318 | ||
|
|
86e5e1a7b1 | ||
|
|
ab10094071 | ||
|
|
2d536d73fe | ||
|
|
80ea8ee50b | ||
|
|
b49c5f47d1 | ||
|
|
a9da78946b | ||
|
|
b6d9accea4 | ||
|
|
4611a65bce | ||
|
|
db29b3fc18 | ||
|
|
2534a46bd3 | ||
|
|
7def4b502e | ||
|
|
4ea26d7ca3 | ||
|
|
c4c8075b1f | ||
|
|
266545d2a0 | ||
|
|
ddd7046513 | ||
|
|
7a44eeb22b | ||
|
|
5bcf8263cf | ||
|
|
f380ba0106 | ||
|
|
1a6d81f68e | ||
|
|
522818113b | ||
|
|
f93d347665 | ||
|
|
e96cee1c3b | ||
|
|
4d46e0a8ac | ||
|
|
80e35c7524 | ||
|
|
186c26bc99 | ||
|
|
be6a7f54f9 | ||
|
|
fff7aac5b6 | ||
|
|
0d90fbd3ff | ||
|
|
e71779049e | ||
|
|
7da909e3c1 | ||
|
|
e439fd14a9 | ||
|
|
ffed1b446a | ||
|
|
7b65412ec6 | ||
|
|
f1cbdcc094 | ||
|
|
ec5573efa6 | ||
|
|
242da62271 | ||
|
|
23d1221ef9 | ||
|
|
5402d8c74f | ||
|
|
176026645c | ||
|
|
b0d4ed6c0e | ||
|
|
2f7ac71921 | ||
|
|
b599315a85 | ||
|
|
e86d8aa77a | ||
|
|
ab4ac87d52 | ||
|
|
af98fb1621 | ||
|
|
0ce7c1b3b4 | ||
|
|
2bdaa0d428 | ||
|
|
2486a4de97 | ||
|
|
6137b215e9 | ||
|
|
265ba16594 | ||
|
|
a7bc3d0c98 | ||
|
|
ee618880d7 | ||
|
|
2c95c8c310 | ||
|
|
b90e37d9fc | ||
|
|
34563070f4 | ||
|
|
66b269f4bf | ||
|
|
168cb4d4a0 | ||
|
|
91b531da43 | ||
|
|
2e0d567361 | ||
|
|
81e6280cff | ||
|
|
e2fda7b9d3 | ||
|
|
ede30807f5 | ||
|
|
9f354013fa | ||
|
|
30552f134c | ||
|
|
e4beba9a21 | ||
|
|
b71ab7ec84 | ||
|
|
596cfc372b | ||
|
|
d5f6c5680c | ||
|
|
28c508ff62 | ||
|
|
97ff6f406d | ||
|
|
bd39b296fa | ||
|
|
bfbe1e930b | ||
|
|
3fe45176cc | ||
|
|
bffa6fbf03 | ||
|
|
161808bc37 | ||
|
|
ac42b15acd | ||
|
|
47d21de4eb | ||
|
|
e12ffa9ebc | ||
|
|
2944023986 | ||
|
|
49c1e7670d | ||
|
|
b157b901e3 | ||
|
|
1a1f88f202 | ||
|
|
d02d70da91 | ||
|
|
c37f2957bb | ||
|
|
a32f5fe57a | ||
|
|
b7f6d36a63 | ||
|
|
2975a025ac | ||
|
|
f4e396dde2 | ||
|
|
ef54911323 | ||
|
|
18b1074232 | ||
|
|
8468e0c5ea | ||
|
|
436d5c9ec9 | ||
|
|
1f88f7e1a3 | ||
|
|
09682754f8 | ||
|
|
8e3d84b1e5 | ||
|
|
8bb2631192 | ||
|
|
38a7fb6266 | ||
|
|
0ac2fb77a1 | ||
|
|
f12774b539 | ||
|
|
d50bdedcec | ||
|
|
8d443e5992 | ||
|
|
2b5fb6de53 | ||
|
|
dc86f27199 | ||
|
|
d97ed80867 | ||
|
|
3601b448fe | ||
|
|
68aab13d51 | ||
|
|
6cef13d756 | ||
|
|
1d45941f5d | ||
|
|
5e5badf11f | ||
|
|
840c65946b | ||
|
|
fdbd786126 | ||
|
|
d5365275e0 | ||
|
|
886e48d3ee | ||
|
|
342c78a4f6 | ||
|
|
a483d94e26 | ||
|
|
b40412a7a1 | ||
|
|
d652e04f7b | ||
|
|
85326d1104 | ||
|
|
28c087138b | ||
|
|
8badf14ec3 | ||
|
|
4f1db29ef8 | ||
|
|
2354dc1b71 | ||
|
|
f106bfa6d7 | ||
|
|
1a7edbea06 | ||
|
|
5bfaee9e4d | ||
|
|
0cb2309d7d | ||
|
|
0415a1d97f | ||
|
|
249c60f4d1 | ||
|
|
c357cc378c | ||
|
|
f7c0d731b6 | ||
|
|
e852e3edac | ||
|
|
2a283b74a3 | ||
|
|
c749bc2372 | ||
|
|
9c60f7190e | ||
|
|
8126b2276e | ||
|
|
e1de21015f | ||
|
|
61e505f1f8 | ||
|
|
2e3611161e | ||
|
|
95f5eaaacf | ||
|
|
87a3ea99e2 | ||
|
|
ddab65a55d | ||
|
|
0cf8f8ce0d | ||
|
|
2b531ec70c | ||
|
|
6cd7966904 | ||
|
|
ad72ab990f | ||
|
|
b53412fc00 | ||
|
|
cbb2de9e11 | ||
|
|
593c42f601 | ||
|
|
84b10b9dad | ||
|
|
0f286061e4 | ||
|
|
c8aad01480 | ||
|
|
d70a6bee69 | ||
|
|
67799b5c54 |
No files matched your search
@@ -10,6 +10,7 @@ module.exports = {
|
||||
jasmine: false,
|
||||
},
|
||||
rules: {
|
||||
'no-console':'warn',
|
||||
'no-constant-condition': ['warn', { checkLoops: false }],
|
||||
'sort-imports': [
|
||||
'warn',
|
||||
@@ -18,6 +19,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
'curly': 'warn',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
@@ -0,0 +1 @@
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -3,6 +3,7 @@
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
|
||||
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
|
||||
|
||||
# Why
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@ tmp
|
||||
node_modules
|
||||
coverage
|
||||
.history
|
||||
.DS_Store
|
||||
+275
-2
@@ -6,6 +6,281 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix build profile schema validation when `simulator` key is present for iOS ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
|
||||
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
|
||||
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
|
||||
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix iOS capability syncing on build. ([#521](https://github.com/expo/eas-cli/pull/521) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
|
||||
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix runtime version checks. ([#495](https://github.com/expo/eas-cli/pull/495) by [@dsokal](https://github.com/dsokal))
|
||||
- Resolve `--android-package` correctly in `eas submit` command. ([#494](https://github.com/expo/eas-cli/pull/494) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.19.1](https://github.com/expo/eas-cli/releases/tag/v0.19.1) - 2021-07-02
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve project workflow detection (fixes the case where the `android` and/or `ios` directories are git-ignored). ([#490](https://github.com/expo/eas-cli/pull/490) by [@dsokal](https://github.com/dsokal))
|
||||
- Improve credentials workflow with project creation and current working directories ([#491](https://github.com/expo/eas-cli/pull/491) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Better debugging experience ([#492](https://github.com/expo/eas-cli/pull/492) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.19.0](https://github.com/expo/eas-cli/releases/tag/v0.19.0) - 2021-07-01
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto-suggest application id and bundle identifier when running `eas build:configure` for a managed project. ([#487](https://github.com/expo/eas-cli/pull/487) by [@dsokal](https://github.com/dsokal))
|
||||
- Support configuring one platform at a time when running `eas build`. ([#483](https://github.com/expo/eas-cli/pull/483) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.3](https://github.com/expo/eas-cli/releases/tag/v0.18.3) - 2021-06-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Push Key setup integrated with ios build command ([#473](https://github.com/expo/eas-cli/pull/473) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix android builds for PKCS12 keystores. ([#484](https://github.com/expo/eas-cli/pull/484) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.18.2](https://github.com/expo/eas-cli/releases/tag/v0.18.2) - 2021-06-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Change default iOS image for projects with Expo SDK <= 41 (SDK 41 won't build with Xcode 12.5). ([#479](https://github.com/expo/eas-cli/pull/479) by [@dsokal](https://github.com/dsokal))
|
||||
- Make Apple team optional in appropriate cases. ([#468](https://github.com/expo/eas-cli/pull/468) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Show projects that depend on a push key. ([#472](https://github.com/expo/eas-cli/pull/472) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix inverted conditional so we actually only prompt about commit when index is dirty. ([#481](https://github.com/expo/eas-cli/pull/481) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.1](https://github.com/expo/eas-cli/releases/tag/v0.18.1) - 2021-06-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add basic printing support for Apple App Specific Password. ([#474](https://github.com/expo/eas-cli/pull/474) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix CLI UI getting blocked on credentials migration, leading to partially migrated state. ([#477](https://github.com/expo/eas-cli/pull/477) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.0](https://github.com/expo/eas-cli/releases/tag/v0.18.0) - 2021-06-24
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for `experimental.npmToken` in credentials.json, EAS Secrets can be used instead. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove `--allow-experimental` flag from `eas build:configure` as it has no effect now. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make credentials manager work with multi-target iOS projects. ([#441](https://github.com/expo/eas-cli/pull/441) by [@dsokal](https://github.com/dsokal))
|
||||
- Copy over credentials from Expo Classic to EAS. ([#445](https://github.com/expo/eas-cli/pull/445) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add Big Sur image for iOS builds. ([#457](https://github.com/expo/eas-cli/pull/457) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
|
||||
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
|
||||
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
|
||||
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.17.0](https://github.com/expo/eas-cli/releases/tag/v0.17.0) - 2021-06-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix bundle identifier resolution when native target is not provided. ([#434](https://github.com/expo/eas-cli/pull/434) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix git repo root path getter on Windows. ([#429](https://github.com/expo/eas-cli/pull/429) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix resolving Android application identifier. ([#431](https://github.com/expo/eas-cli/pull/431) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Opt out of capability syncing with `EXPO_NO_CAPABILITY_SYNC=1`. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add more verbose logging around capability syncing to help debug reported issues. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add managed credentials support for multi-target iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.1](https://github.com/expo/eas-cli/releases/tag/v0.15.1) - 2021-05-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support auto capabilities in managed workflow using Expo config plugin introspection. ([#419](https://github.com/expo/eas-cli/pull/419) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix sending `appBuildVersion` as part of the build metadata. ([#423](https://github.com/expo/eas-cli/pull/423) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.0](https://github.com/expo/eas-cli/releases/tag/v0.15.0) - 2021-05-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
|
||||
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support empty passwords in Android and iOS keystores. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add more build metadata - `appBuildVersion`. ([#413](https://github.com/expo/eas-cli/pull/413) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
|
||||
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
|
||||
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
|
||||
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make the default urql requestPolicy 'network-only'. ([#397](https://github.com/expo/eas-cli/pull/397) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
## [0.14.0](https://github.com/expo/eas-cli/releases/tag/v0.14.0) - 2021-05-10
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Make eas branch:publish work with expo-cli >= 4.4.3 ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
|
||||
- Create asset keys without an extension. ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Display platforms supported by an update group ([#391](https://github.com/expo/eas-cli/pull/391) by [@jkhales](https://github.com/jkhales))
|
||||
- Add a --platform flag to branch:publish command ([#389](https://github.com/expo/eas-cli/pull/389) by [@jkhales](https://github.com/jkhales))
|
||||
- Release new credentials manager. ([#363](https://github.com/expo/eas-cli/pull/363) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add QR code to install internal distribution build on device. ([#371](https://github.com/expo/eas-cli/pull/371) by [@axeldelafosse](https://github.com/axeldelafosse))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.13.0](https://github.com/expo/eas-cli/releases/tag/v0.13.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.12.0](https://github.com/expo/eas-cli/releases/tag/v0.12.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add the initiating user's username to the build metadata. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `--clear-cache` flag for `eas build`. ([#355](https://github.com/expo/eas-cli/pull/355) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where the account name was used as the username. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.11.1](https://github.com/expo/eas-cli/releases/tag/v0.11.1) - 2021-04-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Resolve ios distribution types. ([#348](https://github.com/expo/eas-cli/pull/348) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump version of `@expo/apple-utils` to fix capabilities sync immediately after initial identifier registration. ([593c4](https://github.com/expo/eas-cli/commit/593c42f601396deba1751489343ec07b0a8876da) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.11.0](https://github.com/expo/eas-cli/releases/tag/v0.11.0) - 2021-04-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `"credentialsSource": "auto"` is now deprecated. The option will use `"remote"` as the default value. ([#345](https://github.com/expo/eas-cli/pull/345) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
@@ -17,8 +292,6 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Fix the bug where all Android managed builds produced AAB archives. The `buildType` property from `eas.json` is not ignored now. ([#349](https://github.com/expo/eas-cli/pull/349) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
+31
-2
@@ -2,9 +2,38 @@
|
||||
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag like `v1.0.0`.
|
||||
1. **Wait until the `release` GitHub workflow has finished.**
|
||||
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
- Remember to copy the changelog entries from `CHANGELOG.md`.
|
||||

|
||||
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
|
||||
|
||||
- This command works only if `HEAD` is on the release commit, e.g. when `git log -1` prints:
|
||||
|
||||
```
|
||||
commit cbb2de9e118bfbcb5a597b261d2dbef0bb5d4b0b (tag: v0.11.1)
|
||||
Author: Brent Vatne <[email protected]>
|
||||
Date: Tue Apr 20 15:00:15 2021 -0700
|
||||
|
||||
v0.11.1
|
||||
```
|
||||
|
||||
- If you run `yarn lerna publish from-git` on any other commit, it'll print something like:
|
||||
|
||||
```
|
||||
lerna notice from-git No tagged release found
|
||||
lerna success No changed packages to publish
|
||||
```
|
||||
|
||||
- If you ever run into the above issue, use `git checkout RELEASE_COMMIT_HASH` to checkout to the release commit. Later, when you've published the packages to npm, run `git checkout main` to go back to the latest commit.
|
||||
|
||||
1. Update `CHANGELOG.md`.
|
||||
- Add the new section with the release version in the title.
|
||||
- Leave the `main` section empty (without any changelog entries).
|
||||
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
- Remember to copy the changelog entries from `CHANGELOG.md`.
|
||||
- Before hitting the submit button, make sure there are 5 assets available under the release entry.
|
||||

|
||||
1. If any of the changelog entries have been tagged with `[EAS BUILD API]`
|
||||
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
|
||||
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commit/) for that version. In most cases, it will be the latest `Publish` commit.
|
||||
- Find the `eas-cli-local-build-plugin` version published in that commit.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.11.0"
|
||||
"version": "0.22.4"
|
||||
}
|
||||
+4
-4
@@ -17,12 +17,12 @@
|
||||
"devDependencies": {
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "^26.0.13",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-universe": "^4.0.0",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"eslint": "7.31.0",
|
||||
"eslint-config-universe": "7.0.1",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"jest": "^26.4.2",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
"lerna": "^3.22.1",
|
||||
"prettier": "^2.0.5"
|
||||
"prettier": "2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
graphql.schema.json linguist-generated
|
||||
src/graphql/generated.ts linguist-generated
|
||||
+77
-25
@@ -42,11 +42,14 @@ eas --help COMMAND
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
@@ -69,7 +72,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -83,7 +86,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -97,7 +100,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -108,7 +111,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -120,15 +123,15 @@ USAGE
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--clear-cache Clear cache before the build
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
--skip-credentials-check Skip validation of build credentials
|
||||
--skip-project-configuration Skip project configuration
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -139,7 +142,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -151,10 +154,9 @@ USAGE
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -165,12 +167,21 @@ USAGE
|
||||
$ eas build:list
|
||||
|
||||
OPTIONS
|
||||
--appBuildVersion=appBuildVersion
|
||||
--appIdentifier=appIdentifier
|
||||
--appVersion=appVersion
|
||||
--buildProfile=buildProfile
|
||||
--channel=channel
|
||||
--distribution=(store|internal|simulator)
|
||||
--gitCommitHash=gitCommitHash
|
||||
--limit=limit
|
||||
--platform=(all|android|ios)
|
||||
--status=(in-queue|in-progress|errored|finished|canceled)
|
||||
--runtimeVersion=runtimeVersion
|
||||
--sdkVersion=sdkVersion
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -181,7 +192,22 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
Show the eas.json config
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas config
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios)
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -192,7 +218,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -203,7 +229,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -217,7 +243,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -228,7 +254,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -247,6 +273,31 @@ OPTIONS
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
information about the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
create or link an EAS project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:init
|
||||
|
||||
ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
@@ -256,12 +307,13 @@ USAGE
|
||||
$ eas secret:create
|
||||
|
||||
OPTIONS
|
||||
--force Delete and recreate existing secrets
|
||||
--name=name Name of the secret
|
||||
--scope=(account|project) [default: project] Scope for the secret
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -278,7 +330,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -289,7 +341,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
@@ -369,7 +421,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -388,7 +440,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -402,7 +454,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -416,7 +468,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -436,7 +488,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -450,5 +502,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.11.0/build/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,2 +1,9 @@
|
||||
import { fs } from 'memfs';
|
||||
|
||||
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
|
||||
fs.mkdirSync('/tmp');
|
||||
if (process.env.TMPDIR) {
|
||||
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
|
||||
}
|
||||
|
||||
module.exports = fs;
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
};
|
||||
@@ -3,6 +3,7 @@ schema: 'https://staging.exp.host/--/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
|
||||
Generated
+2951
-834
File diff suppressed because it is too large.
Load diff
@@ -2,5 +2,6 @@ module.exports = {
|
||||
...require('../../jest/jest.config.js'),
|
||||
rootDir: __dirname,
|
||||
roots: ['src', '__mocks__'],
|
||||
globalSetup: './global-setup.ts',
|
||||
globalSetup: './jest/global-setup.ts',
|
||||
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
process.env.EAS_NO_VCS = '1';
|
||||
process.env.EAS_PROJECT_ROOT = '/app';
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
@@ -1,69 +1,74 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.11.0",
|
||||
"version": "0.22.4",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@amplitude/identify": "^1.5.0",
|
||||
"@amplitude/node": "^1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.17",
|
||||
"@expo/config": "~3.3.19",
|
||||
"@expo/config-plugins": "1.0.24",
|
||||
"@expo/eas-build-job": "0.2.22",
|
||||
"@expo/eas-json": "^0.11.0",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/sdk-runtime-versions": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/errors": "^1.3.3",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"@urql/core": "^2.0.0",
|
||||
"@urql/exchange-retry": "^0.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-table3": "^0.6.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"env-paths": "^2.2.0",
|
||||
"fast-glob": "^3.2.4",
|
||||
"figures": "^3.2.0",
|
||||
"form-data": "^3.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"getenv": "^1.0.0",
|
||||
"got": "^11.5.2",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"keychain": "^1.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"mime": "^2.4.7",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"ora": "^5.1.0",
|
||||
"pkg-dir": "^4.2.0",
|
||||
"progress": "^2.0.3",
|
||||
"prompts": "^2.3.2",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"semver": "^7.3.4",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"tar": "^6.0.5",
|
||||
"tempy": "^0.7.0",
|
||||
"terminal-link": "^2.1.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"tslib": "^1",
|
||||
"turndown": "^7.0.0",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^8.3.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
"@amplitude/identify": "1.7.0",
|
||||
"@amplitude/node": "1.7.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.24",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "3.0.3",
|
||||
"@expo/eas-build-job": "0.2.45",
|
||||
"@expo/eas-json": "^0.22.3",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@hapi/joi": "17.1.1",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/errors": "1.3.3",
|
||||
"@oclif/plugin-help": "3.2.0",
|
||||
"@urql/core": "2.0.0",
|
||||
"@urql/exchange-retry": "0.2.0",
|
||||
"chalk": "4.1.0",
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "3.0.3",
|
||||
"env-paths": "2.2.0",
|
||||
"fast-glob": "3.2.7",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "3.0.0",
|
||||
"fs-extra": "9.0.1",
|
||||
"getenv": "1.0.0",
|
||||
"got": "11.5.2",
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"ignore": "5.1.8",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.0",
|
||||
"keychain": "1.3.0",
|
||||
"lodash": "4.17.20",
|
||||
"mime": "2.4.7",
|
||||
"minimatch": "3.0.4",
|
||||
"nanoid": "3.1.23",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-forge": "0.10.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"progress": "2.0.3",
|
||||
"prompts": "2.3.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.4",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.0.5",
|
||||
"tempy": "0.7.0",
|
||||
"terminal-link": "2.1.1",
|
||||
"timeago.js": "4.0.2",
|
||||
"tslib": "2.3.0",
|
||||
"turndown": "7.0.0",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
@@ -74,7 +79,6 @@
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/color-string": "^1.5.0",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
@@ -88,18 +92,15 @@
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/wrap-ansi": "^3.0.0",
|
||||
"@vercel/ncc": "^0.23.0",
|
||||
"axios": "^0.21.0",
|
||||
"axios": "0.21.1",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"mockdate": "^3.0.2",
|
||||
"nock": "^13.0.5",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.9.7",
|
||||
"wonka": "^4.0.14"
|
||||
"typescript": "4.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -126,15 +127,27 @@
|
||||
],
|
||||
"helpClass": "./build/help",
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage your account"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
},
|
||||
"channel": {
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage your Apple devices for internal distribution"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage your project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
},
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
@@ -163,13 +176,14 @@
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme && node patch-readme",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && git add README.md",
|
||||
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
},
|
||||
"types": "build/index.d.ts"
|
||||
"types": "build/index.d.ts",
|
||||
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// Patch `oclif-dev readme` path and link generation
|
||||
let readmeContent = fs.readFileSync('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
|
||||
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
|
||||
fs.writeFileSync('README.md', readmeContent);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Patched README path generation');
|
||||
@@ -36,7 +36,7 @@ export async function initAsync(): Promise<void> {
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? 'cdebbc678931403439486c4750781544'
|
||||
: '4ac443afd5073c0df6169291db1d3495';
|
||||
client = Amplitude.init(apiKey);
|
||||
client = Amplitude.init(apiKey, { retryClass: new Amplitude.OfflineRetryHandler(apiKey) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@ function getContext() {
|
||||
os_version: os.release(),
|
||||
device_brand: platform,
|
||||
device_model: platform,
|
||||
app_version: packageJSON?.version,
|
||||
source_version: packageJSON?.version,
|
||||
source: 'eas cli',
|
||||
};
|
||||
}
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
@@ -10,24 +10,20 @@ import { ensureValidVersions } from '../utils/updates';
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
|
||||
projectDir,
|
||||
buildGradleContents
|
||||
);
|
||||
const updatedBuildGradleContents =
|
||||
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
|
||||
projectDir,
|
||||
buildGradleContents
|
||||
);
|
||||
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
|
||||
@@ -52,10 +48,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
let androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
let androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
|
||||
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
|
||||
@@ -70,7 +63,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
@@ -78,13 +71,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
|
||||
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
@@ -93,13 +80,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.RELEASE_CHANNEL
|
||||
@@ -108,3 +89,27 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
describe(readChannelSafelyAsync, () => {
|
||||
it('retrieves the channel when it is defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./android/app/src/main/AndroidManifest.xml': AndroidManifestWithChannel,
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBe(testChannel);
|
||||
});
|
||||
it('returns null if the channel is not defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./android/app/src/main/AndroidManifest.xml': AndroidManifestNoChannel,
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const AndroidManifestWithChannel = `
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value='{"expo-channel-name":"${testChannel}"}'/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
`;
|
||||
const AndroidManifestNoChannel = `
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
`;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readVersionCodeAsync, readVersionNameAsync } from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
});
|
||||
|
||||
describe(readVersionCodeAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version code from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const versionCode = await readVersionCodeAsync('/repo', exp);
|
||||
expect(versionCode).toBe(123);
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version code from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const versionCode = await readVersionCodeAsync('/repo', exp);
|
||||
expect(versionCode).toBe(123);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readVersionNameAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version name from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const versionName = await readVersionNameAsync('/repo', exp);
|
||||
expect(versionName).toBe('1.0');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const versionName = await readVersionNameAsync('/repo', exp);
|
||||
expect(versionName).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode 123
|
||||
versionName "1.0"
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
return fakeExp;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureAppIdentifierIsDefinedAsync,
|
||||
getAndroidApplicationIdAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { Platform } from '../types';
|
||||
|
||||
enum ApplicationIdSource {
|
||||
AndroidProject,
|
||||
AppJson,
|
||||
}
|
||||
|
||||
function isApplicationIdValid(applicationId: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
|
||||
}
|
||||
|
||||
export async function ensureApplicationIdIsValidAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const applicationId = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform: Platform.ANDROID,
|
||||
exp,
|
||||
});
|
||||
if (!isApplicationIdValid(applicationId)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
Log.error(
|
||||
`Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
|
||||
);
|
||||
Log.error(`Update "android.package" in ${configDescription} and run this command again.`);
|
||||
throw new Error('Invalid applicationId');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureApplicationIdAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
allowExperimental: boolean
|
||||
): Promise<void> {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
const applicationIdFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
|
||||
|
||||
if (applicationIdFromAndroidProject && applicationIdFromConfig) {
|
||||
if (applicationIdFromConfig !== applicationIdFromAndroidProject) {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`We detected that your Android project is configured with a different application id than the one defined in ${configDescription}.`
|
||||
);
|
||||
const hasApplicationIdInStaticConfig = await hasApplicationIdInStaticConfigAsync(
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!hasApplicationIdInStaticConfig) {
|
||||
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
|
||||
However, if you choose the one defined in the Android project you'll have to update ${configDescription} on your own.`);
|
||||
}
|
||||
Log.newLine();
|
||||
const { applicationIdSource } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'applicationIdSource',
|
||||
message: 'Which application id should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `${chalk.bold(
|
||||
applicationIdFromAndroidProject
|
||||
)} - In build.gradle in Android project`,
|
||||
value: ApplicationIdSource.AndroidProject,
|
||||
},
|
||||
{
|
||||
title: `${chalk.bold(applicationIdFromConfig)} - In your ${configDescription}`,
|
||||
value: ApplicationIdSource.AppJson,
|
||||
},
|
||||
],
|
||||
});
|
||||
switch (applicationIdSource) {
|
||||
case ApplicationIdSource.AppJson: {
|
||||
await updateApplicationIdInBuildGradleAsync(projectDir, exp);
|
||||
break;
|
||||
}
|
||||
case ApplicationIdSource.AndroidProject: {
|
||||
if (hasApplicationIdInStaticConfig) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
|
||||
} else {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!applicationIdFromAndroidProject && !applicationIdFromConfig) {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
} else if (applicationIdFromAndroidProject && !applicationIdFromConfig) {
|
||||
if (getConfigFilePaths(projectDir).staticConfigPath) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
|
||||
} else {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
}
|
||||
} else if (!applicationIdFromAndroidProject && applicationIdFromConfig) {
|
||||
// This should never happen, adding warning just in case
|
||||
Log.warn(
|
||||
'applicationId is not specified in your ./android/app/build.gradle file. Make sure your project is configured correctly before building.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (allowExperimental) {
|
||||
// this step is optional, the Play store is using applicationId value from build.gradle
|
||||
// to identify the app, so package name does not need to be updated
|
||||
await updatePackageNameAsync(projectDir, exp);
|
||||
await gitAddAsync(path.join(projectDir, 'android'), { intentToAdd: true });
|
||||
}
|
||||
}
|
||||
|
||||
function missingPackageMessage(configDescription: string): string {
|
||||
return `Please define "android.package" in ${configDescription} and run "eas build:configure" again.`;
|
||||
}
|
||||
|
||||
async function updateAppJsonConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
newApplicationId: string
|
||||
): Promise<void> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic configs");
|
||||
|
||||
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
...rawStaticConfig.expo,
|
||||
android: { ...rawStaticConfig.expo?.android, package: newApplicationId },
|
||||
};
|
||||
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
|
||||
|
||||
exp.android = { ...exp.android, package: newApplicationId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static config exists and if expo.android.package is defined there.
|
||||
* It will return false if the value in static config is different than "android.package" in ExpoConfig
|
||||
*/
|
||||
async function hasApplicationIdInStaticConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<boolean> {
|
||||
if (!exp.android?.package) {
|
||||
return false;
|
||||
}
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
return false;
|
||||
}
|
||||
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
|
||||
return rawStaticConfig?.expo?.android?.package === exp.android.package;
|
||||
}
|
||||
|
||||
async function updateApplicationIdInBuildGradleAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const updatedBuildGradleContents = AndroidConfig.Package.setPackageInBuildGradle(
|
||||
exp,
|
||||
buildGradleContents
|
||||
);
|
||||
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
|
||||
}
|
||||
|
||||
async function updatePackageNameAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
await AndroidConfig.Package.renamePackageOnDisk(exp, projectDir);
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const updatedAndroidManifest = AndroidConfig.Package.setPackageInAndroidManifest(
|
||||
exp,
|
||||
androidManifest
|
||||
);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(
|
||||
androidManifestPath,
|
||||
updatedAndroidManifest
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
@@ -10,44 +8,31 @@ import AndroidCredentialsProvider, {
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.ANDROID>({
|
||||
commandCtx,
|
||||
platform: Platform.ANDROID,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (
|
||||
buildProfile.workflow === Workflow.GENERIC &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
|
||||
) {
|
||||
throw new Error(
|
||||
`"android" directory not found. If you're trying to build a managed project, set ${chalk.bold(
|
||||
`builds.android.${commandCtx.profile}.workflow`
|
||||
)} in "eas.json" to "managed".`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
buildProfile.workflow === Workflow.GENERIC &&
|
||||
buildProfile.distribution === 'internal' &&
|
||||
buildProfile.gradleCommand?.match(/bundle/)
|
||||
) {
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
|
||||
@@ -63,17 +48,19 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
}
|
||||
}
|
||||
|
||||
await ensureApplicationIdIsValidAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ctx,
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
if (ctx.workflow === Workflow.GENERIC) {
|
||||
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
},
|
||||
getMetadataContext: () => ({}),
|
||||
prepareJobAsync,
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
@@ -81,30 +68,18 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
if (job.type === Workflow.GENERIC) {
|
||||
const graphqlJob = transformGenericJob(job);
|
||||
return await BuildMutation.createAndroidGenericBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
} else {
|
||||
const graphqlJob = transformManagedJob(job);
|
||||
return await BuildMutation.createAndroidManagedBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
}
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
|
||||
return (
|
||||
ctx.buildProfile.workflow === Workflow.MANAGED ||
|
||||
(ctx.buildProfile.workflow === Workflow.GENERIC && !ctx.buildProfile.withoutCredentials)
|
||||
);
|
||||
return !ctx.buildProfile.withoutCredentials;
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
@@ -113,16 +88,21 @@ async function ensureAndroidCredentialsAsync(
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
await createCredentialsContextAsync(ctx.projectDir, {
|
||||
exp: ctx.exp,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: {
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
|
||||
@@ -2,23 +2,27 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getAndroidApplicationIdAsync } from '../../project/projectUtils';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/android/applicationId';
|
||||
import vcs from '../../vcs';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { configureApplicationIdAsync, ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
await configureApplicationIdAsync(ctx.projectDir, ctx.exp, ctx.allowExperimental);
|
||||
await ensureApplicationIdIsValidAsync(ctx.projectDir, ctx.exp);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await gitAddAsync(easGradlePath, { intentToAdd: true });
|
||||
await vcs.trackFileAsync(easGradlePath);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
@@ -30,20 +34,11 @@ export async function validateAndSyncProjectConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
|
||||
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
const isApplicationIdConfigured =
|
||||
applicationIdFromAndroidProject && applicationIdFromAndroidProject === packageNameFromConfig;
|
||||
|
||||
const isEasBuildGradleConfigured = await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(
|
||||
projectDir
|
||||
);
|
||||
if (!isEasBuildGradleConfigured || !isApplicationIdConfigured) {
|
||||
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
|
||||
throw new Error(
|
||||
'Project is not configured. Please run "eas build:configure" to configure the project.'
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
|
||||
@@ -1,44 +1,31 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
AndroidGenericJobInput,
|
||||
AndroidManagedBuildType,
|
||||
AndroidManagedJobInput,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
gradleCommand: job.gradleCommand,
|
||||
artifactPath: job.artifactPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobInput {
|
||||
return {
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
username: job.username,
|
||||
buildType: transformBuildType(job.buildType),
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
|
||||
if (buildType === Android.ManagedBuildType.APK) {
|
||||
return AndroidManagedBuildType.Apk;
|
||||
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
|
||||
return AndroidManagedBuildType.AppBundle;
|
||||
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
|
||||
if (buildType === Android.BuildType.APK) {
|
||||
return AndroidBuildType.Apk;
|
||||
} else if (buildType === Android.BuildType.APP_BUNDLE) {
|
||||
return AndroidBuildType.AppBundle;
|
||||
} else {
|
||||
return AndroidManagedBuildType.DevelopmentClient;
|
||||
return AndroidBuildType.DevelopmentClient;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
|
||||
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
@@ -14,40 +14,20 @@ interface JobData {
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
throw new Error("Unknown workflow. Shouldn't happen");
|
||||
}
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.ANDROID;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: Android.BuilderEnvironment;
|
||||
cache: Cache;
|
||||
secrets: {
|
||||
buildCredentials?: {
|
||||
keystore: Android.Keystore;
|
||||
};
|
||||
environmentSecrets?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
const credentials = jobData.credentials;
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
@@ -61,55 +41,41 @@ async function prepareJobCommonAsync(
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
let buildType = ctx.buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||
buildType = Android.BuildType.APK;
|
||||
}
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
projectRootDirectory,
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
ndk: ctx.buildProfile.ndk,
|
||||
env: ctx.buildProfile.env,
|
||||
image: buildProfile.image ?? 'default',
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
...buildCredentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidGenericBuildProfile
|
||||
): Promise<Partial<Android.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.GENERIC,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidManagedBuildProfile
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.MANAGED,
|
||||
username: accountName,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
username,
|
||||
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
|
||||
export async function readVersionCodeAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<number | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = readBuildGradle(projectDir);
|
||||
const matchResult = buildGradle?.match(/versionCode (.*)/);
|
||||
if (matchResult) {
|
||||
return Number(matchResult[1]);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return AndroidConfig.Version.getVersionCode(exp) ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readVersionNameAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = readBuildGradle(projectDir);
|
||||
const matchResult = buildGradle?.match(/versionName ["'](.*)["']/);
|
||||
if (matchResult) {
|
||||
return matchResult[1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
function readBuildGradle(projectDir: string): string | undefined {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
if (!fs.pathExistsSync(buildGradlePath)) {
|
||||
return undefined;
|
||||
}
|
||||
return fs.readFileSync(buildGradlePath, 'utf8');
|
||||
}
|
||||
@@ -1,118 +1,138 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import ora from 'ora';
|
||||
|
||||
import { UploadSessionType } from '../graphql/generated';
|
||||
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleep } from '../utils/promise';
|
||||
import vcs from '../vcs';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadata } from './metadata';
|
||||
import { MetadataContext, collectMetadata } from './metadata';
|
||||
import { Platform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import {
|
||||
commitChangedFilesAsync,
|
||||
commitPromptAsync,
|
||||
isGitStatusCleanAsync,
|
||||
makeProjectTarballAsync,
|
||||
showDiffAsync,
|
||||
} from './utils/repository';
|
||||
import { commitPromptAsync, makeProjectTarballAsync } from './utils/repository';
|
||||
|
||||
export interface CredentialsResult<Credentials> {
|
||||
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
credentials: Credentials;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration, TJob extends Job> {
|
||||
export interface JobData<Credentials> {
|
||||
credentials?: Credentials;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>;
|
||||
projectConfiguration: ProjectConfiguration;
|
||||
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(
|
||||
ctx: BuildContext<TPlatform>,
|
||||
jobData: {
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
): Promise<Job>;
|
||||
getMetadataContext: () => MetadataContext<TPlatform>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
|
||||
if (!ctx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
|
||||
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${
|
||||
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
|
||||
}`,
|
||||
{
|
||||
nonInteractive: builder.ctx.commandCtx.nonInteractive,
|
||||
}
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
{ nonInteractive: ctx.nonInteractive }
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = builder.ctx.commandCtx.local
|
||||
const projectArchive = ctx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(builder.ctx),
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
const metadataContext = builder.getMetadataContext();
|
||||
const metadata = await collectMetadata(ctx, metadataContext);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (builder.ctx.commandCtx.local) {
|
||||
if (ctx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return sendBuildRequestAsync(builder, job, metadata);
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
if (error?.expoApiV2ErrorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
|
||||
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;
|
||||
}
|
||||
@@ -157,16 +177,11 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
}
|
||||
}
|
||||
|
||||
async function sendBuildRequestAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>,
|
||||
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
): Promise<string> {
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
@@ -175,13 +190,13 @@ async function sendBuildRequestAsync<
|
||||
}
|
||||
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.commandCtx.projectId,
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return build.id;
|
||||
return build;
|
||||
},
|
||||
{
|
||||
successEvent: Event.BUILD_REQUEST_SUCCESS,
|
||||
@@ -223,7 +238,7 @@ async function reviewAndCommitChangesAsync(
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||
await commitChangedFilesAsync(initialCommitMessage);
|
||||
await vcs.commitAsync({ commitMessage: initialCommitMessage, commitAllFiles: false });
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
}
|
||||
@@ -256,10 +271,105 @@ async function reviewAndCommitChangesAsync(
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await vcs.showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||
nonInteractive,
|
||||
askedFirstTime: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const build = nullthrows(builds[0]);
|
||||
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 status: ${builds} - aborting!`);
|
||||
}
|
||||
} 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 unknownState = 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}`),
|
||||
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleep(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitAddAsync } from '../utils/git';
|
||||
import vcs from '../vcs';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { RequestedPlatform } from './types';
|
||||
import {
|
||||
commitPromptAsync,
|
||||
ensureGitRepoExistsAsync,
|
||||
isGitStatusCleanAsync,
|
||||
maybeBailOnGitStatusAsync,
|
||||
showDiffAsync,
|
||||
} from './utils/repository';
|
||||
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
|
||||
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
@@ -25,13 +22,43 @@ const configureCommitMessage = {
|
||||
[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 vcs.hasUncommittedChangesAsync()) {
|
||||
error(
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.io/build/eas-json'
|
||||
)})`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
allowExperimental: boolean;
|
||||
}): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await maybeBailOnGitStatusAsync();
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
@@ -39,14 +66,15 @@ export async function configureAsync(options: {
|
||||
user: await ensureLoggedInAsync(),
|
||||
projectDir: options.projectDir,
|
||||
exp,
|
||||
allowExperimental: options.allowExperimental,
|
||||
requestedPlatform: options.platform,
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
|
||||
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
|
||||
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
|
||||
hasAndroidNativeProject:
|
||||
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
|
||||
hasIosNativeProject:
|
||||
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
|
||||
};
|
||||
|
||||
Log.newLine();
|
||||
@@ -58,7 +86,7 @@ export async function configureAsync(options: {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
|
||||
} else {
|
||||
@@ -67,62 +95,46 @@ export async function configureAsync(options: {
|
||||
}
|
||||
}
|
||||
|
||||
const MANAGED_DEFAULTS = {
|
||||
release: {},
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
|
||||
const GENERIC_DEFAULTS = {
|
||||
release: {},
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
let existingEasJson;
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
|
||||
await reader.validateAsync();
|
||||
const reader = new EasJsonReader(ctx.projectDir);
|
||||
await reader.readAndValidateAsync();
|
||||
|
||||
existingEasJson = await reader.readRawAsync();
|
||||
Log.withTick('Validated eas.json');
|
||||
|
||||
// If we have already populated eas.json with the default fields for the
|
||||
// platform then proceed
|
||||
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
|
||||
return;
|
||||
} else if (
|
||||
ctx.requestedPlatform === 'all' &&
|
||||
existingEasJson.builds.ios &&
|
||||
existingEasJson.builds.android
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldInitIOS =
|
||||
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
|
||||
const shouldInitAndroid =
|
||||
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
|
||||
|
||||
const easJson = {
|
||||
builds: {
|
||||
...existingEasJson,
|
||||
...(shouldInitAndroid
|
||||
? {
|
||||
android: {
|
||||
release: {
|
||||
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
}
|
||||
: null),
|
||||
...(shouldInitIOS
|
||||
? {
|
||||
ios: {
|
||||
release: {
|
||||
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
}
|
||||
: null),
|
||||
},
|
||||
};
|
||||
const easJson =
|
||||
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
|
||||
? { build: GENERIC_DEFAULTS }
|
||||
: { build: MANAGED_DEFAULTS };
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await gitAddAsync(easJsonPath, { intentToAdd: true });
|
||||
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
|
||||
await vcs.trackFileAsync(easJsonPath);
|
||||
Log.withTick('Generated eas.json');
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
@@ -155,7 +167,7 @@ async function reviewAndCommitChangesAsync(
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await vcs.showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,23 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { Platform, RequestedPlatform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
|
||||
export interface CommandContext {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
user: Actor;
|
||||
accountName: string;
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}
|
||||
|
||||
export async function createCommandContextAsync({
|
||||
requestedPlatform,
|
||||
profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive = false,
|
||||
local,
|
||||
skipCredentialsCheck = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
}: {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
|
||||
return {
|
||||
requestedPlatform,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
user,
|
||||
accountName,
|
||||
projectName,
|
||||
exp,
|
||||
nonInteractive,
|
||||
local,
|
||||
skipCredentialsCheck,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
allowExperimental: boolean;
|
||||
requestedPlatform: RequestedPlatform;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
@@ -81,45 +25,119 @@ export interface ConfigureContext {
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
trackingCtx: TrackingContext;
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
exp: ExpoConfig;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
buildProfile: PlatformBuildProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export function createBuildContext<T extends Platform>({
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
easConfig,
|
||||
commandCtx,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
easConfig: EasConfig;
|
||||
commandCtx: CommandContext;
|
||||
}): BuildContext<T> {
|
||||
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
|
||||
if (!buildProfile) {
|
||||
throw new Error(`${platform} build profile does not exist`);
|
||||
}
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: commandCtx.accountName,
|
||||
project_id: commandCtx.projectId,
|
||||
project_type: buildProfile.workflow,
|
||||
account_name: accountName,
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
return {
|
||||
commandCtx,
|
||||
trackingCtx,
|
||||
platform,
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import ora from 'ora';
|
||||
|
||||
import { BuildFragment, BuildStatus } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { sleep } from '../utils/promise';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { CommandContext } from './context';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { Platform, RequestedPlatform } from './types';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureGitRepoExistsAsync, ensureGitStatusIsCleanAsync } from './utils/repository';
|
||||
|
||||
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
if (commandCtx.local) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
Log.newLine();
|
||||
|
||||
if (commandCtx.waitForBuildEnd) {
|
||||
const builds = await waitForBuildEndAsync(scheduledBuilds.map(i => i.buildId));
|
||||
printBuildResults(commandCtx.accountName, builds);
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
}
|
||||
}
|
||||
|
||||
async function startBuildsAsync(
|
||||
commandCtx: CommandContext
|
||||
): Promise<{ platform: Platform; buildId: string }[]> {
|
||||
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.requestedPlatform
|
||||
).readAsync(commandCtx.profile);
|
||||
|
||||
const builds: {
|
||||
platform: Platform;
|
||||
sendBuildRequestAsync: () => Promise<string | undefined>;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildIos) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
|
||||
}
|
||||
return (
|
||||
await Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
)
|
||||
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
|
||||
}
|
||||
|
||||
async function waitForBuildEndAsync(
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 1800, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log('Waiting for build 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) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const build = nullthrows(builds[0]);
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
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 status: ${builds} - aborting!`);
|
||||
}
|
||||
} 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 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 unknownState = builds.length - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
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}`),
|
||||
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleep(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
@@ -27,19 +28,14 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
appIdentifier: metadata.appIdentifier,
|
||||
appName: metadata.appName,
|
||||
appVersion: metadata.appVersion,
|
||||
buildProfile: metadata.buildProfile,
|
||||
cliVersion: metadata.cliVersion,
|
||||
...metadata,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
gitCommitHash: metadata.gitCommitHash,
|
||||
releaseChannel: metadata.releaseChannel,
|
||||
sdkVersion: metadata.sdkVersion,
|
||||
trackingContext: metadata.trackingContext,
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
iosEnterpriseProvisioning:
|
||||
metadata.iosEnterpriseProvisioning &&
|
||||
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,10 +59,20 @@ function transformDistribution(distribution: Metadata['distribution']): Distribu
|
||||
}
|
||||
}
|
||||
|
||||
function transformWorkflow(workflow: Metadata['workflow']): BuildWorkflow {
|
||||
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
return BuildWorkflow.Generic;
|
||||
} else {
|
||||
return BuildWorkflow.Managed;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformIosEnterpriseProvisioning(
|
||||
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
|
||||
): BuildIosEnterpriseProvisioning {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return BuildIosEnterpriseProvisioning.Adhoc;
|
||||
} else {
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
@@ -75,7 +75,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
|
||||
|
||||
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
return await readPlistAsync(expoPlistPath);
|
||||
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
@@ -84,7 +84,7 @@ async function writeExpoPlistAsync(
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await gitAddAsync(expoPlistPath, { intentToAdd: true });
|
||||
await vcs.trackFileAsync(expoPlistPath);
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
@@ -95,3 +95,13 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
describe(readChannelSafelyAsync, () => {
|
||||
it('retrieves the channel when it is defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithChannel,
|
||||
'ios/expo-project/AppDelegate.m': '',
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBe(testChannel);
|
||||
});
|
||||
it('returns null if the channel is not defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithOutChannel,
|
||||
'ios/expo-project/AppDelegate.m': '',
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const ExpoPlistWithChannel = `
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<true/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
<key>EXUpdatesReleaseChannel</key>
|
||||
<string>default</string>
|
||||
<key>EXUpdatesSDKVersion</key>
|
||||
<string>40.0.0</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://exp.host/@user/expo-project</string>
|
||||
<key>EXUpdatesRequestHeaders</key>
|
||||
<dict>
|
||||
<key>expo-channel-name</key>
|
||||
<string>${testChannel}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
const ExpoPlistWithOutChannel = `
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<true/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
<key>EXUpdatesReleaseChannel</key>
|
||||
<string>default</string>
|
||||
<key>EXUpdatesSDKVersion</key>
|
||||
<string>40.0.0</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://exp.host/@user/expo-project</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
@@ -5,16 +5,17 @@ import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readPlistAsync } from '../plist';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from '../version';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
@@ -144,7 +145,55 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject(): ExpoConfig {
|
||||
describe(readBuildNumberAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the build number from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readShortVersionAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initGenericProject({
|
||||
shortVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||
});
|
||||
expect(buildNumber).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject({
|
||||
shortVersion = '1.0.0',
|
||||
version = '1',
|
||||
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
@@ -160,9 +209,9 @@ function initGenericProject(): ExpoConfig {
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<string>${shortVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>${version}</string>
|
||||
</dict>
|
||||
</plist>`,
|
||||
},
|
||||
|
||||
@@ -1,137 +1,89 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import path from 'path';
|
||||
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { IosMetadataContext } from '../metadata';
|
||||
import { Platform } from '../types';
|
||||
import { ensureBundleIdentifierIsValidAsync } from './bundleIdentifer';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
import { prepareJobAsync, sanitizedTargetName } from './prepareJob';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.IOS>({
|
||||
commandCtx,
|
||||
platform: Platform.IOS,
|
||||
easConfig,
|
||||
});
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (
|
||||
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
|
||||
) {
|
||||
throw new Error(
|
||||
`"ios" directory not found. If you're trying to build a managed project, set ${chalk.bold(
|
||||
`builds.ios.${commandCtx.profile}.workflow`
|
||||
)} in "eas.json" to "managed".`
|
||||
);
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
let iosBuildScheme: string | undefined;
|
||||
let iosApplicationTarget: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
iosBuildScheme
|
||||
);
|
||||
} else {
|
||||
if (!commandCtx.exp.name) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
iosApplicationTarget = sanitizedTargetName(commandCtx.exp.name);
|
||||
if (!iosApplicationTarget) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
}
|
||||
|
||||
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosBuildScheme,
|
||||
iosApplicationTarget,
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
},
|
||||
ensureCredentialsAsync: ensureIosCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
buildProfile: buildCtx.buildProfile,
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync,
|
||||
getMetadataContext: (): IosMetadataContext => {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return { buildSettings: {} };
|
||||
}
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
|
||||
return { buildSettings };
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
if (job.type === Workflow.GENERIC) {
|
||||
const graphqlJob = transformGenericJob(job);
|
||||
return await BuildMutation.createIosGenericBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
} else {
|
||||
const graphqlJob = transformManagedJob(job);
|
||||
return await BuildMutation.createIosManagedBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
}
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.IOS>): Promise<string> {
|
||||
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
|
||||
if (schemes.length === 1) {
|
||||
return schemes[0];
|
||||
}
|
||||
|
||||
const sortedSchemes = sortBy(schemes);
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
|
||||
);
|
||||
Log.log(
|
||||
`You can specify the scheme you want to build at ${chalk.bold(
|
||||
`builds.ios.${ctx.commandCtx.profile}.scheme`
|
||||
)} in eas.json.`
|
||||
);
|
||||
if (ctx.commandCtx.nonInteractive) {
|
||||
const withoutTvOS = sortedSchemes.filter(i => !i.includes('tvOS'));
|
||||
const scheme = withoutTvOS.length > 0 ? withoutTvOS[0] : sortedSchemes[0];
|
||||
Log.log(
|
||||
`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`
|
||||
);
|
||||
Log.newLine();
|
||||
return scheme;
|
||||
} else {
|
||||
const { selectedScheme } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selectedScheme',
|
||||
message: 'Which scheme would you like to build now?',
|
||||
choices: sortedSchemes.map(scheme => ({ title: scheme, value: scheme })),
|
||||
});
|
||||
Log.newLine();
|
||||
return selectedScheme as string;
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureAppIdentifierIsDefinedAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Platform } from '../types';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
|
||||
enum BundleIdentiferSource {
|
||||
XcodeProject,
|
||||
AppJson,
|
||||
}
|
||||
|
||||
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(bundleIdentifier);
|
||||
}
|
||||
|
||||
export async function ensureBundleIdentifierIsValidAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform: Platform.IOS,
|
||||
exp,
|
||||
});
|
||||
if (!isBundleIdentifierValid(bundleIdentifier)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
Log.error(
|
||||
`Invalid format of iOS bundleId. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`
|
||||
);
|
||||
Log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
|
||||
throw new Error('Invalid bundleIdentifier');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
const bundleIdentifierFromConfig = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
|
||||
if (bundleIdentifierFromPbxproj !== bundleIdentifierFromConfig) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
|
||||
);
|
||||
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!hasBundleIdentifierInStaticConfig) {
|
||||
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
|
||||
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`);
|
||||
}
|
||||
Log.newLine();
|
||||
const { bundleIdentifierSource } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'bundleIdentifierSource',
|
||||
message: 'Which bundle identifier should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `${chalk.bold(bundleIdentifierFromPbxproj)} - In Xcode project`,
|
||||
value: BundleIdentiferSource.XcodeProject,
|
||||
},
|
||||
{
|
||||
title: `${chalk.bold(bundleIdentifierFromConfig)} - In your ${configDescription}`,
|
||||
value: BundleIdentiferSource.AppJson,
|
||||
},
|
||||
],
|
||||
});
|
||||
switch (bundleIdentifierSource) {
|
||||
case BundleIdentiferSource.AppJson: {
|
||||
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
|
||||
projectDir,
|
||||
bundleIdentifierFromConfig
|
||||
);
|
||||
break;
|
||||
}
|
||||
case BundleIdentiferSource.XcodeProject: {
|
||||
if (hasBundleIdentifierInStaticConfig) {
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
|
||||
});
|
||||
} else {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
|
||||
if (getConfigFilePaths(projectDir).staticConfigPath) {
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
|
||||
});
|
||||
} else {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
}
|
||||
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
|
||||
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
|
||||
projectDir,
|
||||
bundleIdentifierFromConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function missingBundleIdentifierMessage(configDescription: string): string {
|
||||
return `Please define "ios.bundleIdentifier" in ${configDescription} and run "eas build:configure" again.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static config exists and if ios.bundleIdentifier is defined there.
|
||||
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
|
||||
*/
|
||||
async function hasBundleIdentifierInStaticConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<boolean> {
|
||||
if (!exp.ios?.bundleIdentifier) {
|
||||
return false;
|
||||
}
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
return false;
|
||||
}
|
||||
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
|
||||
return rawStaticConfig?.expo?.ios?.bundleIdentifier === exp.ios.bundleIdentifier;
|
||||
}
|
||||
@@ -1,35 +1,25 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
IosBuildProfile,
|
||||
IosGenericBuildProfile,
|
||||
VersionAutoIncrement,
|
||||
} from '@expo/eas-json';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import {
|
||||
configureBundleIdentifierAsync,
|
||||
ensureBundleIdentifierIsValidAsync,
|
||||
} from './bundleIdentifer';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
const disableIosBundleIdentifierValidation = (
|
||||
await new EasJsonReader(ctx.projectDir, 'all').readRawAsync()
|
||||
)?.experimental?.disableIosBundleIdentifierValidation;
|
||||
if (!disableIosBundleIdentifierValidation) {
|
||||
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureBundleIdentifierIsValidAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
@@ -46,24 +36,11 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
exp: ExpoConfig;
|
||||
buildProfile: IosBuildProfile;
|
||||
}): Promise<void> {
|
||||
const { workflow, autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const genericBuildProfile = buildProfile as IosGenericBuildProfile;
|
||||
if (!genericBuildProfile.disableIosBundleIdentifierValidation) {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
if (
|
||||
!bundleIdentifierFromPbxproj ||
|
||||
bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier
|
||||
) {
|
||||
throw new Error(
|
||||
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
|
||||
@@ -1,65 +1,38 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppLookupParamsFromContext } from '../../credentials/ios/actions/new/BuildCredentialsUtils';
|
||||
import { AppLookupParams } from '../../credentials/ios/credentials';
|
||||
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';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
buildCtx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
if (!shouldProvideCredentials(buildCtx)) {
|
||||
return;
|
||||
}
|
||||
assert(ctx.commandCtx.exp?.ios?.bundleIdentifier, 'ios.bundleIdentifier is required');
|
||||
return await resolveIosCredentialsAsync(ctx.commandCtx.projectDir, {
|
||||
app: {
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
bundleIdentifier: ctx.commandCtx.exp.ios.bundleIdentifier,
|
||||
|
||||
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
|
||||
exp: buildCtx.exp,
|
||||
nonInteractive: buildCtx.nonInteractive,
|
||||
});
|
||||
|
||||
const provider = new IosCredentialsProvider(credentialsCtx, {
|
||||
app: getAppFromContext(credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
|
||||
},
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: ctx.buildProfile.enterpriseProvisioning,
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
}
|
||||
|
||||
interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
distribution: IosDistributionType;
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
}
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
});
|
||||
const app = getAppLookupParamsFromContext(ctx);
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app,
|
||||
distribution: params.distribution,
|
||||
enterpriseProvisioning: params.enterpriseProvisioning,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
});
|
||||
const { credentialsSource } = params;
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
@@ -67,6 +40,6 @@ export async function resolveIosCredentialsAsync(
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
|
||||
return ctx.buildProfile.distribution !== 'simulator';
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
}
|
||||
@@ -3,40 +3,28 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
DistributionType,
|
||||
IosGenericJobInput,
|
||||
IosBuildType,
|
||||
IosJobInput,
|
||||
IosJobSecretsInput,
|
||||
IosManagedBuildType,
|
||||
IosManagedJobInput,
|
||||
IosSchemeBuildConfiguration,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
distribution: job.distribution && transformDistributionType(job.distribution),
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
|
||||
scheme: job.scheme,
|
||||
schemeBuildConfiguration:
|
||||
job.schemeBuildConfiguration &&
|
||||
transformSchemeBuildConfiguration(job.schemeBuildConfiguration),
|
||||
buildConfiguration: job.buildConfiguration,
|
||||
artifactPath: job.artifactPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
|
||||
return {
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
distribution: job.distribution && transformDistributionType(job.distribution),
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
username: job.username,
|
||||
};
|
||||
@@ -74,20 +62,10 @@ function transformIosSecrets(secrets: {
|
||||
};
|
||||
}
|
||||
|
||||
function transformSchemeBuildConfiguration(
|
||||
schemeBuildConfiguration: Ios.SchemeBuildConfiguration
|
||||
): IosSchemeBuildConfiguration {
|
||||
if (schemeBuildConfiguration === Ios.SchemeBuildConfiguration.DEBUG) {
|
||||
return IosSchemeBuildConfiguration.Debug;
|
||||
function transformBuildType(buildType: Ios.BuildType): IosBuildType {
|
||||
if (buildType === Ios.BuildType.DEVELOPMENT_CLIENT) {
|
||||
return IosBuildType.DevelopmentClient;
|
||||
} else {
|
||||
return IosSchemeBuildConfiguration.Release;
|
||||
}
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
|
||||
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
|
||||
return IosManagedBuildType.DevelopmentClient;
|
||||
} else {
|
||||
return IosManagedBuildType.Release;
|
||||
return IosBuildType.Release;
|
||||
}
|
||||
}
|
||||
@@ -1,165 +1,96 @@
|
||||
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import { ArchiveSource, Ios, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
IosCredentials,
|
||||
IosTargetCredentials,
|
||||
isCredentialsMap,
|
||||
readEnvironmentSecretsAsync,
|
||||
} from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosBuildScheme?: string;
|
||||
iosApplicationTarget?: string;
|
||||
};
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
throw new Error("Unknown workflow. Shouldn't happen");
|
||||
}
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.IOS;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: Ios.BuilderEnvironment;
|
||||
releaseChannel?: string;
|
||||
distribution?: Ios.DistributionType;
|
||||
cache: Cache;
|
||||
secrets: {
|
||||
buildCredentials: Ios.BuildCredentials;
|
||||
environmentSecrets?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{
|
||||
credentials,
|
||||
targetName,
|
||||
projectArchive,
|
||||
}: { credentials?: IosCredentials; targetName?: string; projectArchive: ArchiveSource }
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
|
||||
let buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials && isCredentialsMap(credentials)) {
|
||||
const targets = Object.keys(credentials);
|
||||
for (const target of targets) {
|
||||
buildCredentials[target] = prepareTargetCredentials(credentials[target]);
|
||||
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
for (const targetName of targetNames) {
|
||||
buildCredentials[targetName] = prepareTargetCredentials(jobData.credentials[targetName]);
|
||||
}
|
||||
} else if (credentials) {
|
||||
// targetName
|
||||
// - for managed projects: sanitized .name from the app config
|
||||
// - for generic projects: name of the application target
|
||||
assert(targetName, 'target name should be defined');
|
||||
buildCredentials = {
|
||||
[targetName]: prepareTargetCredentials(credentials),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
const job: Ios.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.IOS,
|
||||
projectArchive,
|
||||
distribution: ctx.buildProfile.distribution,
|
||||
projectArchive: jobData.projectArchive,
|
||||
projectRootDirectory,
|
||||
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
image: resolveImage(ctx),
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
cocoapods: ctx.buildProfile.cocoapods,
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
|
||||
buildType: ctx.buildProfile.developmentClient
|
||||
? Ios.BuildType.DEVELOPMENT_CLIENT
|
||||
: Ios.BuildType.RELEASE,
|
||||
username,
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.TargetCredentials {
|
||||
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
|
||||
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
|
||||
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
|
||||
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
|
||||
if (majorSdkVersion <= 41) {
|
||||
return 'macos-catalina-10.15-xcode-12.4';
|
||||
}
|
||||
}
|
||||
return ctx.buildProfile.image ?? 'default';
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: targetCredentials.distributionCertificate.certP12,
|
||||
password: targetCredentials.distributionCertificate.certPassword,
|
||||
dataBase64: targetCredentials.distributionCertificate.certificateP12,
|
||||
password: targetCredentials.distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: IosGenericBuildProfile
|
||||
): Promise<Partial<Ios.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.GENERIC,
|
||||
scheme: jobData.projectConfiguration.iosBuildScheme,
|
||||
schemeBuildConfiguration:
|
||||
buildProfile.schemeBuildConfiguration === 'Auto'
|
||||
? undefined
|
||||
: buildProfile.schemeBuildConfiguration,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: IosManagedBuildProfile
|
||||
): Promise<Partial<Ios.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.MANAGED,
|
||||
buildType: buildProfile.buildType,
|
||||
username: accountName,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
|
||||
// it's used in eject
|
||||
export function sanitizedTargetName(name: string) {
|
||||
return name
|
||||
.replace(/[\W_]+/g, '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import semver from 'semver';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { evaluateString } from '../utils/template';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export enum BumpStrategy {
|
||||
@@ -100,9 +104,35 @@ export async function bumpVersionInAppJsonAsync({
|
||||
}
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
return await readPlistAsync(infoPlistPath);
|
||||
export async function readShortVersionAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return (
|
||||
infoPlist.CFBundleShortVersionString &&
|
||||
evaluateString(infoPlist.CFBundleShortVersionString, buildSettings)
|
||||
);
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readBuildNumberAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleVersion && evaluateString(infoPlist.CFBundleVersion, buildSettings);
|
||||
} else {
|
||||
return IOSConfig.Version.getBuildNumber(exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToInfoPlistAsync({
|
||||
@@ -120,6 +150,11 @@ async function writeVersionsToInfoPlistAsync({
|
||||
return updatedInfoPlist;
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
async function writeInfoPlistAsync({
|
||||
projectDir,
|
||||
infoPlist,
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { getAppIdentifierAsync } from '../project/projectUtils';
|
||||
import { gitCommitHashAsync } from '../utils/git';
|
||||
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
|
||||
import { getApplicationIdAsync } from '../project/android/applicationId';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import vcs from '../vcs';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from './android/UpdatesModule';
|
||||
import { readVersionCodeAsync, readVersionNameAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import { readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync } from './ios/UpdatesModule';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from './ios/UpdatesModule';
|
||||
import { readBuildNumberAsync, readShortVersionAsync } from './ios/version';
|
||||
import { Platform } from './types';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
@@ -16,51 +28,131 @@ import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
*/
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
|
||||
? object
|
||||
: IosMetadataContext;
|
||||
|
||||
export interface IosMetadataContext {
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}
|
||||
|
||||
export async function collectMetadata<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
{
|
||||
credentialsSource,
|
||||
}: {
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
}
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<Metadata> {
|
||||
return {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: ctx.commandCtx.exp.version!,
|
||||
appVersion: await resolveAppVersionAsync(ctx),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
releaseChannel: await resolveReleaseChannel(ctx),
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: ctx.exp.runtimeVersion,
|
||||
...channelOrReleaseChannel,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
appName: ctx.commandCtx.exp.name,
|
||||
appIdentifier:
|
||||
(await getAppIdentifierAsync({
|
||||
projectDir: ctx.commandCtx.projectDir,
|
||||
platform: ctx.platform,
|
||||
exp: ctx.commandCtx.exp,
|
||||
})) ?? undefined,
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
gitCommitHash: await gitCommitHashAsync(),
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await vcs.getCommitHashAsync(),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
ctx as BuildContext<Platform.IOS>
|
||||
),
|
||||
}),
|
||||
};
|
||||
return sanitizeMetadata(metadata);
|
||||
}
|
||||
|
||||
async function resolveReleaseChannel<T extends Platform>(
|
||||
async function resolveAppVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = platformContext as IosMetadataContext | undefined;
|
||||
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
|
||||
} else {
|
||||
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAppBuildVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = platformContext as IosMetadataContext | undefined;
|
||||
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
|
||||
} else {
|
||||
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.exp);
|
||||
return versionCode !== undefined ? String(versionCode) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAppIdentifierAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
} else {
|
||||
return await getApplicationIdAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.buildProfile.channel) {
|
||||
return { channel: ctx.buildProfile.channel };
|
||||
}
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return { releaseChannel: ctx.buildProfile.releaseChannel };
|
||||
}
|
||||
const channel = await getNativeChannelAsync(ctx);
|
||||
if (channel) {
|
||||
return { channel };
|
||||
}
|
||||
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
|
||||
return { releaseChannel };
|
||||
}
|
||||
|
||||
async function getNativeReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
async function getNativeChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
|
||||
return undefined;
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return ctx.buildProfile.releaseChannel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let maybeReleaseChannel: string | null;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
} else {
|
||||
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
}
|
||||
return maybeReleaseChannel ?? 'default';
|
||||
function resolveIosEnterpriseProvisioning(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): IosEnterpriseProvisioning | undefined {
|
||||
return ctx.buildProfile.enterpriseProvisioning;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export enum RequestedPlatform {
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
ERRORED = 'errored',
|
||||
@@ -14,4 +15,10 @@ export enum BuildStatus {
|
||||
CANCELED = 'canceled',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number>;
|
||||
export enum BuildDistributionType {
|
||||
STORE = 'store',
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
@@ -1,4 +1,3 @@
|
||||
// import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import * as ExpoConfig from '@expo/config';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -7,9 +7,10 @@ import { requestedPlatformDisplayNames } from '../constants';
|
||||
|
||||
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL)
|
||||
if (credentialsSource === CredentialsSource.LOCAL) {
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
else if (credentialsSource === CredentialsSource.REMOTE)
|
||||
} else if (credentialsSource === CredentialsSource.REMOTE) {
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
}
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { BuildFragment, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { appPlatformDisplayNames } from '../constants';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
|
||||
export function formatGraphQLBuild(build: BuildFragment) {
|
||||
const actor = getActorName(build);
|
||||
const account = build.project.__typename === 'App' ? build.project.ownerAccount.name : 'unknown';
|
||||
const fields: { label: string; value: string }[] = [
|
||||
const fields: { label: string; value?: string | null }[] = [
|
||||
{ label: 'ID', value: build.id },
|
||||
{
|
||||
label: 'Platform',
|
||||
@@ -18,6 +21,8 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
label: 'Status',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
return chalk.blue('new');
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
return chalk.blue('in queue');
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
@@ -35,53 +40,83 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
},
|
||||
{
|
||||
label: 'Distribution',
|
||||
value: build.distribution?.toLowerCase() ?? chalk.gray('unknown'),
|
||||
value: build.distribution?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Enterprise Provisioning',
|
||||
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Release Channel',
|
||||
value: build.releaseChannel ?? chalk.gray('unknown'),
|
||||
value: build.releaseChannel,
|
||||
},
|
||||
{
|
||||
label: 'Channel',
|
||||
value: build.channel,
|
||||
},
|
||||
{
|
||||
label: 'SDK Version',
|
||||
value: build.sdkVersion,
|
||||
},
|
||||
{
|
||||
label: 'Runtime Version',
|
||||
value: build.runtimeVersion,
|
||||
},
|
||||
{
|
||||
label: 'Version',
|
||||
value: build.appVersion,
|
||||
},
|
||||
{
|
||||
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
|
||||
value: build.appBuildVersion,
|
||||
},
|
||||
{
|
||||
label: 'Commit',
|
||||
value: build.gitCommitHash,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: getBuildLogsUrl({ buildId: build.id, account }),
|
||||
value: getBuildLogsUrl(build),
|
||||
},
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
if (
|
||||
build.status === GraphQLBuildStatus.InQueue ||
|
||||
build.status === GraphQLBuildStatus.InProgress
|
||||
) {
|
||||
}
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return '---------';
|
||||
return null;
|
||||
case GraphQLBuildStatus.Finished: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? url : chalk.red('not found');
|
||||
}
|
||||
default:
|
||||
return 'unknown';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
|
||||
{
|
||||
label: 'Finished at',
|
||||
value:
|
||||
build.status === GraphQLBuildStatus.InQueue ||
|
||||
build.status === GraphQLBuildStatus.InProgress
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
value: [
|
||||
GraphQLBuildStatus.New,
|
||||
GraphQLBuildStatus.InQueue,
|
||||
GraphQLBuildStatus.InProgress,
|
||||
].includes(build.status)
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
},
|
||||
{ label: 'Started by', value: actor ?? 'unknown' },
|
||||
];
|
||||
|
||||
return formatFields(fields);
|
||||
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
return formatFields(filteredFields);
|
||||
}
|
||||
|
||||
const getActorName = (build: BuildFragment): string => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
import {
|
||||
BuildError,
|
||||
@@ -10,51 +12,34 @@ import {
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../constants';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(
|
||||
accountName: string,
|
||||
builds: { platform: 'android' | 'ios'; buildId: string }[]
|
||||
): void {
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
const { buildId } = builds[0];
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId,
|
||||
account: accountName,
|
||||
});
|
||||
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
|
||||
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(({ buildId, platform }) => {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId,
|
||||
account: accountName,
|
||||
});
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${requestedPlatformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`
|
||||
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function printBuildResults(accountName: string, builds: (BuildFragment | null)[]): void {
|
||||
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(accountName, build);
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build =>
|
||||
printBuildResult(accountName, build)
|
||||
);
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
|
||||
function printBuildResult(accountName: string, build: BuildFragment): void {
|
||||
function printBuildResult(build: BuildFragment): void {
|
||||
Log.addNewLineIfNone();
|
||||
if (build.status === BuildStatus.Errored) {
|
||||
const userError = build.error;
|
||||
@@ -78,14 +63,13 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
|
||||
}
|
||||
|
||||
if (build.distribution === DistributionType.Internal) {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId: build.id,
|
||||
account: accountName,
|
||||
});
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
const installUrl = getInternalDistributionInstallUrl(build);
|
||||
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} Open this link on your ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} devices to install the app:`
|
||||
} devices (or scan the QR code) to install the app:`
|
||||
);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import ora from 'ora';
|
||||
@@ -9,55 +8,16 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import {
|
||||
doesGitRepoExistAsync,
|
||||
getGitDiffOutputAsync,
|
||||
gitDiffAsync,
|
||||
gitRootDirectoryAsync,
|
||||
gitStatusAsync,
|
||||
isGitInstalledAsync,
|
||||
} from '../../utils/git';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
async function ensureGitRepoExistsAsync(): Promise<void> {
|
||||
if (!(await isGitInstalledAsync())) {
|
||||
throw new Error('git command not found, install it before proceeding');
|
||||
}
|
||||
|
||||
if (await doesGitRepoExistAsync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warn("It looks like you haven't initialized the git repository yet.");
|
||||
Log.warn('EAS Build requires you to use a git repository for your project.');
|
||||
|
||||
const confirmInit = await confirmAsync({
|
||||
message: `Would you like to run 'git init' in the current directory?`,
|
||||
});
|
||||
if (!confirmInit) {
|
||||
throw new Error(
|
||||
'A git repository is required for building your project. Initialize it and run this command again.'
|
||||
);
|
||||
}
|
||||
await spawnAsync('git', ['init']);
|
||||
|
||||
Log.log("We're going to make an initial commit for you repository.");
|
||||
|
||||
await commitPromptAsync({ initialCommitMessage: 'Initial commit', commitAllFiles: true });
|
||||
}
|
||||
|
||||
async function isGitStatusCleanAsync(): Promise<boolean> {
|
||||
const changes = await gitStatusAsync({ showUntracked: true });
|
||||
return changes.length === 0;
|
||||
}
|
||||
|
||||
async function maybeBailOnGitStatusAsync(): Promise<void> {
|
||||
if (await isGitStatusCleanAsync()) {
|
||||
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
if (!(await vcs.hasUncommittedChangesAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`It's recommended to ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
@@ -72,12 +32,12 @@ async function maybeBailOnGitStatusAsync(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (await isGitStatusCleanAsync()) {
|
||||
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (!(await vcs.hasUncommittedChangesAsync())) {
|
||||
return;
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`This operation needs to be run on a clean working tree, please ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
@@ -96,7 +56,24 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
export async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await vcs.commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
@@ -118,14 +95,7 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await spawnAsync('git', [
|
||||
'clone',
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
`file://${await gitRootDirectoryAsync()}`,
|
||||
shallowClonePath,
|
||||
]);
|
||||
await vcs.makeShallowCopyAsync(shallowClonePath);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
@@ -151,44 +121,3 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
async function showDiffAsync() {
|
||||
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
|
||||
await gitDiffAsync({ withPager: outputTooLarge });
|
||||
}
|
||||
|
||||
async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
if (commitAllFiles) {
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
}
|
||||
await commitChangedFilesAsync(message);
|
||||
}
|
||||
|
||||
async function commitChangedFilesAsync(message: string): Promise<void> {
|
||||
await spawnAsync('git', ['add', '-u']);
|
||||
await spawnAsync('git', ['commit', '-m', message]);
|
||||
}
|
||||
|
||||
export {
|
||||
isGitStatusCleanAsync,
|
||||
showDiffAsync,
|
||||
commitPromptAsync,
|
||||
commitChangedFilesAsync,
|
||||
ensureGitRepoExistsAsync,
|
||||
ensureGitStatusIsCleanAsync,
|
||||
maybeBailOnGitStatusAsync,
|
||||
makeProjectTarballAsync,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import template from 'lodash/template';
|
||||
|
||||
export function evaluateString(s: string, vars: Record<string, any>): string {
|
||||
return template(s, { interpolate: /\$\(([\s\S]+?)\)/g })(vars);
|
||||
}
|
||||
@@ -1,15 +1,31 @@
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import assert from 'assert';
|
||||
|
||||
export function getBuildLogsUrl({
|
||||
buildId,
|
||||
account,
|
||||
}: {
|
||||
buildId: string;
|
||||
account: string;
|
||||
}): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
if (project.__typename === 'App') {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/projects/${
|
||||
project.name
|
||||
}/builds/${build.id}`;
|
||||
} else {
|
||||
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
|
||||
}
|
||||
|
||||
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
|
||||
if (build.platform === AppPlatform.Ios) {
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
|
||||
build.project.id
|
||||
}/builds/${build.id}/manifest.plist`;
|
||||
}
|
||||
|
||||
assert(build.artifacts?.buildUrl, 'buildUrl is missing');
|
||||
|
||||
return build.artifacts.buildUrl;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
*/
|
||||
protected requiresAuthentication = true;
|
||||
|
||||
async init() {
|
||||
await initAnalyticsAsync();
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
await ensureLoggedInAsync();
|
||||
} else {
|
||||
await getUserAsync();
|
||||
}
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
async finally(err: Error) {
|
||||
await flushAnalyticsAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics';
|
||||
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';
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
beforeAll(() => {
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
jest.mock('../../user/User', () => ({ getUserAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../analytics', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(ensureLoggedInAsync as jest.Mock).mockClear();
|
||||
(initAsync as jest.Mock).mockClear();
|
||||
(flushAsync as jest.Mock).mockClear();
|
||||
(logEvent as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('without exceptions', () => {
|
||||
it('ensures the user is logged in', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(ensureLoggedInAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
|
||||
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(getUserAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after exceptions', () => {
|
||||
it('flushes analytics', async () => {
|
||||
try {
|
||||
await TestEasCommand.run().then(() => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
export default class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = this.authValue();
|
||||
|
||||
authValue() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async run() {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
@@ -10,11 +10,13 @@ import {
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getBranchNameAsync } from '../../utils/git';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -78,11 +80,8 @@ export default class BranchCreate extends Command {
|
||||
}
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
@@ -94,7 +93,7 @@ export default class BranchCreate extends Command {
|
||||
name: 'name',
|
||||
message: 'Please name the branch:',
|
||||
initial:
|
||||
(await getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
@@ -107,9 +106,7 @@ export default class BranchCreate extends Command {
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${exp.slug}`
|
||||
)}.`
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,17 @@ import {
|
||||
GetBranchInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
async function getBranchInfoAsync({
|
||||
appId,
|
||||
name,
|
||||
}: GetBranchInfoQueryVariables): Promise<{ branchId: string }> {
|
||||
}: GetBranchInfoQueryVariables): Promise<GetBranchInfoQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
|
||||
@@ -44,11 +46,7 @@ async function getBranchInfoAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const branchId = data.app?.byId.updateBranchByName?.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${name} on ${appId}`);
|
||||
}
|
||||
return { branchId };
|
||||
return data;
|
||||
}
|
||||
|
||||
async function deleteBranchOnAppAsync({
|
||||
@@ -104,11 +102,8 @@ export default class BranchDelete extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
@@ -123,7 +118,11 @@ export default class BranchDelete extends Command {
|
||||
}));
|
||||
}
|
||||
|
||||
const { branchId } = await getBranchInfoAsync({ appId: projectId, name });
|
||||
const data = await getBranchInfoAsync({ appId: projectId, name });
|
||||
const branchId = data.app?.byId.updateBranchByName?.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${name} on ${fullName}`);
|
||||
}
|
||||
|
||||
if (!jsonFlag) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -148,9 +147,7 @@ export default class BranchDelete extends Command {
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(
|
||||
`@${accountName}/${exp.slug}`
|
||||
)}.`
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,28 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
import { format } from 'timeago.js';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
BranchesByAppQuery,
|
||||
BranchesByAppQueryVariables,
|
||||
Maybe,
|
||||
Robot,
|
||||
Update,
|
||||
User,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchesByAppQuery, BranchesByAppQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'> & {
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
};
|
||||
|
||||
export const UPDATE_COLUMNS = ['update description', 'update runtime version', 'update group ID'];
|
||||
const BRANCHES_LIMIT = 10_000;
|
||||
|
||||
export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
export async function listBranchesAsync({ projectId }: { projectId: string }) {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
|
||||
gql`
|
||||
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
|
||||
query BranchesByAppQuery($appId: String!, $limit: Int!) {
|
||||
app {
|
||||
byFullName(fullName: $fullName) {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
fullName
|
||||
updateBranches(offset: 0, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
actor {
|
||||
__typename
|
||||
@@ -54,6 +40,7 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
message
|
||||
runtimeVersion
|
||||
group
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,14 +48,14 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
}
|
||||
`,
|
||||
{
|
||||
fullName,
|
||||
appId: projectId,
|
||||
limit: BRANCHES_LIMIT,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data?.app?.byFullName.updateBranches ?? [];
|
||||
return data?.app?.byId.updateBranches ?? [];
|
||||
}
|
||||
|
||||
export default class BranchList extends Command {
|
||||
@@ -91,8 +78,8 @@ export default class BranchList extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const branches = await listBranchesAsync({ fullName });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
if (flags.json) {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
} else {
|
||||
@@ -100,14 +87,21 @@ export default class BranchList extends Command {
|
||||
head: ['branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
table.push(
|
||||
...branches.map(branch => [
|
||||
branch.name,
|
||||
formatUpdate(branch.updates[0]),
|
||||
branch.updates[0]?.runtimeVersion ?? 'N/A',
|
||||
branch.updates[0]?.group ?? 'N/A',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branches with their most recent update group:'));
|
||||
Log.log(table.toString());
|
||||
if (branches.length >= BRANCHES_LIMIT) {
|
||||
@@ -116,13 +110,3 @@ export default class BranchList extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUpdate(update: FormatUpdateParameter): string {
|
||||
if (!update) {
|
||||
return 'N/A';
|
||||
}
|
||||
const message = update.message ? `"${update.message}" ` : '';
|
||||
return `${message}(${format(update.createdAt, 'en_US')} by ${getActorDisplayName(
|
||||
update.actor as any
|
||||
)})`;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
import gql from 'graphql-tag';
|
||||
import { uniqBy } from 'lodash';
|
||||
@@ -19,20 +18,23 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { getLastCommitMessageAsync } from '../../utils/git';
|
||||
import { formatUpdate, listBranchesAsync } from './list';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import vcs from '../../vcs';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
|
||||
type PublishPlatforms = 'android' | 'ios';
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
type PlatformFlag = PublishPlatform | 'all';
|
||||
|
||||
async function getUpdateGroupAsync({
|
||||
group,
|
||||
@@ -68,7 +70,6 @@ export default class BranchPublish extends Command {
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
description: 'Name of the branch to publish on',
|
||||
},
|
||||
];
|
||||
@@ -95,6 +96,13 @@ export default class BranchPublish extends Command {
|
||||
description: `skip running Expo CLI to bundle the app before publishing`,
|
||||
default: false,
|
||||
}),
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
description: `Only publish to a single platform`,
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: `return a json with the new update group.`,
|
||||
default: false,
|
||||
@@ -113,6 +121,7 @@ export default class BranchPublish extends Command {
|
||||
'skip-bundler': skipBundler,
|
||||
},
|
||||
} = this.parse(BranchPublish);
|
||||
const platformFlag = this.parse(BranchPublish).flags.platform as PlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
@@ -121,9 +130,11 @@ export default class BranchPublish extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
let { slug, runtimeVersion, sdkVersion } = exp;
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
let { runtimeVersion, sdkVersion } = exp;
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
// construct the runtimeVersion with special meaning indicating that the runtime is an
|
||||
@@ -139,10 +150,7 @@ export default class BranchPublish extends Command {
|
||||
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
@@ -150,7 +158,7 @@ export default class BranchPublish extends Command {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const branches = await listBranchesAsync({ fullName: `@${accountName}/${slug}` });
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
name = await selectAsync<string>(
|
||||
'which branch would you like to publish on?',
|
||||
branches.map(branch => {
|
||||
@@ -182,30 +190,68 @@ export default class BranchPublish extends Command {
|
||||
if (group) {
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
const updateGroups = uniqBy(updates, u => u.group).map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
// 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.');
|
||||
}
|
||||
|
||||
const updateGroups = uniqBy(updates, u => u.group)
|
||||
.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return platformFlag === 'all' || update.platform === platformFlag;
|
||||
})
|
||||
.map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}". Did you mean to do a regular publish?`
|
||||
`There are no updates on branch "${name}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
const updateGroup = await selectAsync<string>(
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === updateGroup);
|
||||
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
update => platformFlag === 'all' || update.platform === platformFlag
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
for (const update of updatesToRepublish) {
|
||||
const { platform, manifestFragment } = update;
|
||||
updateInfoGroup[platform as PublishPlatforms] = JSON.parse(manifestFragment);
|
||||
let publicationPlatformMessage: string;
|
||||
if (platformFlag === 'all') {
|
||||
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
|
||||
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
|
||||
}
|
||||
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
|
||||
.map(update => update.platform)
|
||||
.join(',')}`;
|
||||
} else {
|
||||
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
updateInfoGroup[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublish[0].group;
|
||||
oldMessage = updatesToRepublish[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublish[0].runtimeVersion;
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
} else {
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
@@ -214,9 +260,10 @@ export default class BranchPublish extends Command {
|
||||
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const assets = collectAssets(inputDir!);
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = collectAssets({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
@@ -235,7 +282,7 @@ export default class BranchPublish extends Command {
|
||||
message: `Please enter a publication message.`,
|
||||
initial: republish
|
||||
? `Republish "${oldMessage!}" - group: ${group}`
|
||||
: (await getLastCommitMessageAsync())?.trim(),
|
||||
: (await vcs.getLastCommitMessageAsync())?.trim(),
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
@@ -258,35 +305,14 @@ export default class BranchPublish extends Command {
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newUpdateGroup));
|
||||
} else {
|
||||
const outputMessage = new Table({
|
||||
wordWrap: true,
|
||||
chars: {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: ' ',
|
||||
},
|
||||
style: { 'padding-left': 0, 'padding-right': 0 },
|
||||
});
|
||||
outputMessage.push(
|
||||
[chalk.dim('project'), `@${accountName}/${slug}`],
|
||||
[chalk.dim('branch'), name],
|
||||
[chalk.dim('runtimeVersion'), runtimeVersion],
|
||||
[chalk.dim('groupID'), newUpdateGroup.group],
|
||||
[chalk.dim('message'), message]
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'branch', value: name },
|
||||
{ label: 'runtime version', value: runtimeVersion },
|
||||
{ label: 'update group ID', value: newUpdateGroup.group },
|
||||
{ label: 'message', value: message },
|
||||
])
|
||||
);
|
||||
Log.log(outputMessage.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
async function renameUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -76,12 +78,8 @@ export default class BranchRename extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!currentName) {
|
||||
const validationMessage = 'current name may not be empty.';
|
||||
@@ -123,7 +121,7 @@ export default class BranchRename extends Command {
|
||||
Log.withTick(
|
||||
`️Renamed branch from ${currentName} to ${chalk.bold(
|
||||
editedBranch.name
|
||||
)} on project ${chalk.bold(`@${accountName}/${exp.slug}`)}.`
|
||||
)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,13 @@ import {
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { UPDATE_COLUMNS, formatUpdate } from './list';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
|
||||
type TruncatedUpdate = Pick<Update, 'group' | 'message' | 'createdAt' | 'actor'>;
|
||||
|
||||
export async function viewUpdateBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
@@ -118,12 +116,7 @@ export default class BranchView extends Command {
|
||||
}
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
@@ -158,15 +151,27 @@ export default class BranchView extends Command {
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
groupTable.push([formatUpdate(update), update.runtimeVersion, update.group]);
|
||||
groupTable.push([
|
||||
formatUpdate(update),
|
||||
update.runtimeVersion,
|
||||
update.group,
|
||||
getPlatformsForGroup({
|
||||
updates: UpdateBranch.updates,
|
||||
group: update.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branch:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: UpdateBranch.name },
|
||||
{ label: 'ID', value: UpdateBranch.id },
|
||||
])
|
||||
);
|
||||
Log.log(chalk.bold('Recent update groups published on this branch:'));
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Recently published update groups:'));
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,15 @@ function formatUnfinishedBuild(
|
||||
): string {
|
||||
const platform = appPlatformEmojis[build.platform];
|
||||
const startTime = new Date(build.createdAt).toLocaleString();
|
||||
const status = chalk.blue(build.status === BuildStatus.InQueue ? 'in-queue' : 'in-progress');
|
||||
let statusText: string;
|
||||
if (build.status === BuildStatus.New) {
|
||||
statusText = 'new';
|
||||
} else if (build.status === BuildStatus.InQueue) {
|
||||
statusText = 'in queue';
|
||||
} else {
|
||||
statusText = 'in progress';
|
||||
}
|
||||
const status = chalk.blue(statusText);
|
||||
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
|
||||
}
|
||||
|
||||
@@ -58,12 +66,13 @@ async function selectBuildToCancelAsync(
|
||||
const spinner = ora().start('Fetching the uncompleted builds…');
|
||||
let builds;
|
||||
try {
|
||||
const [inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
|
||||
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
|
||||
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
|
||||
]);
|
||||
spinner.stop();
|
||||
builds = [...inQueueBuilds, ...inProgressBuilds];
|
||||
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
|
||||
} catch (error) {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
|
||||
|
||||
@@ -17,10 +17,6 @@ export default class BuildConfigure extends Command {
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
'allow-experimental': flags.boolean({
|
||||
description: 'Enable experimental configuration steps.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
@@ -32,20 +28,10 @@ export default class BuildConfigure extends Command {
|
||||
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
const allowExperimental = flags['allow-experimental'];
|
||||
|
||||
if (allowExperimental) {
|
||||
Log.warn(
|
||||
`Project configuration will execute some additional steps that might fail if structure of your native project is significantly different from ${chalk.bold(
|
||||
'expo eject'
|
||||
)} or ${chalk.bold('expo init')}`
|
||||
);
|
||||
}
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
await configureAsync({
|
||||
platform,
|
||||
allowExperimental,
|
||||
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,44 +1,62 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
hasMismatchedExtendsAsync,
|
||||
isUsingDeprecatedFormatAsync,
|
||||
migrateAsync,
|
||||
} from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error, exit } from '@oclif/errors';
|
||||
|
||||
import { prepareAndroidBuildAsync } from '../../build/android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { Platform, RequestedPlatform } from '../../build/types';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { BuildFragment, BuildStatus } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
} from '../../analytics';
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import { isGitStatusCleanAsync } from '../../build/utils/repository';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
EAS_UNAVAILABLE_MESSAGE,
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
export default class Build extends Command {
|
||||
interface RawBuildFlags {
|
||||
platform?: string;
|
||||
'skip-credentials-check': boolean;
|
||||
'skip-project-configuration': boolean;
|
||||
profile: string;
|
||||
'non-interactive': boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
'clear-cache': boolean;
|
||||
}
|
||||
|
||||
interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
skipProjectConfiguration: boolean;
|
||||
profile: string;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'Start a build';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
|
||||
'skip-credentials-check': flags.boolean({
|
||||
default: false,
|
||||
description: 'Skip validation of build credentials',
|
||||
hidden: true,
|
||||
}),
|
||||
'skip-project-configuration': flags.boolean({
|
||||
default: false,
|
||||
@@ -61,181 +79,184 @@ export default class Build extends Command {
|
||||
allowNo: true,
|
||||
description: 'Wait for build(s) to complete',
|
||||
}),
|
||||
'clear-cache': flags.boolean({
|
||||
default: false,
|
||||
description: 'Clear cache before the build',
|
||||
}),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = this.parse(Build);
|
||||
await initAnalyticsAsync(); // TODO: run in oclif hook for every command
|
||||
try {
|
||||
const user = await ensureLoggedInAsync();
|
||||
const { flags: rawFlags } = this.parse(Build);
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const { requestedPlatform } = flags;
|
||||
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('--platform is required when building in non-interactive mode');
|
||||
}
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
platform,
|
||||
projectId,
|
||||
user,
|
||||
});
|
||||
if (accountHasPendingBuilds) {
|
||||
return;
|
||||
}
|
||||
|
||||
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
|
||||
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
skipCredentialsCheck: flags['skip-credentials-check'],
|
||||
const startedBuilds: BuildFragment[] = [];
|
||||
let metroConfigValidated = false;
|
||||
for (const platform of platformsToBuild) {
|
||||
const ctx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
|
||||
local: flags.local,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
});
|
||||
await buildAsync(commandCtx);
|
||||
} finally {
|
||||
await flushAnalyticsAsync(); // TODO: run in oclif hook for every command
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(ctx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
|
||||
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
const maybeBuild = await this.startBuildAsync(ctx);
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push(maybeBuild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNoPendingBuildsExistAsync({
|
||||
user,
|
||||
platform,
|
||||
accountName,
|
||||
projectId,
|
||||
}: {
|
||||
user: Actor;
|
||||
platform: RequestedPlatform;
|
||||
accountName: string;
|
||||
projectId: string;
|
||||
}): Promise<boolean> {
|
||||
// allow expo admins to run as many builds as they wish
|
||||
if (user.isExpoAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const appPlatforms = toAppPlatforms(platform);
|
||||
const maybePendingBuilds = await Promise.all(
|
||||
appPlatforms.map(appPlatform => BuildQuery.getPendingBuildIdAsync(accountName, appPlatform))
|
||||
);
|
||||
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
|
||||
if (pendingBuilds.length > 0) {
|
||||
Log.newLine();
|
||||
Log.error(
|
||||
'Your other builds are still pending. Wait for them to complete before running this command again.'
|
||||
);
|
||||
Log.newLine();
|
||||
const results = await Promise.all(
|
||||
pendingBuilds.map(pendingBuild => {
|
||||
return BuildQuery.byIdAsync(nullthrows(pendingBuild).id);
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
Log.log(formatGraphQLBuild(result));
|
||||
if (flags.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds);
|
||||
Log.newLine();
|
||||
|
||||
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
|
||||
if (platform === RequestedPlatform.All) {
|
||||
Log.error('Builds for multiple platforms are not supported with flag --local');
|
||||
return false;
|
||||
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
|
||||
Log.error('Unsupported platform, macOS is required to build apps for iOS');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
if (flags.wait) {
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
|
||||
printBuildResults(builds);
|
||||
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [AppPlatform.Android, AppPlatform.Ios];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [AppPlatform.Android];
|
||||
} else {
|
||||
return [AppPlatform.Ios];
|
||||
}
|
||||
}
|
||||
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('--platform is required when building in non-interactive mode');
|
||||
}
|
||||
const requestedPlatform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Build for platforms',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
title: 'All',
|
||||
value: RequestedPlatform.All,
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: RequestedPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
if (flags.local) {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
|
||||
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
|
||||
error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
|
||||
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
|
||||
return null;
|
||||
}
|
||||
const confirm = await confirmAsync({
|
||||
message: 'This app is not set up for building with EAS. Set it up now?',
|
||||
});
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: RequestedPlatform.All,
|
||||
allowExperimental: false,
|
||||
});
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
throw new Error(
|
||||
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
|
||||
if (flags['skip-credentials-check']) {
|
||||
Log.warnDeprecatedFlag(
|
||||
'skip-credentials-check',
|
||||
'Build credentials validation is always skipped with the --non-interactive flag. You can also skip interactively.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
return exp;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.io/build/eas-json'
|
||||
)})`
|
||||
|
||||
return {
|
||||
requestedPlatform,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
profile: flags['profile'],
|
||||
nonInteractive,
|
||||
local: flags['local'],
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
};
|
||||
}
|
||||
|
||||
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Build for platforms',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{ title: 'All', value: RequestedPlatform.All },
|
||||
{ title: 'iOS', value: RequestedPlatform.Ios },
|
||||
{ title: 'Android', value: RequestedPlatform.Android },
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
|
||||
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [Platform.ANDROID, Platform.IOS];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [Platform.ANDROID];
|
||||
} else {
|
||||
return [Platform.IOS];
|
||||
}
|
||||
}
|
||||
|
||||
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
if (await isUsingDeprecatedFormatAsync(projectDir)) {
|
||||
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
|
||||
if (nonInteractive) {
|
||||
Log.error('We detected that your eas.json is using a deprecated format.');
|
||||
Log.error(
|
||||
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.io/build/eas-json'
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message:
|
||||
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
|
||||
});
|
||||
if (confirm) {
|
||||
await migrateAsync(projectDir);
|
||||
if (hasMismatchedExtendsKeys) {
|
||||
Log.warn(
|
||||
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
|
||||
);
|
||||
Log.warn(
|
||||
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
|
||||
);
|
||||
error('Fix eas.json manually', { exit: 1 });
|
||||
}
|
||||
} else {
|
||||
Log.error(
|
||||
"Aborting, update your eas.json according to https://docs.expo.io/build/eas-json and run 'eas build' again"
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,13 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { BuildStatus, RequestedPlatform } from '../../build/types';
|
||||
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
DistributionType,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -23,6 +27,7 @@ export default class BuildList extends Command {
|
||||
}),
|
||||
status: flags.enum({
|
||||
options: [
|
||||
BuildStatus.NEW,
|
||||
BuildStatus.IN_QUEUE,
|
||||
BuildStatus.IN_PROGRESS,
|
||||
BuildStatus.ERRORED,
|
||||
@@ -30,16 +35,36 @@ export default class BuildList extends Command {
|
||||
BuildStatus.CANCELED,
|
||||
],
|
||||
}),
|
||||
distribution: flags.enum({
|
||||
options: [
|
||||
BuildDistributionType.STORE,
|
||||
BuildDistributionType.INTERNAL,
|
||||
BuildDistributionType.SIMULATOR,
|
||||
],
|
||||
}),
|
||||
channel: flags.string(),
|
||||
appVersion: flags.string(),
|
||||
appBuildVersion: flags.string(),
|
||||
sdkVersion: flags.string(),
|
||||
runtimeVersion: flags.string(),
|
||||
appIdentifier: flags.string(),
|
||||
buildProfile: flags.string(),
|
||||
gitCommitHash: flags.string(),
|
||||
limit: flags.integer(),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
|
||||
BuildList
|
||||
).flags;
|
||||
const { flags } = this.parse(BuildList);
|
||||
const {
|
||||
platform: requestedPlatform,
|
||||
status: buildStatus,
|
||||
distribution: buildDistribution,
|
||||
limit = 10,
|
||||
} = flags;
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -51,8 +76,19 @@ export default class BuildList extends Command {
|
||||
try {
|
||||
const builds = await BuildQuery.allForAppAsync(projectId, {
|
||||
limit,
|
||||
platform,
|
||||
status: graphqlBuildStatus,
|
||||
filter: {
|
||||
platform,
|
||||
status: graphqlBuildStatus,
|
||||
distribution: graphqlBuildDistribution,
|
||||
channel: flags.channel,
|
||||
appVersion: flags.appVersion,
|
||||
appBuildVersion: flags.appBuildVersion,
|
||||
sdkVersion: flags.sdkVersion,
|
||||
runtimeVersion: flags.runtimeVersion,
|
||||
appIdentifier: flags.appIdentifier,
|
||||
buildProfile: flags.buildProfile,
|
||||
gitCommitHash: flags.gitCommitHash,
|
||||
},
|
||||
});
|
||||
|
||||
if (builds.length) {
|
||||
@@ -89,9 +125,11 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
|
||||
}
|
||||
};
|
||||
|
||||
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
|
||||
const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | undefined => {
|
||||
if (!buildStatus) {
|
||||
return undefined;
|
||||
} else if (buildStatus === BuildStatus.NEW) {
|
||||
return GraphQLBuildStatus.New;
|
||||
} else if (buildStatus === BuildStatus.IN_QUEUE) {
|
||||
return GraphQLBuildStatus.InQueue;
|
||||
} else if (buildStatus === BuildStatus.IN_PROGRESS) {
|
||||
@@ -104,3 +142,17 @@ const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | un
|
||||
return GraphQLBuildStatus.Canceled;
|
||||
}
|
||||
};
|
||||
|
||||
const toGraphQLBuildDistribution = (
|
||||
buildDistribution?: BuildDistributionType
|
||||
): DistributionType | undefined => {
|
||||
if (buildDistribution === BuildDistributionType.STORE) {
|
||||
return DistributionType.Store;
|
||||
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
|
||||
return DistributionType.Internal;
|
||||
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
|
||||
async function createUpdateChannelOnAppAsync({
|
||||
@@ -87,12 +88,7 @@ export default class ChannelCreate extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'Channel name may not be empty.';
|
||||
@@ -144,10 +140,26 @@ export default class ChannelCreate extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.withTick(
|
||||
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
|
||||
`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!'));
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
UpdateChannelBranchMappingMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
@@ -126,12 +125,7 @@ export default class ChannelEdit extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to edit a specific channel.';
|
||||
@@ -152,13 +146,6 @@ export default class ChannelEdit extends Command {
|
||||
'There is a rollout in progress. Please manage it with "channel:rollout" instead.'
|
||||
);
|
||||
}
|
||||
const existingBranch = existingChannel.updateBranches[0];
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk`Channel {bold ${existingChannel.name}} is currently set to branch {bold ${existingBranch.name}}`
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
if (!branchName) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
@@ -191,6 +178,7 @@ export default class ChannelEdit extends Command {
|
||||
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}}.`
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -9,8 +8,8 @@ import {
|
||||
GetAllChannelsForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { logChannelDetails } from './view';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
@@ -35,12 +34,13 @@ async function getAllUpdateChannelForAppAsync({
|
||||
updateBranches(offset: 0, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
platform
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
@@ -84,12 +84,7 @@ export default class ChannelList extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
|
||||
appId: projectId,
|
||||
@@ -104,9 +99,14 @@ export default class ChannelList extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log(); // spacing
|
||||
for (const channel of channels) {
|
||||
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import chalk from 'chalk';
|
||||
|
||||
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { updateChannelBranchMappingAsync } from './edit';
|
||||
@@ -37,9 +37,7 @@ async function promptForRolloutPercentAsync({
|
||||
return rolloutPercent;
|
||||
}
|
||||
|
||||
function getRolloutInfo(
|
||||
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery
|
||||
): {
|
||||
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
|
||||
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
@@ -48,12 +46,14 @@ function getRolloutInfo(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.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 =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
|
||||
if (!newBranch || !oldBranch) {
|
||||
throw new Error(
|
||||
@@ -140,9 +140,10 @@ async function startRolloutAsync({
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
const oldBranch =
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.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}"`
|
||||
@@ -349,12 +350,8 @@ export default class ChannelRollout extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const getUpdateChannelByNameForAppResult = await getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
@@ -399,7 +396,7 @@ export default class ChannelRollout extends Command {
|
||||
percent,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
fullName: `@${accountName}/${slug}`,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import { assert } from 'console';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
GetChannelByNameForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { FormatUpdateParameter, UPDATE_COLUMNS, formatUpdate } from '../branch/list';
|
||||
import {
|
||||
FormatUpdateParameter,
|
||||
UPDATE_COLUMNS,
|
||||
formatUpdate,
|
||||
getPlatformsForGroup,
|
||||
} from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -32,9 +37,11 @@ export type BranchMapping = {
|
||||
* Get the branch mapping and determine whether it is a rollout.
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(
|
||||
branchMappingString?: string
|
||||
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
|
||||
export function getBranchMapping(branchMappingString?: string): {
|
||||
branchMapping: BranchMapping;
|
||||
isRollout: boolean;
|
||||
rolloutPercent?: number;
|
||||
} {
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
@@ -102,12 +109,13 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
updateBranches(offset: 0, limit: 1000) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
platform
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
@@ -133,7 +141,11 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
export function logChannelDetails(channel: {
|
||||
branchMapping: string;
|
||||
updateBranches: {
|
||||
updates: (FormatUpdateParameter & { runtimeVersion?: string; group?: string })[];
|
||||
updates: (FormatUpdateParameter & {
|
||||
runtimeVersion: string;
|
||||
group: string;
|
||||
platform: string;
|
||||
})[];
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
@@ -168,6 +180,10 @@ export function logChannelDetails(channel: {
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A,',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Log.log(table.toString());
|
||||
@@ -203,12 +219,7 @@ export default class ChannelView extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to view a specific channel.';
|
||||
@@ -237,9 +248,15 @@ export default class ChannelView extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
|
||||
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 by this channel, and their most recent update group:}`
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getProjectConfigDescription } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { selectAsync } from '../prompts';
|
||||
import { handleDeprecatedEasJsonAsync } from './build';
|
||||
export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export default class Config extends Command {
|
||||
static description = 'Show the eas.json config';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
profile: flags.string(),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags } = this.parse(Config);
|
||||
const { platform: maybePlatform, profile: maybeProfile } = flags as {
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
};
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, false);
|
||||
|
||||
const reader = new EasJsonReader(projectDir);
|
||||
const profileName =
|
||||
maybeProfile ??
|
||||
(await selectAsync(
|
||||
'Select build profile',
|
||||
(
|
||||
await reader.getBuildProfileNamesAsync()
|
||||
).map(profileName => ({
|
||||
title: profileName,
|
||||
value: profileName,
|
||||
}))
|
||||
));
|
||||
const platform =
|
||||
maybePlatform ??
|
||||
(await selectAsync('Select platform', [
|
||||
{
|
||||
title: 'Android',
|
||||
value: Platform.ANDROID,
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: Platform.IOS,
|
||||
},
|
||||
]));
|
||||
|
||||
const profile = await reader.readBuildProfileAsync(profileName, platform);
|
||||
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
|
||||
|
||||
Log.log(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}`);
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import { CredentialsManager } from '../credentials/CredentialsManager';
|
||||
import { createCredentialsContextAsync } from '../credentials/context';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
|
||||
@@ -9,6 +8,6 @@ export default class Credentials extends Command {
|
||||
|
||||
async run() {
|
||||
const ctx = await createCredentialsContextAsync(process.cwd(), {});
|
||||
new CredentialsManager(ctx).runActionAsync(new SelectPlatform());
|
||||
await new SelectPlatform().runAsync(ctx);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ If you are not sure what is the UDID of the device you are looking for, run:
|
||||
spinner.fail(`Couldn't find a device with the UDID ${UDID}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the device with UDID ${UDID}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AppInfoQuery, AppInfoQueryVariables>(
|
||||
gql`
|
||||
query AppInfo($appId: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default class ProjectInfo extends Command {
|
||||
static description = 'information about the current project';
|
||||
|
||||
async run() {
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const { app } = await projectInfoByIdAsync(projectId);
|
||||
if (!app) {
|
||||
throw new Error(`Could not find project with ID: ${projectId}`);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'fullName', value: app.byId.fullName },
|
||||
{ label: 'ID', value: projectId },
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
|
||||
|
||||
export default class ProjectInit extends Command {
|
||||
static description = 'create or link an EAS project';
|
||||
static aliases = ['init'];
|
||||
|
||||
async run() {
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
if (exp.extra?.eas?.projectId) {
|
||||
Log.error(
|
||||
`app.json is already linked to project with ID: ${chalk.bold(exp.extra?.eas?.projectId)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await setProjectIdAsync(projectDir);
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,25 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export enum EnvironmentSecretScope {
|
||||
ACCOUNT = 'account',
|
||||
PROJECT = 'project',
|
||||
}
|
||||
export default class EnvironmentSecretCreate extends Command {
|
||||
static description = 'Create an environment secret on the current project or owner account.';
|
||||
|
||||
@@ -34,12 +37,16 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
value: flags.string({
|
||||
description: 'Value of the secret',
|
||||
}),
|
||||
force: flags.boolean({
|
||||
description: 'Delete and recreate existing secrets',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const actor = await ensureLoggedInAsync();
|
||||
let {
|
||||
flags: { name, value: secretValue, scope },
|
||||
flags: { name, value: secretValue, scope, force },
|
||||
} = this.parse(EnvironmentSecretCreate);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
@@ -47,10 +54,7 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
@@ -93,7 +97,9 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
},
|
||||
}));
|
||||
|
||||
if (!name) throw new Error('Secret name may not be empty.');
|
||||
if (!name) {
|
||||
throw new Error('Secret name may not be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!secretValue) {
|
||||
@@ -106,10 +112,26 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
|
||||
if (!secretValue) throw new Error(validationMessage);
|
||||
if (!secretValue) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (scope === EnvironmentSecretScope.PROJECT) {
|
||||
if (force) {
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAppIdAsync(projectId);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
await EnvironmentSecretMutation.delete(existingSecret.id);
|
||||
Log.withTick(
|
||||
`Deleting existing secret ${chalk.bold(name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForApp(
|
||||
{ name, value: secretValue },
|
||||
projectId
|
||||
@@ -136,6 +158,20 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAcccountNameAsync(projectId);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
await EnvironmentSecretMutation.delete(existingSecret.id);
|
||||
Log.withTick(
|
||||
`Deleting existing secret ${chalk.bold(name)} on account ${chalk.bold(
|
||||
ownerAccount.name
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForAccount(
|
||||
{ name, value: secretValue },
|
||||
ownerAccount.id
|
||||
|
||||
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretWithScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
@@ -15,12 +16,10 @@ import {
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { EnvironmentSecretScope } from './create';
|
||||
|
||||
export default class EnvironmentSecretDelete extends Command {
|
||||
static description = `Delete an environment secret by ID.
|
||||
@@ -38,7 +37,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
@@ -55,7 +53,7 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
if (!id) {
|
||||
const validationMessage = 'You must select which secret to delete.';
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
({ secret } = await promptAsync({
|
||||
type: 'autocomplete',
|
||||
@@ -69,7 +67,9 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
|
||||
id = secret?.id;
|
||||
|
||||
if (!id) throw new Error(validationMessage);
|
||||
if (!id) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
@@ -27,7 +26,6 @@ export default class EnvironmentSecretList extends Command {
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
@@ -40,7 +38,7 @@ export default class EnvironmentSecretList extends Command {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Scope', 'ID', 'Updated at'],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { learnMore } from '../log';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
@@ -9,13 +10,12 @@ import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
const ANDROID_FLAGS = 'Android specific options';
|
||||
const IOS_FLAGS = 'iOS specific options';
|
||||
|
||||
export default class BuildSubmit extends Command {
|
||||
export default class BuildSubmit extends EasCommand {
|
||||
static description = 'Submits build artifact to app store';
|
||||
static usage = 'submit --platform=(android|ios)';
|
||||
static aliases = ['build:submit'];
|
||||
@@ -160,8 +160,6 @@ export default class BuildSubmit extends Command {
|
||||
},
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
|
||||
@@ -5,7 +5,7 @@ import gql from 'graphql-tag';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, formatUpdate } from '../branch/list';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
export async function viewUpdateAsync({
|
||||
groupId,
|
||||
@@ -80,18 +80,19 @@ export default class UpdateView extends Command {
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS, 'platforms'],
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
// The relevant info of the updates are same except for their platform
|
||||
const platforms = updatesByGroup.map(update => update.platform);
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
platforms.join(','),
|
||||
getPlatformsForGroup({
|
||||
updates: updatesByGroup,
|
||||
group: updatesByGroup[0]?.group,
|
||||
}),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Context } from './context';
|
||||
|
||||
export interface Action<T = void> {
|
||||
runAsync(manager: CredentialsManager, ctx: Context): Promise<T>;
|
||||
}
|
||||
|
||||
export class CredentialsManager {
|
||||
constructor(private ctx: Context) {}
|
||||
|
||||
public async runActionAsync<T>(action: Action<T>): Promise<T> {
|
||||
return await action.runAsync(this, this.ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { getAndroidApiMock } from '../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../__tests__/fixtures-context';
|
||||
|
||||
export const createCredentialsContextAsync = () =>
|
||||
createCtxMock({
|
||||
android: getAndroidApiMock(),
|
||||
});
|
||||
@@ -1,70 +1,134 @@
|
||||
import { AndroidCredentials } from '../android/credentials';
|
||||
import { testKeystore2Base64, testKeystoreBase64 } from './fixtures-base64-data';
|
||||
import { testExperienceName, testJester2ExperienceName } from './fixtures-constants';
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidFcmFragment,
|
||||
AndroidFcmVersion,
|
||||
AndroidKeystoreFragment,
|
||||
AndroidKeystoreType,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { Keystore } from '../android/credentials';
|
||||
import {
|
||||
testKeystore2Base64,
|
||||
testKeystoreBase64,
|
||||
testPKCS12KeystoreBase64,
|
||||
testPKCS12KeystoreEmptyPasswordBase64,
|
||||
} from './fixtures-base64-data';
|
||||
|
||||
export const testKeystore = {
|
||||
export const testKeystore: Keystore = {
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: 'ae6777e9444a436dbe533d2be46c83ba',
|
||||
keyAlias: 'QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==',
|
||||
keyPassword: '43f760fe7ecd4e6a925779eb45bc787b',
|
||||
};
|
||||
export const testKeystore2 = {
|
||||
export const testKeystore2: Keystore = {
|
||||
keystore: testKeystore2Base64,
|
||||
keystorePassword: '6faeed2326b94effadbeb731510c2378',
|
||||
keyAlias: 'QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==',
|
||||
keyPassword: 'e4829b38057042d78f25053f390478f9',
|
||||
};
|
||||
|
||||
export const testPushCredentials = {
|
||||
fcmApiKey: 'examplefcmapikey',
|
||||
// Generated by: keytool -genkey -alias test-alias -keystore keystore.p12 -storetype PKCS12 -keyalg RSA -storepass password -validity 730 -keysize 2048
|
||||
// A Keystore in a PKCS#12 file, where all entries are under aliases
|
||||
// This particular PKCS#12 has one key and one certificate under 'test-alias'
|
||||
export const testPKCS12Keystore: Keystore = {
|
||||
keystore: testPKCS12KeystoreBase64,
|
||||
keystorePassword: 'password',
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
export const testAppCredentials = {
|
||||
experienceName: testExperienceName,
|
||||
keystore: testKeystore,
|
||||
pushCredentials: testPushCredentials,
|
||||
// openssl req -new -newkey rsa:4096 -nodes -keyout test.key -out test.csr
|
||||
// openssl x509 -req -sha256 -days 365 -in test.csr -signkey test.key -out test.pem
|
||||
// openssl pkcs12 -export -in test.pem -inkey test.key -name "test-alias" -passout pass: -out emptyPassword.p12
|
||||
// cat emptyPassword.p12 | base64
|
||||
export const testPKCS12EmptyPasswordKeystore: Keystore = {
|
||||
keystore: testPKCS12KeystoreEmptyPasswordBase64,
|
||||
keystorePassword: '',
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
export const testJester2AppCredentials = {
|
||||
experienceName: testJester2ExperienceName,
|
||||
keystore: testKeystore2,
|
||||
pushCredentials: testPushCredentials,
|
||||
export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testAllCredentials: { [key: string]: AndroidCredentials } = {
|
||||
[testExperienceName]: testAppCredentials,
|
||||
export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
|
||||
id: 'test-id',
|
||||
snippet: {
|
||||
firstFourCharacters: 'abcd',
|
||||
lastFourCharacters: 'efgh',
|
||||
},
|
||||
credential: 'abcdxxxxxxefgh',
|
||||
version: AndroidFcmVersion.Legacy,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export function getApiClientWrapperMock() {
|
||||
// by default all method throw exceptions to make sure that we only call what is expected
|
||||
const getUnexpectedCallMock = () =>
|
||||
jest.fn(() => {
|
||||
throw new Error('unexpected call');
|
||||
});
|
||||
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
|
||||
id: 'test-id',
|
||||
type: AndroidKeystoreType.Jks,
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
md5CertificateFingerprint: 'test-md5',
|
||||
sha1CertificateFingerprint: 'test-sha1',
|
||||
sha256CertificateFingerprint: 'test-sha256',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: true,
|
||||
name: 'legacy',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testLegacyAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: true,
|
||||
app: testAppFragment,
|
||||
androidFcm: testLegacyAndroidFcmFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: false,
|
||||
name: 'Google App Store Build Credentials',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: false,
|
||||
app: testAppFragment,
|
||||
androidFcm: testLegacyAndroidFcmFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export function getNewAndroidApiMock() {
|
||||
return {
|
||||
getAllCredentialsApi: getUnexpectedCallMock(),
|
||||
getAllCredentialsForAppApi: getUnexpectedCallMock(),
|
||||
updateKeystoreApi: getUnexpectedCallMock(),
|
||||
updateFcmKeyApi: getUnexpectedCallMock(),
|
||||
removeKeystoreApi: getUnexpectedCallMock(),
|
||||
removeFcmKeyApi: getUnexpectedCallMock(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAndroidApiMock() {
|
||||
return {
|
||||
fetchAllAsync: jest.fn(() => testAllCredentials),
|
||||
fetchKeystoreAsync: jest.fn(() => testKeystore),
|
||||
updateKeystoreAsync: jest.fn(),
|
||||
removeKeystoreAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAndroidApiMockWithoutCredentials() {
|
||||
return {
|
||||
fetchAllAsync: jest.fn(() => []),
|
||||
fetchKeystoreAsync: jest.fn(() => null),
|
||||
updateKeystoreAsync: jest.fn(),
|
||||
removeKeystoreAsync: jest.fn(),
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
updateAndroidAppCredentialsAsync: jest.fn(),
|
||||
updateAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createKeystoreAsync: jest.fn(),
|
||||
createFcmAsync: jest.fn(),
|
||||
deleteKeystoreAsync: jest.fn(),
|
||||
deleteFcmAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -4,4 +4,15 @@ export const testKeystoreBase64 =
|
||||
export const testKeystore2Base64 =
|
||||
'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';
|
||||
|
||||
export const testPKCS12KeystoreBase64 =
|
||||
'MIIKIAIBAzCCCdoGCSqGSIb3DQEHAaCCCcsEggnHMIIJwzCCBXAGCSqGSIb3DQEHAaCCBWEEggVdMIIFWTCCBVUGCyqGSIb3DQEMCgECoIIE+jCCBPYwKAYKKoZIhvcNAQwBAzAaBBRL1ZAQAe2uDgQ/8fOeQM0bkOpjdwICBAAEggTIwbo6AGi+KXBdZL4ysxz829d7mPkEUI1ZAGJHXfJiCrEPj2qRy/fZdurAq0WUGds/kqJYu68q9ZV6x+MkOr99Fm3AB5gBfI6I8yVZl6cdoYJ9jbAXLq3Md4Yx9NE6J7Krz1st/GTRxMlcAfs+aymdAG8XbFEyMkvcFKQD3pmiwklQLbAy+I0z7cyGw4qs3FhqYDcYUCZ/AIAQEm+c10oCX6IFwi2PvmIGU8vXjf99Ee9MkMGPn4m/h2DIIYNfyHYuG7MtG65Wcb9Nc5jLXCXg5BuC4c/cnBS8MkG7MLFLhUyRY6WE5OtEQvXeq2rn8h0Eeb6GTbXqG4ZNNpXle0eRr6x8Id+VjTJGOC9rBCPwznzgLizNM2ZQmbG68XC5ZAHvoFP8r2d5rlzmNiTsGqBuIfiIKANEhdT2aAiCqHhL8WxbgOk4bnNhdQyVBFGPt5FUM9193S9fUOJK3TLMyhTnshO5vI2KxP8kFoE+ePWCltpad6w7KBio150zDRQDbUsVcf4PMgs++r1aN06AkGeqYLBsle3mT7o3IvK04k+gP243O/kPF+jvApEPDOVLVgKG5AQkZEM75L9uAQqcgZ+VidNhz87X3WvVVgBqFzjDkW4/MHFTSxwr5zI7MPjxkeGnjEeINaIkYPdsV3vGHT6RUXodZK+pW8iWaA9JlzuBvoCu6vf88Kwx9mqWKbUC/wejQS1JMscuP/IaQYoLsiCy93puopmkezoZn9EPVCtE0QULoJWdZ0xuWsr4fl/KIMpFr4ivJp+PrBP+PxmuI3RpfaGQfi3p8rMvHqwtkFWtfXFjF9DLHI0R0nUY2bcLCk96RMrqA0hcUCJpzHcxNDcgiXyBMPlACRT5JGv3E7V+1rs/SuDZaGXoijt7G6MhL2zs80OdoWNneozYJ5jMFkLvwI0zYKf8djD4vpMOGzamRnFH3xyq0ACM5RaVOGRQEWp4SFSuUSTSUQrdSi553xevd4WZwwNjCI2Kox/jPn7LzWa69wSAY1ZlgNiW8ISsMvq073IPg9wnp32ShGKAs3ivkpAGcS3HXaMHxEb0/h08kt0ee1039ldncNav0HzNdT+fuGR1spCLyvVEUJ+z3IEzFRje80QSDs2dC6ZjjnoR9CFfp5DulU97qTy04HnHI2UALBZ7VOfkbAmzqQ1+ggY12rQa0htYyw5xcJ/1zbiVyfHaOo4bM0g1kN2XK1YF3cTxnD87OhyZVktE74y22B5+D/LXPUiQP4HxmNMV4vDs4Y/km/RymKrG7U+ju8yVasIYgT6mPfQ5N2NpFtQnEW31siOMAR1PCsYDK/XYHczhuk2HF8bR5U5ev5jLc6qSEyvlAZmBQtJMJVmaLiKbvGQXeMp+v8zbqP4IuynpBts6s4ODT888sAUCrFz2dIBsodowOpxLiKmVOYlXkCpR948GwrjOYOHpBJKKMZB6ec/yT/5dgHGNvFCTnuPPhRDmrHOizmelR3n0xn3mILGEHJmBUPiXy7eZL3PjunUxPrHuvKsQo8oCvo9dJN0EjvjNVyWTjB3mKUL3d7Ce4+DToYyxeAHMv99aQ/8ZxlPkTdBYUSAhixUX2Hl6DN1thCuNNjqdqlmg91koxNmNsXrh6brd6ySPTi0R64E9MUgwIwYJKoZIhvcNAQkUMRYeFAB0AGUAcwB0AC0AYQBsAGkAYQBzMCEGCSqGSIb3DQEJFTEUBBJUaW1lIDE2MDIxNTU3ODIzMzAwggRLBgkqhkiG9w0BBwagggQ8MIIEOAIBADCCBDEGCSqGSIb3DQEHATAoBgoqhkiG9w0BDAEGMBoEFF/lSFudPAx/ZJ2iptxeZUA7a0yOAgIEAICCA/gE1nqwzU9Cw+6I7LHhFi2QZzaK2D5YMgr8xfFP/dxOD0ZUHFia7IJVJIQu22gygm5JB++L6yoptSrFWHVDvm93fQSMo3VW871+NpCqDLSMRk7pVSrwsSOP1HjSVpOQk+QoqjPf1N647SotAGdF3UxU4zQNteuoxN+vryXtA74z2TfQq2QYhLfFMb2vC/bevr2SIECR+b52NjIKoTxni07tzLKF7TOGwZh0HutdpQWS0iBnwHfKU5t0R6Mv8dB3bp9QmhMqqfM4Hj4CtnxzfAtPbfXUJSsx5j42Mhm18rnTFUL0vYQOiWLx6go5kAORSEUa6q64+ga7Zt59e028F1Q8oVviaaeUnJooVhs3kQW+/qT1OqTBWUmichzXbrA1pEedXVzVoQIEVUgbwV29jFjKfX8D9EvQv6P4bUHf9nIQyD66evzYEgNssfe3OT42IyCN/N3f+lzRQAKE3ExBeBIL/f1v3e69V/FygMmEuH6M9jTzcuV5FyC/rCArMLJZ56Zx5dVZcZ7LxTfWPb95atFJ/89FVWrpxhS6xvjOVjWFRc/SjdEju/qMN2NortcNNnnKSDms+BuEmb9gqwj/uuaHY3Q08tQCPwYeZ+4IlWxDR7O7UiEY5KY1I0ath4tWt0t/m6v7juydIOpX+rVdPCHbhZIWmuqMhU6zHb7yH+CzhIhv1E41pt75UPNRPrRCOtZgCCeS1ARp2HkB+D4TrPLKHiII1wLGCFGv/CfYm1feq8fHRItqWW7oCWdXaD234Zhc7vpqZQSjonSpk+6hVNngqzBfj2ZP9ZM/Ot5mkFm3ws0Wtp5IDQ0FXIYdksFzVIY0nRvNcBOlJzu0CmFq/WQQ6CO+Q/ApfV8ybnLSu98t44404NRu0ZNhkXIZ1pdLqb6+k6mzIw9+N/+hxPOvTf33aNnTOb1tEOPYII3t0MFkudox5c7n92+xW/SnijdEiPE6+K8iKPR/DUr2YxSUH24mIy355xgZaVRyViG+JCF1do7Bxx052UYmawTPKyS2RbqE9iXZf7SedAT2ALLwXC39rblRpwl7P+cLeu5if/qoTLDisfmgwlKXEVn8NFOR1vFzFnCZxHDrZ+EH5l9zqkeDtwHfnyeqrDj83VVTPVe4Hr9QXAqDi4kevcjWLW4VBUGsa0+L6021c1aHmWa/gzm//MrDKVS2RZPPfX5f4KUUNX/rYSvnr7vKHZ2FrpiP96w/HaGnuT3A/Nye1UqfPIYYN26L7Vr1dYolsvo2GAdX4x7EjJGpVUt8RPTSj4fcLNqXN6LdpMfrwMlkpl3wSBCoDjAn3jMmeyhic3rIGEYqlf3DpodpUNC4p4Q57rck21A4jjBycCTBZTA9MCEwCQYFKw4DAhoFAAQU3TeAb7mBYAHCMdzxMG9pfxvCLlsEFHO4kStrckWktQa7mPq269mrTHk3AgIEAA==';
|
||||
|
||||
export const testPKCS12KeystoreEmptyPasswordBase64 =
|
||||
'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
Line truncated
|
||||
|
||||
export const testProvisioningProfileBase64 = `MIIdDQYJKoZIhvcNAQcCoIIc/jCCHPoCAQExCzAJBgUrDgMCGgUAMIINoAYJKoZIhvcNAQcBoIINkQSCDY08P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+CjxkaWN0PgoJPGtleT5BcHBJRE5hbWU8L2tleT4KCTxzdHJpbmc+dGVzdGFwcCA8L3N0cmluZz4KCTxrZXk+QXBwbGljYXRpb25JZGVudGlmaWVyUHJlZml4PC9rZXk+Cgk8YXJyYXk+Cgk8c3RyaW5nPlFMNzZYWUg3M1A8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PgoJPGRhdGU+MjAyMC0wNy0xM1QxMjozNzowM1o8L2RhdGU+Cgk8a2V5PlBsYXRmb3JtPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5pT1M8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PklzWGNvZGVNYW5hZ2VkPC9rZXk+Cgk8ZmFsc2UvPgoJPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT4KCTxhcnJheT4KCQk8ZGF0YT5NSUlGb1RDQ0JJbWdBd0lCQWdJSU9UWW9sUmhEaTlrd0RRWUpLb1pJaHZjTkFRRUxCUUF3Z1pZeEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVN3d0tnWURWUVFMRENOQmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3pGRU1FSUdBMVVFQXd3N1FYQndiR1VnVjI5eWJHUjNhV1JsSUVSbGRtVnNiM0JsY2lCU1pXeGhkR2x2Ym5NZ1EyVnlkR2xtYVdOaGRHbHZiaUJCZFhSb2IzSnBkSGt3SGhjTk1qQXdOekV6TVRJeU5qRTJXaGNOTWpFd056RXpNVEl5TmpFMldqQ0JsREVhTUJnR0NnbVNKb21UOGl4a0FRRU1DbEZNTnpaWVdVZzNNMUF4T2pBNEJnTlZCQU1NTVdsUWFHOXVaU0JFYVhOMGNtbGlkWFJwYjI0NklFRnNhV05xWVNCWFlYSmphR0hGZ2lBb1VVdzNObGhaU0RjelVDa3hFekFSQmdOVkJBc01DbEZNTnpaWVdVZzNNMUF4R0RBV0JnTlZCQW9NRDBGc2FXTnFZU0JYWVhKamFHSEZnakVMTUFrR0ExVUVCaE1DVlZNd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNuTUNPT2VqWjEvUjBBTTJ3Sis0cjBDc2F3N0x3N2xFenBTREQ5OUd4TjA2ZkZ3cW1HRnBWdFhsRzhWYUN0cW1sZzl5MzJiMVltd1pJcXJRR3FteVY1cUk4RDJvTzh2QjVvRDJVT3IrUHgzOTQ0YnQyZFFZaUlHQTNlZi81SktZa1E3VDhXWXFQczg3Q1NORnZ5MHhJa1dHbFBpa0dhd0xXcmtzZmJUNFdkMU90dVhiYUtoTEZGNmp5alU0ZCtYN2NoNGtQOFFvdHlkWk5vWmltQTVTYWNtVmRidUM0YTlvQStPL3dscUliaXdiTUtGQlE2Q29tRTFKelpQaWtXMDFoZ1V2T2kvSEtKb0Nsdnp5SmZMLzhZbm43dDRSdUs5bmxHaHNIanJ5b3hFRDdiTGZpcUlKRTllcWlvNTBTK2cycFllWS9tNUh6SW04V1lOZ0RXcjFsTkFnTUJBQUdqZ2dIeE1JSUI3VEFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGSWduRndtcHRoaGdpK3pydXZaSFdjVlNWS08zTUQ4R0NDc0dBUVVGQndFQkJETXdNVEF2QmdnckJnRUZCUWN3QVlZamFIUjBjRG92TDI5amMzQXVZWEJ3YkdVdVkyOXRMMjlqYzNBd015MTNkMlJ5TVRFd2dnRWRCZ05WSFNBRWdnRVVNSUlCRURDQ0FRd0dDU3FHU0liM1kyUUZBVENCL2pDQnd3WUlLd1lCQlFVSEFnSXdnYllNZ2JOU1pXeHBZVzVqWlNCdmJpQjBhR2x6SUdObGNuUnBabWxqWVhSbElHSjVJR0Z1ZVNCd1lYSjBlU0JoYzNOMWJXVnpJR0ZqWTJWd2RHRnVZMlVnYjJZZ2RHaGxJSFJvWlc0Z1lYQndiR2xqWVdKc1pTQnpkR0Z1WkdGeVpDQjBaWEp0Y3lCaGJtUWdZMjl1WkdsMGFXOXVjeUJ2WmlCMWMyVXNJR05sY25ScFptbGpZWFJsSUhCdmJHbGplU0JoYm1RZ1kyVnlkR2xtYVdOaGRHbHZiaUJ3Y21GamRHbGpaU0J6ZEdGMFpXMWxiblJ6TGpBMkJnZ3JCZ0VGQlFjQ0FSWXFhSFIwY0RvdkwzZDNkeTVoY0hCc1pTNWpiMjB2WTJWeWRHbG1hV05oZEdWaGRYUm9iM0pwZEhrdk1CWUdBMVVkSlFFQi93UU1NQW9HQ0NzR0FRVUZCd01ETUIwR0ExVWREZ1FXQkJUTGhSdGRhRVRjRmx4Q0grZnRGQ3d4U3UvZmlUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0V3WUtLb1pJaHZkalpBWUJCQUVCL3dRQ0JRQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSEJxL2VSeitIYTRtemZ0cHZ0SGQ4TTFNb2d0c01UaVNNVDFZK0VGbUQ5UWZYdXdCM3F1ckhQbHFiMjFvV01CWU9TaWcwRDlFZG9Ta29STXAwdU5kaUltVnMycVc0bEgvQ25zL0w1QkhqU2NQc3kwbXUrSitlQjZXZzNXTDduNm1pM3BlOCtxZ2dxak41RVU2YW1Ca04wMmU3Z0pMZnlkcHRSRlBOaklOWFh6bDkxOVNDeUo2VXl3OGFUblBrZFc5eTI1UHgrUHJBSmNDVm1hbFRlYkJvNWxnbUNKejlKUkU3Q3pQd2xmaVBRbUZRMlJVTEpONTBrMmRGSmtESk1jUHd1OFdvUmNBMFE3M21NU256OVNiS21UcHhzeUNEUUo2WHJ5Sm1vKy9aQmEvM0k0dE15dkNBUVQ1alNLWjFrUXJ4c2xFdGJHZE5Bb0xUaUJ6dHY3Wjk0PTwvZGF0YT4KCTwvYXJyYXk+CgoJCQkJCQkJCQkJCgk8a2V5PkVudGl0bGVtZW50czwva2V5PgoJPGRpY3Q+CgkJPGtleT5iZXRhLXJlcG9ydHMtYWN0aXZlPC9rZXk+CgkJPHRydWUvPgoJCQkJCgkJCQk8a2V5PmFwcGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT4KCQk8c3RyaW5nPlFMNzZYWUg3M1Aub3JnLnJlYWN0anMubmF0aXZlLmV4YW1wbGUudGVzdGFwcC50dXJ0bGV2Mjwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT4KCQk8YXJyYXk+CgkJCQk8c3RyaW5nPlFMNzZYWUg3M1AuKjwvc3RyaW5nPgoJCQkJPHN0cmluZz5jb20uYXBwbGUudG9rZW48L3N0cmluZz4KCQk8L2FycmF5PgoJCQkJCgkJCQk8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+CgkJPGZhbHNlLz4KCQkJCQoJCQkJPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcjwva2V5PgoJCTxzdHJpbmc+UUw3NlhZSDczUDwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmFwcy1lbnZpcm9ubWVudDwva2V5PgoJCTxzdHJpbmc+cHJvZHVjdGlvbjwvc3RyaW5nPgoKCTwvZGljdD4KCTxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT4KCTxkYXRlPjIwMjEtMDctMTNUMTI6MjY6MTZaPC9kYXRlPgoJPGtleT5OYW1lPC9rZXk+Cgk8c3RyaW5nPm9yZy5yZWFjdGpzLm5hdGl2ZS5leGFtcGxlLnRlc3RhcHAudHVydGxldjIgcHJvZmlsPC9zdHJpbmc+Cgk8a2V5PlRlYW1JZGVudGlmaWVyPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5RTDc2WFlINzNQPC9zdHJpbmc+Cgk8L2FycmF5PgoJPGtleT5UZWFtTmFtZTwva2V5PgoJPHN0cmluZz5BbGljamEgV2FyY2hhxYI8L3N0cmluZz4KCTxrZXk+VGltZVRvTGl2ZTwva2V5PgoJPGludGVnZXI+MzY0PC9pbnRlZ2VyPgoJPGtleT5VVUlEPC9rZXk+Cgk8c3RyaW5nPjQ1NjBiNTU1LWFhODMtNDRiNy1iYjMxLTAwMGIwMDZjNDM2ZDwvc3RyaW5nPgoJPGtleT5WZXJzaW9uPC9rZXk+Cgk8aW50ZWdlcj4xPC9pbnRlZ2VyPgo8L2RpY3Q+CjwvcGxpc3Q+oIIMsjCCA/MwggLboAMCAQICARcwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTA3MDQxMjE3NDMyOFoXDTIyMDQxMjE3NDMyOFoweTELMAkGA1UEBhMCVVMxE
Line truncated
|
||||
|
||||
// Generated using instructions from https://support.magplus.com/hc/en-us/articles/203808748-iOS-Creating-a-Distribution-Certificate-and-p12-File
|
||||
// When exporting from kechain password was left blank
|
||||
export const testDistributionCertificateEmptyPasswordBase64 =
|
||||
'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';
|
||||
@@ -34,6 +34,7 @@ export const testAppJson = {
|
||||
slug: testSlug,
|
||||
sdkVersion: '38.0.0',
|
||||
ios: { bundleIdentifier: testBundleIdentifier },
|
||||
android: { package: testPackageName },
|
||||
};
|
||||
|
||||
export const testAppJsonWithDifferentOwner = {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { Context } from '../context';
|
||||
import { getAndroidApiMockWithoutCredentials } from './fixtures-android';
|
||||
import { getNewAndroidApiMock } from './fixtures-android';
|
||||
import { getAppstoreMock } from './fixtures-appstore';
|
||||
import { testAppJson, testUsername } from './fixtures-constants';
|
||||
import { getIosApiMockWithoutCredentials } from './fixtures-ios';
|
||||
import { getNewIosApiMockWithoutCredentials } from './fixtures-new-ios';
|
||||
import { getNewIosApiMock } from './fixtures-ios';
|
||||
|
||||
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
|
||||
const defaultMock = {
|
||||
ios: getIosApiMockWithoutCredentials(),
|
||||
newIos: getNewIosApiMockWithoutCredentials(),
|
||||
android: getAndroidApiMockWithoutCredentials(),
|
||||
ios: getNewIosApiMock(),
|
||||
android: getNewAndroidApiMock(),
|
||||
appStore: getAppstoreMock(),
|
||||
bestEffortAppStoreAuthenticateAsync: jest.fn(),
|
||||
ensureAppleCtx: jest.fn(),
|
||||
ensureProjectContext: jest.fn(),
|
||||
user: {
|
||||
@@ -28,14 +26,3 @@ export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
|
||||
};
|
||||
return merge(defaultMock, mockOverride) as any;
|
||||
}
|
||||
|
||||
export function createManagerMock(mockOverride: Record<string, any> = {}): CredentialsManager {
|
||||
return merge(
|
||||
{
|
||||
runActionAsync: jest.fn(() => {
|
||||
throw new Error('unexpected call'); // should be implemented in test directly
|
||||
}),
|
||||
},
|
||||
mockOverride
|
||||
) as any;
|
||||
}
|
||||
@@ -3,46 +3,21 @@ import {
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
ApplePushKeyFragment,
|
||||
AppleTeamFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../graphql/generated';
|
||||
import { IosAppCredentialsWithBuildCredentialsQueryResult } from '../ios/api/graphql/queries/IosAppCredentialsQuery';
|
||||
import {
|
||||
DistributionCertificate,
|
||||
DistributionCertificateStoreInfo,
|
||||
ProvisioningProfile,
|
||||
ProvisioningProfileStoreInfo,
|
||||
PushKey,
|
||||
PushKeyStoreInfo,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
|
||||
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
|
||||
import { Target } from '../ios/types';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfiles = [testProvisioningProfile];
|
||||
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
expires: tomorrow.getTime(),
|
||||
distributionMethod: 'test',
|
||||
certificates: [],
|
||||
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
|
||||
provisioningProfile: testProvisioningProfile.provisioningProfile,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
|
||||
|
||||
export const testAppleTeamFragment: AppleTeamFragment = {
|
||||
id: 'test-apple-team-id-1',
|
||||
@@ -60,11 +35,21 @@ export const testAppFragment: AppFragment = {
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testPushKey: ApplePushKeyFragment = {
|
||||
id: 'test-push-key-id',
|
||||
keyIdentifier: 'test-key-identifier',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
updatedAt: new Date(),
|
||||
iosAppCredentialsList: [],
|
||||
};
|
||||
|
||||
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
|
||||
|
||||
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
|
||||
id: 'test-prov-prof-id-1',
|
||||
expiration: new Date(),
|
||||
developerPortalIdentifier: 'test-developer-identifier',
|
||||
provisioningProfile: 'test-provisioning-profile',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
updatedAt: new Date(),
|
||||
status: 'Active',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
@@ -117,127 +102,46 @@ export const testIosAppBuildCredentialsFragment: IosAppBuildCredentialsFragment
|
||||
provisioningProfile: testProvisioningProfileFragment,
|
||||
};
|
||||
|
||||
export const testIosAppCredentialsWithBuildCredentialsQueryResult: IosAppCredentialsWithBuildCredentialsQueryResult = {
|
||||
id: 'test-app-credential-id',
|
||||
iosAppBuildCredentialsArray: [testIosAppBuildCredentialsFragment],
|
||||
export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragment = {
|
||||
id: 'test-common-ios-app-credentials-id',
|
||||
app: testAppFragment,
|
||||
appleTeam: testAppleTeamFragment,
|
||||
appleAppIdentifier: testAppleAppIdentifierFragment,
|
||||
pushKey: testPushKey,
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
|
||||
export function getNewIosApiMock() {
|
||||
return {
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
createOrGetIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
updateIosAppCredentialsAsync: jest.fn(),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
createPushKeyAsync: jest.fn(),
|
||||
getPushKeyForAppAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export const testDistCert: DistributionCertificate = {
|
||||
certP12: 'Y2VydHAxMg==',
|
||||
certPassword: 'test-password',
|
||||
distCertSerialNumber: 'test-serial',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
export const testIosDistCredential: IosDistCredentials = {
|
||||
id: 1,
|
||||
type: 'dist-cert',
|
||||
...testDistCert,
|
||||
};
|
||||
export const testIosDistCredentials = [testIosDistCredential];
|
||||
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
|
||||
id: 'test-id',
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
created: today.getTime(),
|
||||
expires: tomorrow.getTime(),
|
||||
ownerName: 'test-owner',
|
||||
ownerId: 'test-id',
|
||||
serialNumber: testIosDistCredential.distCertSerialNumber as string,
|
||||
};
|
||||
export const testDistCertsFromApple = [testDistCertFromApple];
|
||||
|
||||
export const testPushKey: PushKey = {
|
||||
apnsKeyP8: 'test-p8',
|
||||
apnsKeyId: 'test-key-id',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
|
||||
export const testIosPushCredential: IosPushCredentials = {
|
||||
id: 2,
|
||||
type: 'push-key',
|
||||
...testPushKey,
|
||||
};
|
||||
export const testIosPushCredentials = [testIosPushCredential];
|
||||
export const testPushKeyFromApple: PushKeyStoreInfo = {
|
||||
id: testIosPushCredential.apnsKeyId,
|
||||
name: 'test-name',
|
||||
};
|
||||
export const testPushKeysFromApple = [testPushKeyFromApple];
|
||||
export const testLegacyPushCert = {
|
||||
pushId: 'test-push-id',
|
||||
pushP12: 'test-push-p12',
|
||||
pushPassword: 'test-push-password',
|
||||
};
|
||||
export const testAppCredential = {
|
||||
experienceName: testExperienceName,
|
||||
bundleIdentifier: testBundleIdentifier,
|
||||
distCredentialsId: testIosDistCredential.id,
|
||||
pushCredentialsId: testIosPushCredential.id,
|
||||
credentials: {
|
||||
...testProvisioningProfile,
|
||||
},
|
||||
};
|
||||
export const testAllCredentialsForApp = {
|
||||
...testAppCredential,
|
||||
pushCredentials: testPushKey,
|
||||
distCredentials: testDistCert,
|
||||
};
|
||||
export const testAppCredentials = [testAppCredential];
|
||||
export const testAllCredentials = {
|
||||
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
|
||||
appCredentials: testAppCredentials,
|
||||
};
|
||||
|
||||
export function getIosApiMock() {
|
||||
return {
|
||||
getAllCredentials: jest.fn(() => testAllCredentials),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getDistributionCertificateByIdAsync: jest.fn(id =>
|
||||
id === testIosDistCredential.id ? testIosDistCredential : null
|
||||
),
|
||||
createDistributionCertificateAsync: jest.fn((owner, cert) => ({
|
||||
id: 123,
|
||||
type: 'dist-cert',
|
||||
...cert,
|
||||
})),
|
||||
updateDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
useDistributionCertificateAsync: jest.fn(),
|
||||
getPushKey: jest.fn(() => testIosPushCredential),
|
||||
createPushKeyAsync: jest.fn((owner, key) => ({ id: 124, type: 'push-key', ...key })),
|
||||
updatePushKeyAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
usePushKeyAsync: jest.fn(),
|
||||
|
||||
getAppCredentialsAsync: jest.fn(() => testAppCredentials),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfileAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getIosApiMockWithoutCredentials() {
|
||||
return {
|
||||
getAllCredentials: jest.fn(() => ({ appCredentials: [], userCredentials: [] })),
|
||||
getDistributionCertificateAsync: jest.fn(() => null),
|
||||
getDistributionCertificateByIdAsync: jest.fn(() => {
|
||||
throw new Error('not found');
|
||||
}),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
updateDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
useDistributionCertificateAsync: jest.fn(),
|
||||
getPushKeyAsync: jest.fn(() => null),
|
||||
createPushKeyAsync: jest.fn(),
|
||||
updatePushKeyAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
usePushKeyAsync: jest.fn(),
|
||||
getAppCredentialsAsync: jest.fn(app => ({
|
||||
experienceName: `${app.accountName}/${app.projectName}`,
|
||||
bundleIdentifier: app.bundleIdentifier,
|
||||
})),
|
||||
getProvisioningProfileAsync: jest.fn(() => null),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfileAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import {
|
||||
DistributionCertificate,
|
||||
DistributionCertificateStoreInfo,
|
||||
ProvisioningProfile,
|
||||
ProvisioningProfileStoreInfo,
|
||||
PushKey,
|
||||
PushKeyStoreInfo,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfiles = [testProvisioningProfile];
|
||||
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
expires: tomorrow.getTime(),
|
||||
distributionMethod: 'test',
|
||||
certificates: [],
|
||||
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
|
||||
provisioningProfile: testProvisioningProfile.provisioningProfile,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
|
||||
|
||||
export const testDistCert: DistributionCertificate = {
|
||||
certP12: 'Y2VydHAxMg==',
|
||||
certPassword: 'test-password',
|
||||
distCertSerialNumber: 'test-serial',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
export const testIosDistCredential: IosDistCredentials = {
|
||||
id: 1,
|
||||
type: 'dist-cert',
|
||||
...testDistCert,
|
||||
};
|
||||
export const testIosDistCredentials = [testIosDistCredential];
|
||||
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
|
||||
id: 'test-id',
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
created: today.getTime(),
|
||||
expires: tomorrow.getTime(),
|
||||
ownerName: 'test-owner',
|
||||
ownerId: 'test-id',
|
||||
serialNumber: testIosDistCredential.distCertSerialNumber as string,
|
||||
};
|
||||
export const testDistCertsFromApple = [testDistCertFromApple];
|
||||
|
||||
export const testPushKey: PushKey = {
|
||||
apnsKeyP8: 'test-p8',
|
||||
apnsKeyId: 'test-key-id',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
|
||||
export const testIosPushCredential: IosPushCredentials = {
|
||||
id: 2,
|
||||
type: 'push-key',
|
||||
...testPushKey,
|
||||
};
|
||||
export const testIosPushCredentials = [testIosPushCredential];
|
||||
export const testPushKeyFromApple: PushKeyStoreInfo = {
|
||||
id: testIosPushCredential.apnsKeyId,
|
||||
name: 'test-name',
|
||||
};
|
||||
export const testPushKeysFromApple = [testPushKeyFromApple];
|
||||
export const testLegacyPushCert = {
|
||||
pushId: 'test-push-id',
|
||||
pushP12: 'test-push-p12',
|
||||
pushPassword: 'test-push-password',
|
||||
};
|
||||
export const testAppCredential = {
|
||||
experienceName: testExperienceName,
|
||||
bundleIdentifier: testBundleIdentifier,
|
||||
distCredentialsId: testIosDistCredential.id,
|
||||
pushCredentialsId: testIosPushCredential.id,
|
||||
credentials: {
|
||||
...testProvisioningProfile,
|
||||
},
|
||||
};
|
||||
export const testAllCredentialsForApp = {
|
||||
...testAppCredential,
|
||||
pushCredentials: testPushKey,
|
||||
distCredentials: testDistCert,
|
||||
};
|
||||
export const testAppCredentials = [testAppCredential];
|
||||
export const testAllCredentials = {
|
||||
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
|
||||
appCredentials: testAppCredentials,
|
||||
};
|
||||
|
||||
export function getNewIosApiMockWithoutCredentials() {
|
||||
return {
|
||||
getAppAsync: jest.fn(),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewIosApiMockWithCredentials() {
|
||||
return {
|
||||
getAppAsync: jest.fn(),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: jest.fn(),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,20 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { Keystore } from './credentials';
|
||||
|
||||
export interface AndroidCredentials {
|
||||
keystore: Keystore;
|
||||
}
|
||||
|
||||
interface AppLookupParams {
|
||||
projectName: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider {
|
||||
@@ -26,11 +22,6 @@ export default class AndroidCredentialsProvider {
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
private get projectFullName(): string {
|
||||
const { projectName, accountName } = this.options.app;
|
||||
return `@${accountName}/${projectName}`;
|
||||
}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
): Promise<AndroidCredentials> {
|
||||
@@ -43,14 +34,22 @@ export default class AndroidCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
await new CredentialsManager(this.ctx).runActionAsync(
|
||||
new SetupBuildCredentials(this.projectFullName)
|
||||
);
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
if (!keystore) {
|
||||
throw new Error('Unable to set up credentials, failed to fetch keystore from EAS servers');
|
||||
}
|
||||
return { keystore };
|
||||
const setupBuildCredentialsAction = await new SetupBuildCredentials({ app: this.options.app });
|
||||
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
|
||||
return this.toAndroidCredentials(buildCredentials);
|
||||
}
|
||||
|
||||
private toAndroidCredentials(
|
||||
androidBuildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): AndroidCredentials {
|
||||
return {
|
||||
keystore: {
|
||||
keystore: nullthrows(androidBuildCredentials.androidKeystore?.keystore),
|
||||
keystorePassword: nullthrows(androidBuildCredentials.androidKeystore?.keystorePassword),
|
||||
keyAlias: nullthrows(androidBuildCredentials.androidKeystore?.keyAlias),
|
||||
keyPassword: androidBuildCredentials.androidKeystore?.keyPassword ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<AndroidCredentials> {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
AndroidFcmFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export class AssignFcm {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
fcm: AndroidFcmFragment
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const appCredentials =
|
||||
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
|
||||
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
|
||||
appCredentials,
|
||||
{
|
||||
androidFcmId: fcm.id,
|
||||
}
|
||||
);
|
||||
Log.succeed(`FCM API Key assigned to ${this.app.androidApplicationIdentifier}`);
|
||||
return updatedAppCredentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import assert from 'assert';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { getApplicationIdAsync } from '../../../project/android/applicationId';
|
||||
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
/**
|
||||
* Legacy credentials can be copied over to EAS if the user does not have
|
||||
* EAS credentials set up yet
|
||||
*/
|
||||
export async function canCopyLegacyCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams
|
||||
): Promise<boolean> {
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
if (appCredentials) {
|
||||
return false; // modern credentials already exist
|
||||
}
|
||||
|
||||
const legacyAppCredentials =
|
||||
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
return !!legacyAppCredentials; // user has some legacy credentials
|
||||
}
|
||||
|
||||
export async function promptUserAndCopyLegacyCredentialsAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams
|
||||
): Promise<void> {
|
||||
assert(
|
||||
await canCopyLegacyCredentialsAsync(ctx, app),
|
||||
'User not eligible to copy classic build credentials to EAS'
|
||||
);
|
||||
|
||||
const spinner = ora('Classic credentials detected, copying to EAS...').start();
|
||||
|
||||
try {
|
||||
const legacyAppCredentials =
|
||||
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
|
||||
if (!legacyAppCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appCredentials =
|
||||
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(app);
|
||||
const legacyFcm = legacyAppCredentials.androidFcm;
|
||||
if (legacyFcm) {
|
||||
const clonedFcm = await ctx.android.createFcmAsync(
|
||||
app.account,
|
||||
legacyFcm.credential,
|
||||
legacyFcm.version
|
||||
);
|
||||
await ctx.android.updateAndroidAppCredentialsAsync(appCredentials, {
|
||||
androidFcmId: clonedFcm.id,
|
||||
});
|
||||
}
|
||||
|
||||
const legacyBuildCredentials = await ctx.android.getLegacyAndroidAppBuildCredentialsAsync(app);
|
||||
const legacyKeystore = legacyBuildCredentials?.androidKeystore ?? null;
|
||||
|
||||
if (legacyKeystore) {
|
||||
const clonedKeystore = await ctx.android.createKeystoreAsync(app.account, {
|
||||
keystore: legacyKeystore.keystore,
|
||||
keystorePassword: legacyKeystore.keystorePassword,
|
||||
keyAlias: legacyKeystore.keyAlias,
|
||||
keyPassword: legacyKeystore.keyPassword ?? undefined,
|
||||
type: legacyKeystore.type,
|
||||
});
|
||||
await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
|
||||
androidKeystoreId: clonedKeystore.id,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
spinner.fail(`Unable to migrate credentials to EAS.`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
spinner.succeed('Credentials copied to EAS.');
|
||||
}
|
||||
|
||||
export async function getAppLookupParamsFromContextAsync(ctx: Context): Promise<AppLookupParams> {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
|
||||
if (!androidApplicationIdentifier) {
|
||||
throw new Error(
|
||||
`android.package needs to be defined in your ${getProjectConfigDescription(
|
||||
ctx.projectDir
|
||||
)} file`
|
||||
);
|
||||
}
|
||||
|
||||
return { account, projectName, androidApplicationIdentifier };
|
||||
}
|
||||
|
||||
export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
appLookupParams: AppLookupParams,
|
||||
{
|
||||
androidKeystoreId,
|
||||
}: {
|
||||
androidKeystoreId: string;
|
||||
}
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
assert(
|
||||
!ctx.nonInteractive,
|
||||
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
|
||||
);
|
||||
const existingDefaultBuildCredentials =
|
||||
await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(appLookupParams);
|
||||
if (existingDefaultBuildCredentials) {
|
||||
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
|
||||
existingDefaultBuildCredentials,
|
||||
{ androidKeystoreId }
|
||||
);
|
||||
}
|
||||
return await ctx.android.createAndroidAppBuildCredentialsAsync(appLookupParams, {
|
||||
name: generateRandomName(),
|
||||
isDefault: true,
|
||||
androidKeystoreId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptForNameAsync(): Promise<string> {
|
||||
const { providedName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'providedName',
|
||||
message: 'Assign a name to your build credentials:',
|
||||
initial: generateRandomName(),
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
return providedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort a build credentials array in descending order of preference
|
||||
* prefer default credentials, then prefer names that come first lexicographically
|
||||
*/
|
||||
export function sortBuildCredentials(
|
||||
androidAppBuildCredentialsList: AndroidAppBuildCredentialsFragment[]
|
||||
): AndroidAppBuildCredentialsFragment[] {
|
||||
return androidAppBuildCredentialsList.sort((buildCredentialsA, buildCredentialsB) => {
|
||||
if (buildCredentialsA.isDefault) {
|
||||
return -1;
|
||||
} else if (buildCredentialsB.isDefault) {
|
||||
return 1;
|
||||
}
|
||||
return buildCredentialsA.name.localeCompare(buildCredentialsB.name);
|
||||
});
|
||||
}
|
||||
|
||||
function generateRandomName(): string {
|
||||
return `Build Credentials ${nanoid(10)}`;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { AndroidFcmFragment, AndroidFcmVersion } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
|
||||
export class CreateFcm {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AndroidFcmFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`FCM API Key cannot be uploaded in non-interactive mode.`);
|
||||
}
|
||||
const { fcmApiKey } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fcmApiKey',
|
||||
message: 'FCM API Key',
|
||||
validate: (value: string) => value.length > 0 || "FCM API Key can't be empty",
|
||||
},
|
||||
]);
|
||||
const fcmFragment = await ctx.android.createFcmAsync(
|
||||
this.account,
|
||||
fcmApiKey,
|
||||
AndroidFcmVersion.Legacy
|
||||
);
|
||||
Log.succeed('Uploaded FCM API Key');
|
||||
return fcmFragment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
import { KeystoreWithType, keystoreSchema } from '../credentials';
|
||||
import { generateRandomKeystoreAsync } from '../utils/keystore';
|
||||
import { getKeystoreWithType, validateKeystore } from '../utils/keystoreNew';
|
||||
|
||||
export class CreateKeystore {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AndroidKeystoreFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`New keystore cannot be created in non-interactive mode.`);
|
||||
}
|
||||
const keystore = await this.provideOrGenerateAsync();
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
|
||||
Log.succeed('Created keystore');
|
||||
return keystoreFragment;
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(): Promise<KeystoreWithType> {
|
||||
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
|
||||
if (providedKeystore) {
|
||||
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
|
||||
validateKeystore(providedKeystoreWithType);
|
||||
return providedKeystoreWithType;
|
||||
}
|
||||
return await generateRandomKeystoreAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,82 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { maybeRenameExistingFileAsync } from '../../../utils/files';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams, formatProjectFullName } from '../api/GraphqlClient';
|
||||
import { displayAndroidKeystore } from '../utils/printCredentials';
|
||||
|
||||
interface DownloadKeystoreOptions {
|
||||
displayCredentials?: boolean;
|
||||
app: AppLookupParams;
|
||||
displaySensitiveInformation?: boolean;
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
export class DownloadKeystore implements Action {
|
||||
constructor(private projectFullName: string, private options?: DownloadKeystoreOptions) {}
|
||||
export class DownloadKeystore {
|
||||
constructor(private options: DownloadKeystoreOptions) {}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const keystore = await ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
const keystore = buildCredentials.androidKeystore;
|
||||
if (!keystore) {
|
||||
Log.warn('There is no valid Keystore defined for this app');
|
||||
Log.warn(
|
||||
`There is no valid Keystore defined for build credentials: ${buildCredentials.name}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let displayCredentials = this.options?.displayCredentials;
|
||||
if (displayCredentials === undefined && !ctx.nonInteractive) {
|
||||
displayCredentials = await confirmAsync({
|
||||
message: 'Do you want to display the Android Keystore credentials?',
|
||||
let displaySensitiveInformation = this.options?.displaySensitiveInformation;
|
||||
if (displaySensitiveInformation === undefined && !ctx.nonInteractive) {
|
||||
displaySensitiveInformation = await confirmAsync({
|
||||
message: 'Do you want to display the sensitive information of the Android Keystore?',
|
||||
});
|
||||
}
|
||||
|
||||
const keystorePath =
|
||||
this.options?.outputPath ?? `${this.projectFullName.replace('/', '__')}.jks`;
|
||||
const projectFullName = formatProjectFullName(this.options.app);
|
||||
const keystorePath = this.options?.outputPath ?? `${projectFullName.replace('/', '__')}.jks`;
|
||||
|
||||
await maybeRenameExistingFileAsync(ctx.projectDir, keystorePath);
|
||||
|
||||
Log.log(`Saving Keystore to ${keystorePath}`);
|
||||
await fs.writeFile(keystorePath, keystore.keystore, 'base64');
|
||||
|
||||
if (displayCredentials) {
|
||||
Log.log(`Keystore credentials
|
||||
Keystore password: ${chalk.bold(keystore.keystorePassword)}
|
||||
Key alias: ${chalk.bold(keystore.keyAlias)}
|
||||
Key password: ${chalk.bold(keystore.keyPassword)}
|
||||
// non-sensitive information
|
||||
displayAndroidKeystore(keystore);
|
||||
|
||||
Path to Keystore: ${keystorePath}
|
||||
if (displaySensitiveInformation) {
|
||||
Log.newLine();
|
||||
Log.log(`Sensitive Keystore information:
|
||||
Keystore password: ${chalk.bold(keystore.keystorePassword)}
|
||||
Key alias: ${chalk.bold(keystore.keyAlias)}
|
||||
Key password: ${chalk.bold(keystore.keyPassword)}
|
||||
|
||||
Path to Keystore: ${keystorePath}
|
||||
`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupKeystore implements Action {
|
||||
constructor(private projectFullName: string) {}
|
||||
export class BackupKeystore {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
|
||||
Log.warn('Backing up your old Android Keystore now...');
|
||||
await manager.runActionAsync(
|
||||
new DownloadKeystore(this.projectFullName, {
|
||||
displayCredentials: true,
|
||||
outputPath: `${this.projectFullName}.bak.jks`.replace('/', '__'),
|
||||
})
|
||||
);
|
||||
public async runAsync(
|
||||
ctx: Context,
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): Promise<void> {
|
||||
if (!buildCredentials.androidKeystore) {
|
||||
return;
|
||||
}
|
||||
const projectFullName = formatProjectFullName(this.app);
|
||||
Log.warn('Backing up your old Android Keystore now...');
|
||||
await new DownloadKeystore({
|
||||
app: this.app,
|
||||
outputPath: `${projectFullName}.bak.jks`.replace('/', '__'),
|
||||
displaySensitiveInformation: true,
|
||||
}).runAsync(ctx, buildCredentials);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
|
||||
export class UpdateFcmKey implements Action {
|
||||
constructor(private experienceName: string) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const { fcmApiKey } = await promptAsync([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fcmApiKey',
|
||||
message: 'FCM API Key',
|
||||
validate: (value: string) => value.length > 0 || "FCM API Key can't be empty",
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.android.updateFcmKeyAsync(this.experienceName, fcmApiKey);
|
||||
Log.succeed('Updated successfully');
|
||||
}
|
||||
}
|
||||
Loaded 100 of 327 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user