mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
05b8eef622 | ||
|
|
50e56b5bc4 | ||
|
|
30ab4e62ec | ||
|
|
a1d901294b | ||
|
|
eed818cfe5 | ||
|
|
db0b6bfcf5 | ||
|
|
da81c1c6df | ||
|
|
42d3296f07 | ||
|
|
f9b4b1a6e8 | ||
|
|
d03603a75f | ||
|
|
ffa9e5ef66 | ||
|
|
e7f3cf9c74 | ||
|
|
e82a4a0a57 | ||
|
|
175f07fac9 | ||
|
|
2869744a5d | ||
|
|
99f898a483 | ||
|
|
1448db96e6 | ||
|
|
e02036a46a | ||
|
|
09f33263cb | ||
|
|
412b1ec74c | ||
|
|
9a1538fb4d | ||
|
|
0ad9b31ee3 | ||
|
|
4cbb7bb2f2 | ||
|
|
ae109cd8c6 | ||
|
|
fbaba774a6 | ||
|
|
38bc8062a4 | ||
|
|
d41d184b55 | ||
|
|
d6d32a7a7d | ||
|
|
15a9532fa7 | ||
|
|
3b9fb724fe | ||
|
|
33767b5635 | ||
|
|
175fc2b30e | ||
|
|
aec9347d69 | ||
|
|
91500d07c0 | ||
|
|
62ca14b504 | ||
|
|
9158a95d57 | ||
|
|
d0f3e387e7 | ||
|
|
158856f8ec | ||
|
|
1e59957753 | ||
|
|
f97d46040c | ||
|
|
d7485cb5d2 | ||
|
|
071fdb7f4a | ||
|
|
6dd96ffb20 | ||
|
|
f56fb0f7de | ||
|
|
7d28f6add9 | ||
|
|
eebef3e461 | ||
|
|
8336f630ea | ||
|
|
7d6bdbb6ed | ||
|
|
403b5c237f | ||
|
|
2367ed7911 |
No files matched your search
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 |
@@ -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
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn lerna run build
|
||||
- run: yarn typecheck
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
|
||||
+235
-1
@@ -6,15 +6,249 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 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))
|
||||
|
||||
### 🐛 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))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [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
|
||||
|
||||
- Add validation when setting up ad-hoc provisioning profile in non-interactive mode. ([#338](https://github.com/expo/eas-cli/pull/338) by [@dsokal](https://github.com/dsokal))
|
||||
- Allow for registering devices when running an ad-hoc build for the first time. ([#340](https://github.com/expo/eas-cli/pull/340) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- 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))
|
||||
|
||||
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `secrets:<COMMAND>` is renamed to `secret:<COMMAND>` ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secret:delete` now accepts `id` through a flag rather than an argument ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the behavior where the provisioning profile was invalidated after syncing bundle id capabilities. ([#334](https://github.com/expo/eas-cli/pull/334) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Change build credentials summary format. ([#321](https://github.com/expo/eas-cli/pull/321) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.9.1](https://github.com/expo/eas-cli/releases/tag/v0.9.1) - 2021-04-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix provisioning profile validation to actually check status. ([#318](https://github.com/expo/eas-cli/pull/318) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace Credentials REST API calls with GraphQL counterparts, also improved DX. ([#293](https://github.com/expo/eas-cli/pull/293), [#299](https://github.com/expo/eas-cli/pull/299), [#301](https://github.com/expo/eas-cli/pull/301), [#317](https://github.com/expo/eas-cli/pull/317) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.9.0](https://github.com/expo/eas-cli/releases/tag/v0.9.0) - 2021-04-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
|
||||
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
|
||||
- Bump @expo/config-plugins to fix `eas build:configure` on Windows. ([d0f3e](https://github.com/expo/eas-cli/commit/d0f3e387e7f31dbce4c3b93b8d083c7d20d9e30d) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Replace REST API calls with GraphQL counterparts. ([#333](https://github.com/expo/eas-cli/pull/333) by [@barthap](https://github.com/barthap))
|
||||
|
||||
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue where the build status never got updated when running `eas build`. ([#310](https://github.com/expo/eas-cli/pull/310) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.8.0](https://github.com/expo/eas-cli/releases/tag/v0.8.0) - 2021-04-06
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Change the way of disabling cache - add `cache.disabled` field. ([#295](https://github.com/expo/eas-cli/pull/295) by [@dsokal](https://github.com/dsokal))
|
||||
- `secrets:create` now uses flags rather than positional arguments ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:create`'s `target` arg is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:list`'s `target` property is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:delete`'s `ID` arg is now optional ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:delete`'s now allows users to choose secrets from a list ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `build:view` and `build:list` now showing the distribution type (store / internal) and release channel. ([#284](https://github.com/expo/eas-cli/pull/284) by [@vthibault](https://github.com/vthibault))
|
||||
|
||||
- Add analytics to EAS Build. ([#162](https://github.com/expo/eas-cli/pull/162) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Improve tar archive support in EAS Submit. ([#297](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
|
||||
|
||||
+36
-6
@@ -1,9 +1,39 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Add release notes to `CHANGELOG.md`.
|
||||
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 ready to go, you can go to
|
||||
https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
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. 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.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.8.0"
|
||||
"version": "0.18.2"
|
||||
}
|
||||
@@ -6,6 +6,9 @@
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"release": "lerna version",
|
||||
|
||||
+118
-33
@@ -47,10 +47,15 @@ eas --help COMMAND
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas secrets:create`](#eas-secretscreate)
|
||||
* [`eas secrets:delete ID`](#eas-secretsdelete-id)
|
||||
* [`eas secrets:list`](#eas-secretslist)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
* [`eas webhook:update`](#eas-webhookupdate)
|
||||
* [`eas webhook:view ID`](#eas-webhookview-id)
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
@@ -64,7 +69,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -78,7 +83,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -92,7 +97,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -103,7 +108,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -115,14 +120,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.8.0/build/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -133,7 +139,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -145,10 +151,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.8.0/build/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -161,10 +166,10 @@ USAGE
|
||||
OPTIONS
|
||||
--limit=limit
|
||||
--platform=(all|android|ios)
|
||||
--status=(in-queue|in-progress|errored|finished|canceled)
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -175,7 +180,7 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -186,7 +191,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -197,7 +202,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -211,7 +216,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -222,7 +227,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -241,13 +246,13 @@ OPTIONS
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
|
||||
## `eas secrets:create`
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:create
|
||||
$ eas secret:create
|
||||
|
||||
OPTIONS
|
||||
--name=name Name of the secret
|
||||
@@ -255,35 +260,35 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [build/commands/secrets/create.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/secrets/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secrets:delete ID`
|
||||
## `eas secret:delete`
|
||||
|
||||
Delete an environment secret by ID.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:delete ID
|
||||
$ eas secret:delete
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the secret to delete
|
||||
OPTIONS
|
||||
--id=id ID of the secret to delete
|
||||
|
||||
DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secrets/delete.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/secrets/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secrets:list`
|
||||
## `eas secret:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:list
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secrets/list.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/secrets/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
@@ -309,8 +314,7 @@ OPTIONS
|
||||
|
||||
iOS specific options=asc-app-id App Store Connect unique application Apple ID
|
||||
number. Providing this param results in skipping
|
||||
app creation step. Learn more:
|
||||
https://expo.fyi/asc-app-id
|
||||
app creation step. Learn more.
|
||||
|
||||
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
|
||||
expo.ios.bundleIdentifier from app config)
|
||||
@@ -363,5 +367,86 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.8.0/build/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
Create a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:create
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
|
||||
header.
|
||||
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
Delete a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:delete [ID]
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
List webhooks on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:list
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
Create a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:update
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
--id=id (required) Webhook ID
|
||||
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
|
||||
header.
|
||||
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
View a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:view ID
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.18.2/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -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:
|
||||
|
||||
+3582
-1163
File diff suppressed because it is too large.
Load diff
@@ -1,67 +1,74 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.8.0",
|
||||
"version": "0.18.2",
|
||||
"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.21-alpha.0",
|
||||
"@expo/eas-build-job": "0.2.22",
|
||||
"@expo/eas-json": "^0.8.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/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": "^1.13.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.5.0",
|
||||
"@amplitude/node": "1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.20",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "2.0.2",
|
||||
"@expo/eas-build-job": "0.2.37",
|
||||
"@expo/eas-json": "^0.18.2",
|
||||
"@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.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",
|
||||
"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": "1.14.1",
|
||||
"turndown": "7.0.0",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
@@ -161,11 +168,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",
|
||||
"build": "tsc",
|
||||
"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,9 @@
|
||||
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);
|
||||
|
||||
console.log('Patched README path generation');
|
||||
@@ -21,7 +21,7 @@ describe('apiClient', () => {
|
||||
],
|
||||
});
|
||||
|
||||
let error: Error;
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
@@ -41,7 +41,7 @@ describe('apiClient', () => {
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error;
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
|
||||
@@ -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,294 +0,0 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
|
||||
jest.mock('prompts');
|
||||
|
||||
function createMockCredentialsProvider({
|
||||
hasRemote,
|
||||
hasLocal,
|
||||
isLocalSynced,
|
||||
}: any): CredentialsProvider {
|
||||
return {
|
||||
platform: 'android',
|
||||
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
|
||||
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
|
||||
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
|
||||
} as CredentialsProvider;
|
||||
}
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(ensureCredentialsAsync, () => {
|
||||
describe('for generic builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'local' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask when local and remote are not the same (select remote)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'remote' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on EAS servers'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('should ask when no credentials are present and return remote if confirm=true', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: true }; //confirm
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should ask when no credentials are present and abort if confimr=false', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: false }; //confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, false);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Aborting build process, credentials are not configured for Android'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should throw an error when no credentials are present in non interactive mode', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
return { value: true }; // confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Credentials for this app are not configured');
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for managed builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
|
||||
it('should use credentials.json even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to local', () => {
|
||||
it('should use credentials.json without asking even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.LOCAL,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to remote', () => {
|
||||
it('should use credentials from server even if credentials.json exists and have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.REMOTE,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,7 +10,7 @@ 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)) {
|
||||
@@ -22,12 +22,7 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
|
||||
}
|
||||
|
||||
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 +47,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 +62,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 +70,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 +79,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 +88,28 @@ 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,
|
||||
'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY' //TODO-JJ AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
);
|
||||
if (!stringifiedRequestHeaders) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
console.log({ 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,114 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readVersionCode, readVersionName } 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(readVersionCode, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version code from native code', () => {
|
||||
const exp = initGenericProject();
|
||||
const versionCode = readVersionCode('/repo', exp);
|
||||
expect(versionCode).toBe(123);
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version code from expo config', () => {
|
||||
const exp = initManagedProject();
|
||||
const versionCode = readVersionCode('/repo', exp);
|
||||
expect(versionCode).toBe(123);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readVersionName, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version name from native code', () => {
|
||||
const exp = initGenericProject();
|
||||
const versionName = readVersionName('/repo', exp);
|
||||
expect(versionName).toBe('1.0');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version from expo config', () => {
|
||||
const exp = initManagedProject();
|
||||
const versionName = readVersionName('/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"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
'/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
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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 nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
@@ -10,13 +11,17 @@ import AndroidCredentialsProvider, {
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationId,
|
||||
} from '../../project/android/applicationId';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
@@ -24,7 +29,7 @@ import { prepareJobAsync } from './prepareJob';
|
||||
export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.ANDROID>({
|
||||
commandCtx,
|
||||
platform: Platform.ANDROID,
|
||||
@@ -63,11 +68,12 @@ 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 (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
@@ -113,24 +119,27 @@ async function ensureAndroidCredentialsAsync(
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = getApplicationId(
|
||||
ctx.commandCtx.projectDir,
|
||||
ctx.commandCtx.exp
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
|
||||
`You do not have access to account: ${ctx.commandCtx.accountName}`
|
||||
),
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
ctx.buildProfile.workflow,
|
||||
ctx.buildProfile.credentialsSource,
|
||||
ctx.commandCtx.nonInteractive
|
||||
);
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
|
||||
@@ -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,6 +1,10 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidGenericJobInput, AndroidManagedJobInput } from '../../graphql/generated';
|
||||
import {
|
||||
AndroidGenericJobInput,
|
||||
AndroidManagedBuildType,
|
||||
AndroidManagedJobInput,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
|
||||
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
|
||||
@@ -8,6 +12,7 @@ export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobI
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
@@ -21,9 +26,21 @@ export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobI
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
username: job.username,
|
||||
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;
|
||||
} else {
|
||||
return AndroidManagedBuildType.DevelopmentClient;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,16 @@
|
||||
import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } 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';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
@@ -46,7 +38,6 @@ interface CommonJobProperties {
|
||||
buildCredentials?: {
|
||||
keystore: Android.Keystore;
|
||||
};
|
||||
environmentSecrets?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,8 +45,7 @@ async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
const credentials = jobData.credentials;
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
@@ -71,20 +61,20 @@ async function prepareJobCommonAsync(
|
||||
|
||||
return {
|
||||
platform: Platform.ANDROID,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
},
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
ndk: ctx.buildProfile.ndk,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
...buildCredentials,
|
||||
},
|
||||
};
|
||||
@@ -96,13 +86,14 @@ async function prepareGenericJobAsync(
|
||||
buildProfile: AndroidGenericBuildProfile
|
||||
): Promise<Partial<Android.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.GENERIC,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -113,14 +104,15 @@ async function prepareManagedJobAsync(
|
||||
buildProfile: AndroidManagedBuildProfile
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.MANAGED,
|
||||
username: accountName,
|
||||
username,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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 { resolveWorkflow } from '../../project/workflow';
|
||||
|
||||
export function readVersionCode(projectDir: string, exp: ExpoConfig): number | undefined {
|
||||
const workflow = resolveWorkflow(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) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function readVersionName(projectDir: string, exp: ExpoConfig): string | undefined {
|
||||
const workflow = resolveWorkflow(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,4 +1,4 @@
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -9,52 +9,42 @@ import { promptAsync } from '../prompts';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import vcs from '../vcs';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { 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: {
|
||||
archiveBucketKey: string;
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
}
|
||||
): Promise<Job>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>
|
||||
): Promise<() => Promise<string>> {
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
{
|
||||
@@ -71,7 +61,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${
|
||||
@@ -83,25 +73,51 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
);
|
||||
}
|
||||
|
||||
const archiveBucketKey = await uploadProjectAsync(builder.ctx);
|
||||
const projectArchive = builder.ctx.commandCtx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(builder.ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
archiveBucketKey,
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
return 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 (builder.ctx.commandCtx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
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_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;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -143,13 +159,8 @@ 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> {
|
||||
@@ -209,7 +220,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;
|
||||
}
|
||||
@@ -242,7 +253,7 @@ 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,
|
||||
|
||||
@@ -6,32 +6,25 @@ import path from 'path';
|
||||
import Log from '../log';
|
||||
import { 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',
|
||||
[RequestedPlatform.iOS]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
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,12 +32,11 @@ 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),
|
||||
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')),
|
||||
};
|
||||
@@ -58,7 +50,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,6 +59,37 @@ export async function configureAsync(options: {
|
||||
}
|
||||
}
|
||||
|
||||
const MANAGED_DEFAULTS = {
|
||||
release: {
|
||||
workflow: 'managed',
|
||||
},
|
||||
development: {
|
||||
workflow: 'managed',
|
||||
buildType: 'development-client',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
const ANDROID_GENERIC_DEFAULTS = {
|
||||
release: {
|
||||
workflow: 'generic',
|
||||
},
|
||||
development: {
|
||||
workflow: 'generic',
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
const IOS_GENERIC_DEFAULTS = {
|
||||
release: {
|
||||
workflow: 'generic',
|
||||
},
|
||||
development: {
|
||||
workflow: 'generic',
|
||||
schemeBuildConfiguration: 'Debug',
|
||||
distribution: 'internal',
|
||||
},
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
let existingEasJson;
|
||||
@@ -101,27 +124,19 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
|
||||
...existingEasJson,
|
||||
...(shouldInitAndroid
|
||||
? {
|
||||
android: {
|
||||
release: {
|
||||
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
android: ctx.hasAndroidNativeProject ? ANDROID_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
|
||||
}
|
||||
: null),
|
||||
...(shouldInitIOS
|
||||
? {
|
||||
ios: {
|
||||
release: {
|
||||
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : MANAGED_DEFAULTS,
|
||||
}
|
||||
: null),
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await gitAddAsync(easJsonPath, { intentToAdd: true });
|
||||
await vcs.trackFileAsync(easJsonPath);
|
||||
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
|
||||
}
|
||||
|
||||
@@ -155,7 +170,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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { AppPlatform } from '../graphql/generated';
|
||||
import { RequestedPlatform } from './types';
|
||||
|
||||
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
|
||||
[RequestedPlatform.iOS]: 'iOS',
|
||||
[RequestedPlatform.Ios]: 'iOS',
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@expo/eas-json';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, EasConfig, 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';
|
||||
@@ -19,7 +22,8 @@ export interface CommandContext {
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}
|
||||
@@ -31,7 +35,8 @@ export async function createCommandContextAsync({
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive = false,
|
||||
skipCredentialsCheck = false,
|
||||
local,
|
||||
clearCache = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
}: {
|
||||
@@ -41,7 +46,8 @@ export async function createCommandContextAsync({
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
@@ -59,7 +65,8 @@ export async function createCommandContextAsync({
|
||||
projectName,
|
||||
exp,
|
||||
nonInteractive,
|
||||
skipCredentialsCheck,
|
||||
local,
|
||||
clearCache,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
};
|
||||
@@ -69,7 +76,6 @@ export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
allowExperimental: boolean;
|
||||
requestedPlatform: RequestedPlatform;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
@@ -79,7 +85,7 @@ export interface ConfigureContext {
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
@@ -103,6 +109,7 @@ export function createBuildContext<T extends Platform>({
|
||||
}
|
||||
|
||||
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
|
||||
const devClientProperties = getDevClientEventProperties(platform, commandCtx, buildProfile);
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
@@ -110,6 +117,7 @@ export function createBuildContext<T extends Platform>({
|
||||
account_name: commandCtx.accountName,
|
||||
project_id: commandCtx.projectId,
|
||||
project_type: buildProfile.workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
return {
|
||||
@@ -119,3 +127,36 @@ export function createBuildContext<T extends Platform>({
|
||||
buildProfile,
|
||||
};
|
||||
}
|
||||
|
||||
function getDevClientEventProperties(
|
||||
platform: Platform,
|
||||
commandCtx: CommandContext,
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile
|
||||
): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(commandCtx.projectDir);
|
||||
if (buildProfile.workflow === Workflow.MANAGED) {
|
||||
includesDevClient = buildProfile.buildType === 'development-client';
|
||||
} else if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,22 @@ import { BuildFragment, BuildStatus } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { sleep } from '../utils/promise';
|
||||
import vcs from '../vcs';
|
||||
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';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
if (commandCtx.local) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
Log.newLine();
|
||||
@@ -36,7 +40,7 @@ async function startBuildsAsync(
|
||||
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildiOS = [RequestedPlatform.iOS, RequestedPlatform.All].includes(
|
||||
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
@@ -46,27 +50,29 @@ async function startBuildsAsync(
|
||||
|
||||
const builds: {
|
||||
platform: Platform;
|
||||
sendBuildRequestAsync: () => Promise<string>;
|
||||
sendBuildRequestAsync: () => Promise<string | undefined>;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildiOS) {
|
||||
if (shouldBuildIos) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
|
||||
}
|
||||
return Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await 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 } = {}
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
|
||||
const spinner = ora().start();
|
||||
@@ -76,7 +82,7 @@ async function waitForBuildEndAsync(
|
||||
const builds: (BuildFragment | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId);
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
@@ -88,6 +94,9 @@ async function waitForBuildEndAsync(
|
||||
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;
|
||||
@@ -125,6 +134,7 @@ async function waitForBuildEndAsync(
|
||||
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;
|
||||
@@ -132,6 +142,7 @@ async function waitForBuildEndAsync(
|
||||
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
|
||||
const unknownState = builds.length - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
newBuilds && `Builds created: ${newBuilds}`,
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
canceled && `Builds canceled: ${canceled}`,
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { CredentialsProvider } from '../credentials/CredentialsProvider';
|
||||
import Log from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
|
||||
function logCredentials(target: 'local' | 'remote', platform: Platform) {
|
||||
let message = `Using ${target} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
|
||||
Log.succeed(message);
|
||||
}
|
||||
|
||||
async function ensureCredentialsAutoAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
const platform = requestedPlatformDisplayNames[provider.platform];
|
||||
switch (workflow) {
|
||||
case Workflow.MANAGED:
|
||||
if (await provider.hasLocalAsync()) {
|
||||
return CredentialsSource.LOCAL;
|
||||
} else {
|
||||
return CredentialsSource.REMOTE;
|
||||
}
|
||||
case Workflow.GENERIC: {
|
||||
const hasLocal = await provider.hasLocalAsync();
|
||||
const hasRemote = await provider.hasRemoteAsync();
|
||||
if (hasRemote && hasLocal) {
|
||||
if (!(await provider.isLocalSyncedAsync())) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
|
||||
);
|
||||
}
|
||||
|
||||
const { select } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
message: 'Which credentials do you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on EAS servers.', value: CredentialsSource.REMOTE },
|
||||
],
|
||||
});
|
||||
return select;
|
||||
} else {
|
||||
return CredentialsSource.LOCAL;
|
||||
}
|
||||
} else if (hasLocal) {
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
} else if (hasRemote) {
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
|
||||
);
|
||||
} else if (Log.isDebug) {
|
||||
Log.warn(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${platform} credentials not found, generate now?`,
|
||||
});
|
||||
if (confirm) {
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCredentialsAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
src: CredentialsSource,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
case CredentialsSource.REMOTE:
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
case CredentialsSource.AUTO:
|
||||
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
|
||||
}
|
||||
}
|
||||
@@ -27,18 +27,10 @@ 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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -1,10 +1,17 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
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');
|
||||
|
||||
@@ -38,9 +45,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('2');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
@@ -57,9 +66,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
|
||||
@@ -76,9 +87,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
@@ -99,7 +112,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('2');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
});
|
||||
@@ -115,7 +128,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
@@ -131,12 +144,48 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { Ios, Job, 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 { 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 { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
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 { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.IOS>({
|
||||
commandCtx,
|
||||
platform: Platform.IOS,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
|
||||
if (
|
||||
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
|
||||
buildProfile.workflow === Workflow.GENERIC &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -40,41 +40,47 @@ export async function prepareIosBuildAsync(
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
if (buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.exp
|
||||
);
|
||||
} 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: commandCtx.projectDir,
|
||||
nonInteractive: commandCtx.nonInteractive,
|
||||
exp: commandCtx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosBuildScheme,
|
||||
iosApplicationTarget,
|
||||
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,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync,
|
||||
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,
|
||||
@@ -99,39 +105,3 @@ export async function prepareIosBuildAsync(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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,24 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
VersionAutoIncrement,
|
||||
iOSBuildProfile,
|
||||
iOSGenericBuildProfile,
|
||||
} from '@expo/eas-json';
|
||||
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
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);
|
||||
@@ -44,26 +33,12 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: iOSBuildProfile;
|
||||
buildProfile: IosBuildProfile;
|
||||
}): Promise<void> {
|
||||
const { workflow, autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
|
||||
|
||||
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,73 +1,49 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
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 { ensureCredentialsAsync } from '../credentials';
|
||||
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,
|
||||
},
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
|
||||
const { projectDir } = buildCtx.commandCtx;
|
||||
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: buildCtx.commandCtx.nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
distribution: iOSDistributionType;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
}
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: getAppFromContext(ctx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildCtx.buildProfile.workflow
|
||||
),
|
||||
},
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(
|
||||
await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: params.app,
|
||||
distribution: params.distribution,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
params.workflow,
|
||||
params.credentialsSource,
|
||||
params.nonInteractive
|
||||
);
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
|
||||
return ctx.buildProfile.distribution !== 'simulator';
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return buildCtx.buildProfile.distribution !== 'simulator';
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
IosJobSecretsInput,
|
||||
IosManagedBuildType,
|
||||
IosManagedJobInput,
|
||||
IosSchemeBuildConfiguration,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
|
||||
@@ -16,14 +15,13 @@ export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -33,6 +31,7 @@ export function transformManagedJob(job: Ios.ManagedJob): IosManagedJobInput {
|
||||
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,
|
||||
@@ -74,16 +73,6 @@ function transformIosSecrets(secrets: {
|
||||
};
|
||||
}
|
||||
|
||||
function transformSchemeBuildConfiguration(
|
||||
schemeBuildConfiguration: Ios.SchemeBuildConfiguration
|
||||
): IosSchemeBuildConfiguration {
|
||||
if (schemeBuildConfiguration === Ios.SchemeBuildConfiguration.DEBUG) {
|
||||
return IosSchemeBuildConfiguration.Debug;
|
||||
} else {
|
||||
return IosSchemeBuildConfiguration.Release;
|
||||
}
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
|
||||
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
|
||||
return IosManagedBuildType.DevelopmentClient;
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Ios,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
|
||||
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 {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosBuildScheme?: string;
|
||||
iosApplicationTarget?: string;
|
||||
};
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
export async function prepareJobAsync(
|
||||
@@ -50,7 +35,6 @@ interface CommonJobProperties {
|
||||
platform: Platform.IOS;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: Ios.BuilderEnvironment;
|
||||
releaseChannel?: string;
|
||||
distribution?: Ios.DistributionType;
|
||||
cache: Cache;
|
||||
secrets: {
|
||||
@@ -61,60 +45,57 @@ interface CommonJobProperties {
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{
|
||||
archiveBucketKey,
|
||||
credentials,
|
||||
targetName,
|
||||
}: { archiveBucketKey: string; credentials?: IosCredentials; targetName?: string }
|
||||
{ credentials, projectArchive }: { credentials?: IosCredentials; 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 buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials) {
|
||||
const targetNames = Object.keys(credentials);
|
||||
for (const targetName of targetNames) {
|
||||
buildCredentials[targetName] = prepareTargetCredentials(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 {
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: archiveBucketKey,
|
||||
},
|
||||
projectArchive,
|
||||
distribution: 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: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
buildCredentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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.commandCtx.exp.sdkVersion) {
|
||||
const majorSdkVersion = semver.major(ctx.commandCtx.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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -122,24 +103,21 @@ function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSGenericBuildProfile
|
||||
buildProfile: IosGenericBuildProfile
|
||||
): Promise<Partial<Ios.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.GENERIC,
|
||||
scheme: jobData.projectConfiguration.iosBuildScheme,
|
||||
schemeBuildConfiguration:
|
||||
buildProfile.schemeBuildConfiguration === 'Auto'
|
||||
? undefined
|
||||
: buildProfile.schemeBuildConfiguration,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: buildProfile.schemeBuildConfiguration,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -147,30 +125,21 @@ async function prepareGenericJobAsync(
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSManagedBuildProfile
|
||||
buildProfile: IosManagedBuildProfile
|
||||
): Promise<Partial<Ios.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.MANAGED,
|
||||
buildType: buildProfile.buildType,
|
||||
username: accountName,
|
||||
username,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
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,10 +1,12 @@
|
||||
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 Log from '../../log';
|
||||
import { resolveWorkflow } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
@@ -100,9 +102,30 @@ 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
|
||||
): Promise<string | undefined> {
|
||||
const workflow = resolveWorkflow(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleShortVersionString;
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readBuildNumberAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<string | undefined> {
|
||||
const workflow = resolveWorkflow(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleVersion;
|
||||
} else {
|
||||
return IOSConfig.Version.getBuildNumber(exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToInfoPlistAsync({
|
||||
@@ -120,6 +143,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,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
const execNameOrPath =
|
||||
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
|
||||
(await findWithNodeResolution()) ??
|
||||
PLUGIN_PACKAGE_NAME;
|
||||
try {
|
||||
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
await spawnAsync(execNameOrPath, [arg], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
|
||||
Log.warn(
|
||||
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
|
||||
`npm install -g ${PLUGIN_PACKAGE_NAME}`
|
||||
)} and make sure it's in PATH.`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findWithNodeResolution(): Promise<string | undefined> {
|
||||
try {
|
||||
const resolvedPath = path.resolve(
|
||||
path.dirname(require.resolve(PLUGIN_PACKAGE_NAME)),
|
||||
'../bin/run'
|
||||
);
|
||||
if (await fs.pathExists(resolvedPath)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
import { Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import { getAppIdentifierAsync } from '../project/projectUtils';
|
||||
import { gitCommitHashAsync } from '../utils/git';
|
||||
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
|
||||
import { getApplicationId } from '../project/android/applicationId';
|
||||
import { getBundleIdentifier } 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 { readVersionCode, readVersionName } 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';
|
||||
|
||||
@@ -24,43 +35,103 @@ export async function collectMetadata<T extends Platform>(
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
}
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
return {
|
||||
// TODO: type error fixed in https://github.com/expo/eas-build/pull/34
|
||||
// remove @ts-expect-error after upgrading @expo/eas-build-job
|
||||
// @ts-expect-error error TS2322: not assignable to type 'Record<string, string | number>'
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: ctx.commandCtx.exp.version!,
|
||||
appVersion: await resolveAppVersionAsync(ctx),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
releaseChannel: await resolveReleaseChannel(ctx),
|
||||
...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,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
gitCommitHash: await gitCommitHashAsync(),
|
||||
gitCommitHash: await vcs.getCommitHashAsync(),
|
||||
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveReleaseChannel<T extends Platform>(
|
||||
async function resolveAppVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return ctx.buildProfile.releaseChannel;
|
||||
}
|
||||
|
||||
let maybeReleaseChannel: string | null;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
} else {
|
||||
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
}
|
||||
return maybeReleaseChannel ?? 'default';
|
||||
}
|
||||
|
||||
async function resolveAppBuildVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
} else {
|
||||
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return versionCode !== undefined ? String(versionCode) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
} else {
|
||||
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.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.commandCtx.projectDir)) ?? 'default';
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
|
||||
}
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
async function getNativeChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -2,11 +2,12 @@ export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
iOS = 'ios',
|
||||
Ios = 'ios',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
ERRORED = 'errored',
|
||||
@@ -14,4 +15,4 @@ export enum BuildStatus {
|
||||
CANCELED = 'canceled',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number>;
|
||||
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';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames } from '../constants';
|
||||
|
||||
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL)
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
else if (credentialsSource === CredentialsSource.REMOTE)
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -18,6 +18,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:
|
||||
@@ -48,12 +50,8 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
{
|
||||
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>';
|
||||
@@ -72,11 +70,13 @@ export function formatGraphQLBuild(build: BuildFragment) {
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
import {
|
||||
BuildError,
|
||||
@@ -15,7 +17,7 @@ import {
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../constants';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(
|
||||
accountName: string,
|
||||
@@ -82,10 +84,12 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
|
||||
buildId: build.id,
|
||||
account: accountName,
|
||||
});
|
||||
const installUrl = getInternalDistributionInstallUrl(build);
|
||||
qrcodeTerminal.generate(installUrl, code => console.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,
|
||||
};
|
||||
@@ -1,4 +1,7 @@
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import assert from 'assert';
|
||||
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getBuildLogsUrl({
|
||||
buildId,
|
||||
@@ -13,3 +16,15 @@ export function getBuildLogsUrl({
|
||||
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,10 +12,12 @@ 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,
|
||||
@@ -44,7 +46,7 @@ async function getBranchInfoAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const branchId = data.app?.byId.updateBranchByName.id;
|
||||
const branchId = data.app?.byId.updateBranchByName?.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${name} on ${appId}`);
|
||||
}
|
||||
@@ -104,11 +106,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.';
|
||||
@@ -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)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,30 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
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';
|
||||
|
||||
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
|
||||
@@ -43,8 +36,11 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
updatedAt
|
||||
createdAt
|
||||
message
|
||||
runtimeVersion
|
||||
group
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,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 {
|
||||
@@ -82,13 +78,29 @@ 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 {
|
||||
const table = new CliTable({ head: ['Branch', 'Latest update'] });
|
||||
table.push(...branches.map(branch => [branch.name, formatUpdate(branch.updates[0])]));
|
||||
const table = new CliTable({
|
||||
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.log(chalk.bold('Branches with their most recent update group:'));
|
||||
Log.log(table.toString());
|
||||
if (branches.length >= BRANCHES_LIMIT) {
|
||||
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
|
||||
@@ -96,17 +108,3 @@ export default class BranchList extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUpdate(
|
||||
update: Pick<Update, 'id' | 'updatedAt' | 'message'> & {
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
}
|
||||
): string {
|
||||
if (!update) {
|
||||
return 'N/A';
|
||||
}
|
||||
const message = update.message ? `"${update.message}" ` : '';
|
||||
return `${message}(${format(update.updatedAt, 'en_US')} by ${getActorDisplayName(
|
||||
update.actor as any
|
||||
)})`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { getConfig, getDefaultTarget } from '@expo/config';
|
||||
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
@@ -18,20 +19,26 @@ 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,
|
||||
getProjectAccountNameAsync,
|
||||
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 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,
|
||||
@@ -64,10 +71,14 @@ export default class BranchPublish extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a branch.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Name of the branch to publish on',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
branch: flags.string({
|
||||
description: 'name of the branch to publish on.',
|
||||
}),
|
||||
message: flags.string({
|
||||
description: 'Short message describing the updates.',
|
||||
required: false,
|
||||
@@ -89,6 +100,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,
|
||||
@@ -97,9 +115,9 @@ export default class BranchPublish extends Command {
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
branch: name,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
@@ -107,6 +125,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;
|
||||
|
||||
@@ -117,16 +136,23 @@ export default class BranchPublish extends Command {
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug, runtimeVersion } = exp;
|
||||
let { slug, 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
|
||||
// Expo SDK preset runtime that can be launched in Expo Go.
|
||||
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
|
||||
if (!runtimeVersion && sdkVersion && isManagedProject) {
|
||||
Log.withTick('Generating runtime version from sdk version');
|
||||
runtimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
|
||||
}
|
||||
|
||||
if (!runtimeVersion) {
|
||||
throw new Error(
|
||||
"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.';
|
||||
@@ -134,7 +160,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 => {
|
||||
@@ -166,30 +192,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) {
|
||||
@@ -198,7 +262,8 @@ 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);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
@@ -219,7 +284,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),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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,9 +16,13 @@ import {
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
|
||||
@@ -59,7 +63,7 @@ export async function viewUpdateBranchAsync({
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -117,12 +121,8 @@ 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 fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
@@ -152,22 +152,25 @@ export default class BranchView extends Command {
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: ['createdAt', 'message', 'group', 'actor'],
|
||||
head: UPDATE_COLUMNS,
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
groupTable.push([
|
||||
new Date(update.createdAt).toLocaleString(),
|
||||
update.message,
|
||||
formatUpdate(update),
|
||||
update.runtimeVersion,
|
||||
update.group,
|
||||
update.actor?.firstName,
|
||||
getPlatformsForGroup({
|
||||
updates: UpdateBranch.updates,
|
||||
group: update.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
fullName
|
||||
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
|
||||
);
|
||||
Log.log(chalk.bold('Recent update groups published on this branch:'));
|
||||
|
||||
@@ -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([
|
||||
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.New }),
|
||||
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
|
||||
BuildQuery.allForAppAsync(projectId, { 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(),
|
||||
});
|
||||
|
||||
@@ -87,7 +73,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.iOS,
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
||||
@@ -1,44 +1,31 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
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 EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } 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 {
|
||||
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,
|
||||
@@ -52,117 +39,81 @@ export default class Build extends Command {
|
||||
default: false,
|
||||
description: 'Run command in --non-interactive mode',
|
||||
}),
|
||||
local: flags.boolean({
|
||||
default: false,
|
||||
description: 'Run build locally [experimental]',
|
||||
}),
|
||||
wait: flags.boolean({
|
||||
default: true,
|
||||
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 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();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
platform,
|
||||
projectId,
|
||||
user,
|
||||
});
|
||||
if (accountHasPendingBuilds) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir);
|
||||
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
skipCredentialsCheck: flags['skip-credentials-check'],
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
});
|
||||
await buildAsync(commandCtx);
|
||||
} finally {
|
||||
await flushAnalyticsAsync(); // TODO: run in oclif hook for every command
|
||||
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());
|
||||
|
||||
if (flags['skip-credentials-check']) {
|
||||
Log.warnDeprecatedFlag(
|
||||
'skip-credentials-check',
|
||||
'Build credential validation is always skipped with the --non-interactive flag. You can also skip interactively.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
exp = (await ensureProjectConfiguredAsync(projectDir)) ?? exp;
|
||||
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
clearCache: flags['clear-cache'],
|
||||
local: flags.local,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
});
|
||||
await buildAsync(commandCtx);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +129,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.iOS,
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
@@ -189,9 +140,9 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
return platform;
|
||||
}
|
||||
|
||||
async function ensureProjectConfiguredAsync(projectDir: string): Promise<void> {
|
||||
async function ensureProjectConfiguredAsync(projectDir: string): Promise<ExpoConfig | null> {
|
||||
if (await fs.pathExists(path.join(projectDir, 'eas.json'))) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const confirm = await confirmAsync({
|
||||
message: 'This app is not set up for building with EAS. Set it up now?',
|
||||
@@ -200,13 +151,14 @@ async function ensureProjectConfiguredAsync(projectDir: string): Promise<void> {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: RequestedPlatform.All,
|
||||
allowExperimental: false,
|
||||
});
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
if (await vcs.hasUncommittedChangesAsync()) {
|
||||
throw new Error(
|
||||
'Build process requires clean git working tree, please commit all your changes and run `eas build` again'
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
|
||||
);
|
||||
}
|
||||
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(
|
||||
|
||||
@@ -19,10 +19,11 @@ export default class BuildList extends Command {
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.iOS],
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
}),
|
||||
status: flags.enum({
|
||||
options: [
|
||||
BuildStatus.NEW,
|
||||
BuildStatus.IN_QUEUE,
|
||||
BuildStatus.IN_PROGRESS,
|
||||
BuildStatus.ERRORED,
|
||||
@@ -92,6 +93,8 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
|
||||
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) {
|
||||
|
||||
@@ -9,11 +9,11 @@ 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 { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
@@ -87,12 +87,8 @@ 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 fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'Channel name may not be empty.';
|
||||
@@ -146,7 +142,7 @@ export default class ChannelCreate extends Command {
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
fullName
|
||||
)}. ${branchMessage} and have pointed the channel at it. 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.';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -10,8 +9,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 { logChannelDetails } from './view';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
|
||||
@@ -31,18 +30,21 @@ async function getAllUpdateChannelForAppAsync({
|
||||
updateChannels(offset: $offset, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
branchMapping
|
||||
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 {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -82,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,
|
||||
@@ -102,27 +99,10 @@ export default class ChannelList extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: ['channel', 'branch', 'update', 'message', 'created-at', 'actor'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
Log.log(); // spacing
|
||||
for (const channel of channels) {
|
||||
// TODO (cedric): refactor when multiple branches per channel are available
|
||||
const branch = channel.updateBranches[0];
|
||||
const update = branch.updates[0];
|
||||
|
||||
table.push([
|
||||
channel.name,
|
||||
branch.name,
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Channels for this app:}`);
|
||||
Log.log(table.toString());
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -44,18 +44,20 @@ function getRolloutInfo(
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
} {
|
||||
const { branchMapping } = getBranchMapping(getUpdateChannelByNameForAppResult);
|
||||
const { branchMapping } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
|
||||
if (!newBranch || !oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.name}".`
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.name}".`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,11 +136,11 @@ async function startRolloutAsync({
|
||||
],
|
||||
};
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
if (!oldBranch) {
|
||||
@@ -200,7 +202,7 @@ async function editRolloutAsync({
|
||||
newBranchMapping.data[0].branchMappingLogic.operand = percent / 100;
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
@@ -290,7 +292,7 @@ async function endRolloutAsync({
|
||||
};
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
const logMessage = `️Rollout on channel ${chalk.bold(
|
||||
@@ -347,19 +349,15 @@ 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,
|
||||
channelName: channelName!,
|
||||
});
|
||||
const { branchMapping: currentBranchMapping, isRollout } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
|
||||
if (currentBranchMapping.data.length === 0) {
|
||||
@@ -397,7 +395,7 @@ export default class ChannelRollout extends Command {
|
||||
percent,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
fullName: `@${accountName}/${slug}`,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 gql from 'graphql-tag';
|
||||
@@ -10,9 +11,18 @@ import {
|
||||
GetChannelByNameForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import {
|
||||
FormatUpdateParameter,
|
||||
UPDATE_COLUMNS,
|
||||
formatUpdate,
|
||||
getPlatformsForGroup,
|
||||
} from '../../update/utils';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -31,15 +41,19 @@ export type BranchMapping = {
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(
|
||||
getChannelByNameForAppQuery: GetChannelByNameForAppQuery
|
||||
branchMappingString?: string
|
||||
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
|
||||
const branchMappingString =
|
||||
getChannelByNameForAppQuery.app?.byId.updateChannelByName.branchMapping;
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
|
||||
const branchMapping: BranchMapping = JSON.parse(branchMappingString);
|
||||
if (branchMapping.version !== 0) {
|
||||
throw new Error('Branch mapping must be version 0.');
|
||||
}
|
||||
@@ -96,15 +110,17 @@ 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 {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -123,6 +139,57 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
);
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: {
|
||||
branchMapping: string;
|
||||
updateBranches: {
|
||||
updates: (FormatUpdateParameter & {
|
||||
runtimeVersion: string;
|
||||
group: string;
|
||||
platform: string;
|
||||
})[];
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
}): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const table = new Table({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A,',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Log.log(table.toString());
|
||||
}
|
||||
|
||||
export default class ChannelView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'View a channel on the current project.';
|
||||
@@ -153,12 +220,8 @@ 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 fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to view a specific channel.';
|
||||
@@ -187,53 +250,12 @@ export default class ChannelView extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppresult
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'branch',
|
||||
...(isRollout ? ['rollout percent'] : []),
|
||||
'active update group',
|
||||
'update message',
|
||||
'last publish',
|
||||
'actor',
|
||||
],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${fullName}}. Channel ID: {bold ${channel.id}}`
|
||||
);
|
||||
Log.log(chalk`{bold Recent update groups published on this branch:}`);
|
||||
Log.log(table.toString());
|
||||
Log.log(
|
||||
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 InitView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Create or link an EAS 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 });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -4,12 +4,15 @@ import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
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';
|
||||
@@ -47,10 +50,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();
|
||||
@@ -0,0 +1,100 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretWithScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
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.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
|
||||
static flags = {
|
||||
id: flags.string({
|
||||
description: 'ID of the secret to delete',
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
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))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
flags: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
let secret: EnvironmentSecretWithScope | undefined;
|
||||
|
||||
if (!id) {
|
||||
const validationMessage = 'You must select which secret to delete.';
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
|
||||
({ secret } = await promptAsync({
|
||||
type: 'autocomplete',
|
||||
name: 'secret',
|
||||
message: 'Pick the secret to be deleted:',
|
||||
choices: secrets.map(secret => ({
|
||||
title: `${secret.name} (${secret.scope})`,
|
||||
value: secret,
|
||||
})),
|
||||
}));
|
||||
|
||||
id = secret?.id;
|
||||
|
||||
if (!id) throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permamently delete secret${
|
||||
secret?.name ? ` "${secret?.name}"` : ''
|
||||
} with id: "${id}".\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: `Are you sure you wish to proceed?${
|
||||
secret?.scope === EnvironmentSecretScope.ACCOUNT
|
||||
? ' This secret is applied across your whole account and may affect multiple apps.'
|
||||
: ''
|
||||
}`,
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error(
|
||||
`Canceled deletion of secret${secret?.name ? ` "${secret?.name}"` : ''} with id: "${id}".`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await EnvironmentSecretMutation.delete(id);
|
||||
|
||||
Log.withTick(`️Deleted secret${secret?.name ? ` "${secret?.name}"` : ''} with id "${id}".`);
|
||||
}
|
||||
}
|
||||
+2
-13
@@ -4,7 +4,6 @@ import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import { EnvironmentSecretFragment } from '../../graphql/generated';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -18,11 +17,9 @@ import {
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { EnvironmentSecretScope } from './create';
|
||||
|
||||
export default class EnvironmentSecretsList extends Command {
|
||||
export default class EnvironmentSecretList extends Command {
|
||||
static description = 'Lists environment secrets available for your current app';
|
||||
static usage = 'secrets:list';
|
||||
|
||||
async run(): Promise<void> {
|
||||
await ensureLoggedInAsync();
|
||||
@@ -43,15 +40,7 @@ export default class EnvironmentSecretsList extends Command {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
const [accountSecrets, appSecrets] = await Promise.all([
|
||||
EnvironmentSecretsQuery.byAcccountNameAsync(projectAccountName),
|
||||
EnvironmentSecretsQuery.byAppFullNameAsync(projectFullName),
|
||||
]);
|
||||
|
||||
const secrets = [
|
||||
...appSecrets.map(s => ({ ...s, scope: EnvironmentSecretScope.PROJECT })),
|
||||
...accountSecrets.map(s => ({ ...s, scope: EnvironmentSecretScope.ACCOUNT })),
|
||||
] as (EnvironmentSecretFragment & { scope: EnvironmentSecretScope })[];
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Scope', 'ID', 'Updated at'],
|
||||
@@ -1,59 +0,0 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class EnvironmentSecretDelete extends Command {
|
||||
static description = `Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: `ID of the secret to delete`,
|
||||
},
|
||||
];
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
args: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permamently delete secret with id: "${id}".\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of secret with id: "${id}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await EnvironmentSecretMutation.delete(id);
|
||||
|
||||
Log.withTick(`️Deleted secret with id "${id}".`);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
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';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import {
|
||||
AndroidSubmitCommandFlags,
|
||||
IosSubmitCommandFlags,
|
||||
SubmissionPlatform,
|
||||
} from '../submissions/types';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
|
||||
|
||||
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'];
|
||||
@@ -159,14 +156,13 @@ export default class BuildSubmit extends Command {
|
||||
'company-name': companyName,
|
||||
|
||||
// common
|
||||
platform: platformFromParams,
|
||||
...flags
|
||||
},
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const platform = platformFromParams ?? (await promptForPlatformAsync());
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -183,7 +179,7 @@ export default class BuildSubmit extends Command {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
if (platform === SubmissionPlatform.Android) {
|
||||
if (platform === AppPlatform.Android) {
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
androidPackage,
|
||||
releaseStatus,
|
||||
@@ -193,7 +189,7 @@ export default class BuildSubmit extends Command {
|
||||
const ctx = AndroidSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
} else if (platform === SubmissionPlatform.iOS) {
|
||||
} else if (platform === AppPlatform.Ios) {
|
||||
const options: IosSubmitCommandFlags = {
|
||||
appleId,
|
||||
ascAppId,
|
||||
@@ -213,7 +209,7 @@ export default class BuildSubmit extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
async function promptForPlatformAsync(): Promise<AppPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Submit to platform',
|
||||
@@ -221,11 +217,11 @@ async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
choices: [
|
||||
{
|
||||
title: 'iOS',
|
||||
value: SubmissionPlatform.iOS,
|
||||
value: AppPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: SubmissionPlatform.Android,
|
||||
value: AppPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateGroupMutation,
|
||||
UpdateMutationDeleteUpdateGroupArgs,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
|
||||
async function deleteUpdateGroupAsync({
|
||||
group,
|
||||
}: {
|
||||
group: string;
|
||||
}): Promise<DeleteUpdateGroupMutation> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateGroupMutation, UpdateMutationDeleteUpdateGroupArgs>(
|
||||
gql`
|
||||
mutation DeleteUpdateGroup($group: ID!) {
|
||||
update {
|
||||
deleteUpdateGroup(group: $group) {
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ group }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class UpdateDelete extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Delete all the updates in an update Group.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'groupId',
|
||||
required: true,
|
||||
description: 'The ID of an update group to delete.',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `Return a json with the group ID of the deleted updates.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
args: { groupId: group },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(UpdateDelete);
|
||||
|
||||
if (!jsonFlag) {
|
||||
const shouldAbort = await confirmAsync({
|
||||
message:
|
||||
`🚨${chalk.red('CAUTION')}🚨\n\n` +
|
||||
`${chalk.yellow(`This will delete all of the updates in group "${group}".`)} ${chalk.red(
|
||||
'This is a permanent operation.'
|
||||
)}\n\n` +
|
||||
`If you want to revert to a previous publish, you should use 'branch:publish --republish' targeted at the last working update group instead.\n\n` +
|
||||
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'branch:publish --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
|
||||
`Would you like to abort?`,
|
||||
});
|
||||
|
||||
if (shouldAbort) {
|
||||
Log.log('Aborted.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUpdateGroupAsync({ group });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify({ group }));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.withTick(`Deleted update group ${group}`);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
export default class UpdateShow extends Command {
|
||||
static hidden = true;
|
||||
static description = 'details about a particular revision';
|
||||
|
||||
async run() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import Table from 'cli-table3';
|
||||
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, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
export async function viewUpdateAsync({
|
||||
groupId,
|
||||
}: {
|
||||
groupId: string;
|
||||
}): Promise<UpdatesByGroupQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<UpdatesByGroupQuery, UpdatesByGroupQueryVariables>(
|
||||
gql`
|
||||
query UpdatesByGroup($groupId: ID!) {
|
||||
updatesByGroup(group: $groupId) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
platform
|
||||
message
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
if (data.updatesByGroup.length === 0) {
|
||||
throw new Error(`Could not find any updates with group ID: "${groupId}"`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
export default class UpdateView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Update group details.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'groupId',
|
||||
required: true,
|
||||
description: 'The ID of an update group.',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `Return a json with the updates belonging to the group.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
args: { groupId },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(UpdateView);
|
||||
|
||||
const { updatesByGroup } = await viewUpdateAsync({ groupId });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(updatesByGroup));
|
||||
return;
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
getPlatformsForGroup({
|
||||
updates: updatesByGroup,
|
||||
group: updatesByGroup[0]?.group,
|
||||
}),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { prepareInputParamsAsync } from '../../webhooks/input';
|
||||
|
||||
export default class WebhookCreate extends Command {
|
||||
static description = 'Create a webhook on the current project.';
|
||||
|
||||
static flags = {
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
default: WebhookType.Build,
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'Webhook URL',
|
||||
}),
|
||||
secret: flags.string({
|
||||
description:
|
||||
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
const { flags } = this.parse(WebhookCreate);
|
||||
const webhookInputParams = await prepareInputParamsAsync(flags);
|
||||
|
||||
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 spinner = ora('Creating a webhook').start();
|
||||
try {
|
||||
await WebhookMutation.createWebhookAsync(projectId, webhookInputParams);
|
||||
spinner.succeed('Successfully created a webhook');
|
||||
} catch (err) {
|
||||
spinner.fail('Failed to create a webhook');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import ora from 'ora';
|
||||
|
||||
import { WebhookFragment } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookDelete extends Command {
|
||||
static description = 'Delete a webhook on the current project.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'ID',
|
||||
required: false,
|
||||
description: 'ID of the webhook to delete',
|
||||
},
|
||||
];
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
let {
|
||||
args: { ID: webhookId },
|
||||
} = this.parse(WebhookDelete);
|
||||
|
||||
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);
|
||||
|
||||
let webhook: WebhookFragment | undefined =
|
||||
webhookId && (await WebhookQuery.byIdAsync(webhookId));
|
||||
if (!webhookId) {
|
||||
const webhooks = await fetchWebhooksByAppIdAsync(projectId);
|
||||
if (webhooks.length === 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
({ webhook } = await promptAsync({
|
||||
type: 'autocomplete',
|
||||
name: 'webhook',
|
||||
message: 'Pick the webhook to be deleted:',
|
||||
choices: webhooks.map(i => ({
|
||||
title: `${chalk.bold(i.url)} (Event: ${i.event}, ID: ${i.id})`,
|
||||
value: i,
|
||||
})),
|
||||
}));
|
||||
webhookId = nullthrows(webhook).id;
|
||||
}
|
||||
|
||||
assert(webhook, 'Webhook must be defined here');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatWebhook(webhook));
|
||||
Log.newLine();
|
||||
Log.warn(`You are about to permamently delete this webhook.\nThis action is irreversible.`);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of the webhook`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = ora('Deleting the webhook').start();
|
||||
try {
|
||||
await WebhookMutation.deleteWebhookAsync(webhookId);
|
||||
spinner.succeed('Successfully deleted the webhook');
|
||||
} catch (err) {
|
||||
spinner.fail('Failed to delete the webhook');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWebhooksByAppIdAsync(appId: string): Promise<WebhookFragment[]> {
|
||||
const spinner = ora('Fetching webhooks').start();
|
||||
try {
|
||||
const webhooks = await WebhookQuery.byAppIdAsync(appId);
|
||||
if (webhooks.length === 0) {
|
||||
spinner.fail('There are no webhooks on the project');
|
||||
return [];
|
||||
} else {
|
||||
spinner.succeed(`Successfully fetched ${webhooks.length} webhooks`);
|
||||
return webhooks;
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.fail("Something went wrong and we couldn't fetch the webhook list");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookList extends Command {
|
||||
static description = 'List webhooks on the current project.';
|
||||
|
||||
static flags = {
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
const {
|
||||
flags: { event },
|
||||
} = this.parse(WebhookList);
|
||||
|
||||
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 projectFullName = await getProjectFullNameAsync(exp);
|
||||
|
||||
const spinner = ora(`Fetching the list of webhook on project ${projectFullName}`).start();
|
||||
try {
|
||||
const webhooks = await WebhookQuery.byAppIdAsync(projectId, event && { event });
|
||||
if (webhooks.length === 0) {
|
||||
spinner.fail(`There are no webhooks on project ${projectFullName}`);
|
||||
} else {
|
||||
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectFullName}`);
|
||||
const list = webhooks
|
||||
.map(webhook => formatWebhook(webhook))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
Log.log(`\n${list}`);
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the webhook list for the project ${projectFullName}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import pick from 'lodash/pick';
|
||||
import ora from 'ora';
|
||||
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { prepareInputParamsAsync } from '../../webhooks/input';
|
||||
|
||||
export default class WebhookUpdate extends Command {
|
||||
static description = 'Create a webhook on the current project.';
|
||||
|
||||
static flags = {
|
||||
id: flags.string({
|
||||
description: 'Webhook ID',
|
||||
required: true,
|
||||
}),
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
default: WebhookType.Build,
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'Webhook URL',
|
||||
}),
|
||||
secret: flags.string({
|
||||
description:
|
||||
"Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature' header.",
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
const { flags } = this.parse(WebhookUpdate);
|
||||
|
||||
const webhookId = flags.id;
|
||||
|
||||
const webhook = await WebhookQuery.byIdAsync(webhookId);
|
||||
const webhookInputParams = await prepareInputParamsAsync(
|
||||
pick(flags, ['event', 'url', 'secret']),
|
||||
webhook
|
||||
);
|
||||
|
||||
const spinner = ora('Updating a webhook').start();
|
||||
try {
|
||||
await WebhookMutation.updateWebhookAsync(webhookId, webhookInputParams);
|
||||
spinner.succeed('Successfully updated a webhook');
|
||||
} catch (err) {
|
||||
spinner.fail('Failed to update a webhook');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Command } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import Log from '../../log';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookView extends Command {
|
||||
static description = 'View a webhook on the current project.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'ID',
|
||||
required: true,
|
||||
description: 'ID of the webhook to view',
|
||||
},
|
||||
];
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
const {
|
||||
args: { ID: webhookId },
|
||||
} = this.parse(WebhookView);
|
||||
|
||||
const spinner = ora(`Fetching the webhook details for ID ${webhookId}`).start();
|
||||
try {
|
||||
const webhook = await WebhookQuery.byIdAsync(webhookId);
|
||||
spinner.succeed(`Found the webhook details`);
|
||||
Log.log(`\n${formatWebhook(webhook)}`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Couldn't find the webhook with ID ${webhookId}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,8 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export interface CredentialsProvider {
|
||||
platform: Platform;
|
||||
hasRemoteAsync(): Promise<boolean>;
|
||||
hasLocalAsync(): Promise<boolean>;
|
||||
isLocalSyncedAsync(): Promise<boolean>;
|
||||
}
|
||||
@@ -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 =
|
||||
'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';
|
||||
|
||||
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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';
|
||||
@@ -5,6 +5,7 @@ export const jester: Actor = {
|
||||
id: 'jester-id',
|
||||
username: 'jester',
|
||||
accounts: [{ id: 'jester-account-id', name: 'jester' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const jester2: Actor = {
|
||||
@@ -12,6 +13,7 @@ export const jester2: Actor = {
|
||||
id: 'jester2-id',
|
||||
username: 'jester2',
|
||||
accounts: [{ id: 'jester2-account-id', name: 'jester2' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const testUsername = jester.username;
|
||||
@@ -32,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
|
||||
);
|
||||
}
|
||||
@@ -3,43 +3,21 @@ import {
|
||||
AppleAppIdentifierFragment,
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
ApplePushKeyFragment,
|
||||
AppleTeamFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../graphql/generated';
|
||||
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',
|
||||
@@ -57,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 },
|
||||
@@ -107,122 +95,53 @@ export const testDistCertFragmentOneDependency: AppleDistributionCertificateFrag
|
||||
],
|
||||
};
|
||||
|
||||
export const testIosAppBuildCredentialsFragment: IosAppBuildCredentialsFragment = {
|
||||
id: 'test-ios-app-build-credentials-id',
|
||||
iosDistributionType: IosDistributionType.AppStore,
|
||||
distributionCertificate: testDistCertFragmentNoDependencies,
|
||||
provisioningProfile: testProvisioningProfileFragment,
|
||||
};
|
||||
|
||||
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,77 +1,27 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import Log from '../../log';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { CredentialsProvider } from '../CredentialsProvider';
|
||||
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 implements CredentialsProvider {
|
||||
export default class AndroidCredentialsProvider {
|
||||
public readonly platform = Platform.ANDROID;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
private get projectFullName(): string {
|
||||
const { projectName, accountName } = this.options.app;
|
||||
return `@${accountName}/${projectName}`;
|
||||
}
|
||||
|
||||
public async hasRemoteAsync(): Promise<boolean> {
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
return !!keystore;
|
||||
}
|
||||
|
||||
public async hasLocalAsync(): Promise<boolean> {
|
||||
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
|
||||
return !!rawCredentialsJson?.android;
|
||||
} catch (err) {
|
||||
Log.error(err); // malformed json
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async isLocalSyncedAsync(): Promise<boolean> {
|
||||
try {
|
||||
const [remote, local] = await Promise.all([
|
||||
this.ctx.android.fetchKeystoreAsync(this.projectFullName),
|
||||
credentialsJsonReader.readAndroidCredentialsAsync(this.ctx.projectDir),
|
||||
]);
|
||||
const r = remote;
|
||||
const l = local?.keystore;
|
||||
return !!(
|
||||
r &&
|
||||
l &&
|
||||
r.keystore === l.keystore &&
|
||||
r.keystorePassword === l.keystorePassword &&
|
||||
r.keyAlias === l.keyAlias &&
|
||||
r.keyPassword === l.keyPassword
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
): Promise<AndroidCredentials> {
|
||||
@@ -81,18 +31,25 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
case CredentialsSource.REMOTE:
|
||||
return await this.getRemoteAsync();
|
||||
}
|
||||
throw new Error('Unknown');
|
||||
}
|
||||
|
||||
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: nullthrows(androidBuildCredentials.androidKeystore?.keyPassword),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<AndroidCredentials> {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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,169 @@
|
||||
import assert from 'assert';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { getApplicationId } 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 function getAppLookupParamsFromContext(ctx: Context): 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 = getApplicationId(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;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 326 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user