mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5ac3d26a0 | ||
|
|
6be86d5119 | ||
|
|
04025802ac | ||
|
|
0a4c8785e6 | ||
|
|
1de239b6ba | ||
|
|
4515d29907 | ||
|
|
7591cac883 | ||
|
|
9765f75a7d | ||
|
|
1f1b58db82 | ||
|
|
b0c88a37a9 | ||
|
|
5694bb9fdc | ||
|
|
9245657e23 | ||
|
|
9ef8483882 | ||
|
|
b859e11126 | ||
|
|
cee499e0e6 | ||
|
|
8702f165f3 | ||
|
|
a6bea7e234 | ||
|
|
37343dd586 | ||
|
|
058183f202 | ||
|
|
7ee50de78e | ||
|
|
ca11aa9e41 | ||
|
|
64d985fcda | ||
|
|
bd2bb4dcf6 | ||
|
|
17db6c4cf1 | ||
|
|
5203096aca | ||
|
|
a53e1f916f | ||
|
|
09a4b3d387 | ||
|
|
24e7de5bfb | ||
|
|
613fb5b6ea | ||
|
|
4e117075fa | ||
|
|
33f4ae7a05 | ||
|
|
caba28901d | ||
|
|
77e02f5759 | ||
|
|
fa2717aada | ||
|
|
322f797cbe | ||
|
|
2a64b099ed | ||
|
|
81465733e8 | ||
|
|
9b22056205 | ||
|
|
24841f82e0 | ||
|
|
a8fae470d0 | ||
|
|
72283cdda2 | ||
|
|
9874ff26b3 | ||
|
|
85d64b7ad3 | ||
|
|
2dba2705b7 | ||
|
|
d1bec6e00a | ||
|
|
597e012ca3 | ||
|
|
8632b8ada7 | ||
|
|
b288879838 | ||
|
|
8beb8f46da | ||
|
|
9120bf5777 | ||
|
|
01619d4182 | ||
|
|
aa4ea4ae2a | ||
|
|
c236c2ffb5 | ||
|
|
b5a3a513de | ||
|
|
6058183c45 | ||
|
|
a042d27573 | ||
|
|
88b21e476d | ||
|
|
3d497d4f05 | ||
|
|
9b668e3629 | ||
|
|
a670a2a3fc | ||
|
|
53a407fa78 | ||
|
|
5ab1f637a5 | ||
|
|
f59c945347 | ||
|
|
83e50ab546 | ||
|
|
9a415fe743 | ||
|
|
e722b6601c | ||
|
|
170e5145f0 | ||
|
|
405c532f13 | ||
|
|
5bed1c3a01 | ||
|
|
8d0d34c497 | ||
|
|
6270bc122b | ||
|
|
57f1128450 | ||
|
|
cb09906b2e | ||
|
|
fa704c43b3 | ||
|
|
eede0c3dd7 | ||
|
|
1758a4e5e5 | ||
|
|
20ebf24a20 | ||
|
|
427ececb0b | ||
|
|
f65670e18f | ||
|
|
41a0a4554d | ||
|
|
c1da053f8c | ||
|
|
57da02af9d | ||
|
|
b9c505bbdc | ||
|
|
633e6a6721 | ||
|
|
f7ddb4ba8f | ||
|
|
91eb2c467b | ||
|
|
f237a7e074 | ||
|
|
242c3cffb6 | ||
|
|
eee54af6ee | ||
|
|
86f86dbe86 | ||
|
|
cc99bb3326 | ||
|
|
08dc52c631 | ||
|
|
51db45a861 | ||
|
|
790853b3b2 | ||
|
|
23479acabc | ||
|
|
de965e3693 | ||
|
|
5c3afc5141 | ||
|
|
50814723c5 | ||
|
|
c6919e9fd6 | ||
|
|
6b220f12ae | ||
|
|
2aade4c0c8 | ||
|
|
f16c40a496 | ||
|
|
aed6725d17 | ||
|
|
ac6c240217 | ||
|
|
34ae7f5e0b | ||
|
|
40aa1c7d35 | ||
|
|
b128af464c | ||
|
|
1236152120 |
No files matched your search
@@ -1,2 +1,5 @@
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -32,6 +32,9 @@ jobs:
|
||||
# Remove this whenever we solved those OOM errors.
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
- run: yarn lint --max-warnings=0
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
working-directory: ./scripts
|
||||
- run: yarn lint-changelog
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: ${{ matrix.coverage }}
|
||||
with:
|
||||
|
||||
+191
-31
@@ -10,6 +10,166 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate chosen build in the `eas build:run` command. ([#1614](https://github.com/expo/eas-cli/pull/1614) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [3.2.0](https://github.com/expo/eas-cli/releases/tag/v3.2.0) - 2023-01-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--channel` option to `update:republish`. ([#1580](https://github.com/expo/eas-cli/pull/1580) by [@byCedric](https://github.com/byCedric))
|
||||
- Use `appVersion` as default runtime version policy when running `eas update:configure`. ([#1588](https://github.com/expo/eas-cli/pull/1588) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Support `--json` flag in webhook list command. ([#1605](https://github.com/expo/eas-cli/pull/1605) by [@sheddy7](https://github.com/sheddy7))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Makes eas.json configuration to only run on `update:configure`. ([#1598](https://github.com/expo/eas-cli/pull/1598) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Fix issue with invisible build info in some terminals in the `eas build:run` and `eas build:resign` commands. ([#1602](https://github.com/expo/eas-cli/pull/1602) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Fix issues with invisible build info in some terminals while using the `eas submit` command. ([#1603](https://github.com/expo/eas-cli/pull/1603) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Use the `completedAt` timestamp as the build finish date instead of the `updatedAt` timestamp in the `eas build:run` command. ([#1604](https://github.com/expo/eas-cli/pull/1604) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Make all URLs in logs clickable in terminals supporting hyperlinks. ([#1591](https://github.com/expo/eas-cli/pull/1591) by [@Simek](https://github.com/Simek))
|
||||
- Use paginated select prompt for the `eas build:run` command. ([#1601](https://github.com/expo/eas-cli/pull/1601) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.1.1](https://github.com/expo/eas-cli/releases/tag/v3.1.1) - 2022-12-19
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix update `--channel` and `--branch` flag validation. ([#1579](https://github.com/expo/eas-cli/pull/1579) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [3.1.0](https://github.com/expo/eas-cli/releases/tag/v3.1.0) - 2022-12-17
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Adds `--channel` flag to `eas update`. ([#1567](https://github.com/expo/eas-cli/pull/1567) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Add channel configurations to **eas.json** during `eas update:configure`. ([#1570](https://github.com/expo/eas-cli/pull/1570) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Add `build:resign` command. ([#1575](https://github.com/expo/eas-cli/pull/1575) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.0.0](https://github.com/expo/eas-cli/releases/tag/v3.0.0) - 2022-12-07
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Move update republish to separate command. ([#1533](https://github.com/expo/eas-cli/pull/1533) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make the not recognized image error handler run only if the image is present under a valid key and has an invalid value. ([#1565](https://github.com/expo/eas-cli/pull/1565) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Fix the `node:assert` import error by using imports from `assert` instead of from `node:assert`. ([#1569](https://github.com/expo/eas-cli/pull/1569) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove old republish code from the update command. ([#1535](https://github.com/expo/eas-cli/pull/1535) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [2.9.0](https://github.com/expo/eas-cli/releases/tag/v2.9.0) - 2022-12-05
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add caching for `eas build:run` command. ([#1542](https://github.com/expo/eas-cli/pull/1542) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add `isGitWorkingTreeDirty` to EAS Update records. ([#1550](https://github.com/expo/eas-cli/pull/1550) by [@FiberJW](https://github.com/FiberJW))
|
||||
- Prompt developer to download the app after the simulator build is finished. ([#1554](https://github.com/expo/eas-cli/pull/1554) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Ask for the missing application identifier consistently in `eas build:version:sync` command. ([#1543](https://github.com/expo/eas-cli/pull/1543) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Disable selecting builds whose artifacts have expired in the `eas build:run` command. ([#1547](https://github.com/expo/eas-cli/pull/1547) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Change intent with which the `eas build:run` command opens an Android app to `android.intent.action.MAIN`. ([#1556](https://github.com/expo/eas-cli/pull/1556) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [2.8.0](https://github.com/expo/eas-cli/releases/tag/v2.8.0) - 2022-11-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas build:run` support for Android. ([#1485](https://github.com/expo/eas-cli/pull/1485) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Only create a new channel when update branch is new. ([#1507](https://github.com/expo/eas-cli/pull/1507) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
|
||||
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add a better error message for when an invalid `image` is set in `eas-json`. ([#1531](https://github.com/expo/eas-cli/pull/1531) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Remove internal expo id from device formatter. ([#1541](https://github.com/expo/eas-cli/pull/1541) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
|
||||
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Validate icon PNGs before running Android build. ([#1477](https://github.com/expo/eas-cli/pull/1477) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed the values of the distribution enum on the build profile schema receiving the incorrect descriptions. ([#1504](https://github.com/expo/eas-cli/pull/1504) by [@macksal](https://github.com/macksal))
|
||||
- Remove hard dependency on expo package being installed to init a project. ([#1517](https://github.com/expo/eas-cli/pull/1517) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
|
||||
- Improve choice formatting when selecting a build to submit in the `eas submit` command. ([#1502](https://github.com/expo/eas-cli/pull/1502) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [2.6.0](https://github.com/expo/eas-cli/releases/tag/v2.6.0) - 2022-10-27
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add custom metadata validation command with more complex rules. ([#1416](https://github.com/expo/eas-cli/pull/1416) by [@byCedric](https://github.com/byCedric))
|
||||
- Add `eas build:run` command which runs iOS simulator builds from the CLI. ([#1447](https://github.com/expo/eas-cli/pull/1447) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add feature gate support. ([#1475](https://github.com/expo/eas-cli/pull/1475) by [@wschurman](https://github.com/wschurman))
|
||||
- Warn about outdated build deployment when configuring EAS Update. ([#1467](https://github.com/expo/eas-cli/pull/1467) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace `secret:list` table with formatted fields. ([#1464](https://github.com/expo/eas-cli/pull/1464) by [@byCedric](https://github.com/byCedric))
|
||||
- Replace `device:create` input table with formatted fields. ([#1465](https://github.com/expo/eas-cli/pull/1465) by [@byCedric](https://github.com/byCedric))
|
||||
- Replace `channel:view` table with formatted fields. ([#1466](https://github.com/expo/eas-cli/pull/1466) by [@byCedric](https://github.com/byCedric))
|
||||
- Upgrade dependencies. ([#1480](https://github.com/expo/eas-cli/pull/1480) by [@dsokal](https://github.com/dsokal))
|
||||
- Replace all remaining tables with formatted fields. ([#1481](https://github.com/expo/eas-cli/pull/1481) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [2.5.1](https://github.com/expo/eas-cli/releases/tag/v2.5.1) - 2022-10-24
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert using local CLI for `expo export` ([#1460](https://github.com/expo/eas-cli/pull/1460)). ([#1472](https://github.com/expo/eas-cli/pull/1472) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [2.5.0](https://github.com/expo/eas-cli/releases/tag/v2.5.0) - 2022-10-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Update init command to handle slug and owner. ([#1452](https://github.com/expo/eas-cli/pull/1452) by [@wschurman](https://github.com/wschurman))
|
||||
- Add `eas secret:push`. ([#1457](https://github.com/expo/eas-cli/pull/1457) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Initialize analytics consistently and move analytics into context. ([#1444](https://github.com/expo/eas-cli/pull/1444) by [@wschurman](https://github.com/wschurman))
|
||||
- - Skip using `--non-interactive` and `--experimental-bundle` flags when local Expo CLI is installed. ([#1460](https://github.com/expo/eas-cli/pull/1460) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Handle case where git is installed but not working properly. ([#1454](https://github.com/expo/eas-cli/pull/1454) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Remove table logging in `update:view`. ([#1463](https://github.com/expo/eas-cli/pull/1463) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Fix install prompt language. ([#1462](https://github.com/expo/eas-cli/pull/1462) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [2.4.1](https://github.com/expo/eas-cli/releases/tag/v2.4.1) - 2022-10-17
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump `eas-cli-local-build-plugin` to 0.0.144 to not require `yarn` for local builds. ([c05ef](https://github.com/expo/eas-cli/commit/c05ef9cccaa819d8e5fb84858cb09cadaae90134) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
@@ -66,10 +226,10 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🧹 Chores
|
||||
|
||||
- Standardize the GQL 'Update' schema using UpdateFragmentNode. ([#1348](https://github.com/expo/eas-cli/pull/1348) by [@kgc00](https://github.com/kgc00))
|
||||
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373), by [@wschurman](https://github.com/wschurman))
|
||||
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373) by [@wschurman](https://github.com/wschurman))
|
||||
- Add Xcode 14 image. ([#1365](https://github.com/expo/eas-cli/pull/1365) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Update max `versionCode` for Android. ([#1400](https://github.com/expo/eas-cli/pull/1400) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add and use command context to declare command dependencies. ([#1383](https://github.com/expo/eas-cli/pull/1383), [#1384](https://github.com/expo/eas-cli/pull/1384), [#1387](https://github.com/expo/eas-cli/pull/1387), [#1388](https://github.com/expo/eas-cli/pull/1388), [#1390](https://github.com/expo/eas-cli/pull/1390), [#1391](https://github.com/expo/eas-cli/pull/1391), [#1394](https://github.com/expo/eas-cli/pull/1394), [#1402](https://github.com/expo/eas-cli/pull/1402), [#1403](https://github.com/expo/eas-cli/pull/1403), by [@wschurman](https://github.com/wschurman))
|
||||
- Add and use command context to declare command dependencies. ([#1383](https://github.com/expo/eas-cli/pull/1383), [#1384](https://github.com/expo/eas-cli/pull/1384), [#1387](https://github.com/expo/eas-cli/pull/1387), [#1388](https://github.com/expo/eas-cli/pull/1388), [#1390](https://github.com/expo/eas-cli/pull/1390), [#1391](https://github.com/expo/eas-cli/pull/1391), [#1394](https://github.com/expo/eas-cli/pull/1394), [#1402](https://github.com/expo/eas-cli/pull/1402), [#1403](https://github.com/expo/eas-cli/pull/1403) by [@wschurman](https://github.com/wschurman))
|
||||
- Fix typo in the ad hoc build message. ([#1407](https://github.com/expo/eas-cli/pull/1407) by [@Simek](https://github.com/Simek))
|
||||
- Improve errors and error messages formatting related to **eas.json**. ([#1414](https://github.com/expo/eas-cli/pull/1414) by [@Simek](https://github.com/Simek))
|
||||
- Handle errors from platform-specific kill switches to disable free tier builds. ([#1401](https://github.com/expo/eas-cli/pull/1401) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
@@ -214,7 +374,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
|
||||
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
|
||||
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
|
||||
|
||||
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
|
||||
|
||||
@@ -245,7 +405,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00/))
|
||||
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00))
|
||||
- Allow users to skip metadata validation. ([#1175](https://github.com/expo/eas-cli/pull/1175) by [@byCedric](https://github.com/byCedric))
|
||||
- Add experimental `--resource-class` flag for Build commands. ([#1138](https://github.com/expo/eas-cli/pull/1138) by [@christopherwalter](https://github.com/christopherwalter))
|
||||
- Truncate long messages for `eas update` command, rather than failing. ([#1178](https://github.com/expo/eas-cli/pull/1178) by [@kgc00](https://github.com/kgc00))
|
||||
@@ -282,13 +442,13 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
|
||||
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00))
|
||||
- Added first beta of `eas metadata` to sync store information using store configuration files ([#1136](https://github.com/expo/eas-cli/pull/1136) by [@bycedric](https://github.com/bycedric))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00/))
|
||||
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
|
||||
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00))
|
||||
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -298,7 +458,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00/))
|
||||
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -469,7 +629,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
|
||||
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot), [@dsokal](https://github.com/dsokal))
|
||||
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
|
||||
@@ -521,7 +681,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
|
||||
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates. ([#854](https://github.com/expo/eas-cli/pull/854) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Adds `eas update`, which can bundle and publish updates on a branch.
|
||||
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
|
||||
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
|
||||
@@ -567,7 +727,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
|
||||
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales), [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -603,7 +763,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pull/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
@@ -701,7 +861,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701), [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
@@ -733,7 +893,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
|
||||
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
|
||||
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
|
||||
```json
|
||||
@@ -779,7 +939,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
|
||||
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95), [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -791,7 +951,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project. ([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
|
||||
@@ -925,7 +1085,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix wrong warning for `metro.config.js` check on Windows. ([#588](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
@@ -1005,7 +1165,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
|
||||
- Include the file extension for update's assets in the manifest fragment. ([#535](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
|
||||
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
@@ -1025,7 +1185,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
|
||||
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/520) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -1039,7 +1199,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
|
||||
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504), [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
|
||||
@@ -1119,15 +1279,15 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- 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))
|
||||
- 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))
|
||||
- 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))
|
||||
- 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))
|
||||
|
||||
@@ -1141,7 +1301,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Android credentials setup now on Graphql API. ([#427](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
|
||||
|
||||
@@ -1173,7 +1333,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🎉 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))
|
||||
- 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))
|
||||
@@ -1184,7 +1344,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- 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).
|
||||
- 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
|
||||
@@ -1211,7 +1371,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- 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))
|
||||
- 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))
|
||||
@@ -1272,7 +1432,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 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))
|
||||
- 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
|
||||
|
||||
@@ -1297,7 +1457,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🎉 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))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/311) 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))
|
||||
|
||||
@@ -1381,7 +1541,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
@@ -1408,7 +1568,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `"buildType" is not allowed` error. ([595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
|
||||
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||
packages/eas-cli/src/submit @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @dsokal
|
||||
|
||||
packages/eas-cli/src/update @kgc00 @wschurman
|
||||
packages/eas-cli/src/commands/update @kgc00 @wschurman
|
||||
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
|
||||
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
|
||||
|
||||
packages/eas-cli/schema @bycedric
|
||||
packages/eas-cli/src/metadata @bycedric
|
||||
|
||||
+47
-3
@@ -1,4 +1,48 @@
|
||||
## Releasing a new version
|
||||
# Development
|
||||
|
||||
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
|
||||
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
|
||||
Open your `.zshrc` or other config file and add:
|
||||
|
||||
```
|
||||
alias easd="/PATH/TO/eas-cli/bin/run"
|
||||
```
|
||||
|
||||
Then use it with `easd` like `easd build`.
|
||||
|
||||
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Format
|
||||
|
||||
- Be sure to update the [`CHANGELOG.md`](./CHANGELOG.md) with changes for every PR. There is a changelog bot that can generate the proper entry for you. The instructions on how to use it are in the PR description placeholder.
|
||||
- End `async` functions with `Async` like `runAsync`. This is just how we format functions at Expo.
|
||||
- Utilize the unified `Log` module instead of `console.log`.
|
||||
|
||||
## Using local and staging API
|
||||
|
||||
For development against local API:
|
||||
|
||||
- Set up the API server as described at https://github.com/expo/universe/tree/main/server/www.
|
||||
- Set the `EXPO_LOCAL=1` env variable either on every command run (e.g. `EXPO_LOCAL=1 easd ...`) or more permamently with `export EXPO_LOCAL=1` (works in a current shell session).
|
||||
|
||||
For development against staging API:
|
||||
|
||||
- Set the `EXPO_STAGING=1` env variable either on every command run (e.g. `EXPO_STAGING=1 easd ...`) or more permamently with `export EXPO_STAGING=1` (works in a current shell session).
|
||||
|
||||
## Working on local builds (`eas build --local`)
|
||||
|
||||
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
|
||||
|
||||
## Testing
|
||||
|
||||
Run `yarn test` either in the repository root or in a package directory that you're working on.
|
||||
|
||||
## Releasing
|
||||
|
||||
See [RELEASING.md](./RELEASING.md).
|
||||
@@ -0,0 +1,14 @@
|
||||
# Releasing EAS CLI
|
||||
|
||||
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
|
||||
## Choosing the next version
|
||||
|
||||
As stated above, the next EAS CLI version is chosen automatically based on the existing changelog entries under the unreleased section in **CHANGELOG.md**.
|
||||
|
||||
The algorithm works as follows:
|
||||
|
||||
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
|
||||
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
|
||||
- Otherwise, bump the PATCH version.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "2.4.1"
|
||||
"version": "3.2.1"
|
||||
}
|
||||
+11
-10
@@ -8,28 +8,29 @@
|
||||
"build:tarballs:linux": "yarn build-for-size-check",
|
||||
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"start": "lerna run watch --stream --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "./scripts/bin/run release",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "28.1.6",
|
||||
"@types/node": "18.6.5",
|
||||
"eslint": "8.21.0",
|
||||
"@types/jest": "29.2.0",
|
||||
"@types/node": "18.11.5",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-universe": "11.1.0",
|
||||
"eslint-plugin-async-protect": "2.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "28.1.3",
|
||||
"jest-watch-typeahead": "2.0.0",
|
||||
"lerna": "4.0.0",
|
||||
"oclif": "3.1.2",
|
||||
"jest": "29.2.2",
|
||||
"jest-watch-typeahead": "2.2.0",
|
||||
"lerna": "6.0.1",
|
||||
"oclif": "3.2.22",
|
||||
"prettier": "2.7.1",
|
||||
"ts-jest": "28.0.7",
|
||||
"typescript": "4.7.4"
|
||||
"ts-jest": "29.0.3",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
|
||||
+228
-57
@@ -70,6 +70,8 @@ eas --help COMMAND
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:inspect`](#eas-buildinspect)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:resign`](#eas-buildresign)
|
||||
* [`eas build:run`](#eas-buildrun)
|
||||
* [`eas build:submit`](#eas-buildsubmit)
|
||||
* [`eas build:version:set`](#eas-buildversionset)
|
||||
* [`eas build:version:sync`](#eas-buildversionsync)
|
||||
@@ -89,19 +91,24 @@ eas --help COMMAND
|
||||
* [`eas init`](#eas-init)
|
||||
* [`eas login`](#eas-login)
|
||||
* [`eas logout`](#eas-logout)
|
||||
* [`eas metadata:lint`](#eas-metadatalint)
|
||||
* [`eas metadata:pull`](#eas-metadatapull)
|
||||
* [`eas metadata:push`](#eas-metadatapush)
|
||||
* [`eas open`](#eas-open)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas run`](#eas-run)
|
||||
* [`eas run:run`](#eas-runrun)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas secret:push`](#eas-secretpush)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas update`](#eas-update)
|
||||
* [`eas update:configure`](#eas-updateconfigure)
|
||||
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
* [`eas update:list`](#eas-updatelist)
|
||||
* [`eas update:republish`](#eas-updaterepublish)
|
||||
* [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
@@ -125,7 +132,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -142,7 +149,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -159,7 +166,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -173,7 +180,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -202,7 +209,7 @@ EXAMPLES
|
||||
$ eas autocomplete --refresh-cache
|
||||
```
|
||||
|
||||
_See code: [@expo/plugin-autocomplete](https://github.com/expo/plugin-autocomplete/blob/v1.4.0/src/commands/autocomplete/index.ts)_
|
||||
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.4/src/commands/autocomplete/index.ts)_
|
||||
|
||||
## `eas branch:create [NAME]`
|
||||
|
||||
@@ -223,7 +230,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -244,7 +251,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -264,7 +271,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -284,7 +291,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -307,7 +314,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -337,7 +344,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -354,7 +361,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -371,7 +378,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -406,7 +413,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -443,7 +450,57 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
re-sign a build archive
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:resign [-p android|ios] [-e <value>] [--wait] [--id <value>] [--offset <value>] [--limit <value>]
|
||||
[--json --non-interactive]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
|
||||
eas.json.
|
||||
-p, --platform=(android|ios)
|
||||
--id=<value> ID of the build to re-sign.
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--[no-]wait Wait for build(s) to complete.
|
||||
|
||||
DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
run simulator/emulator builds from eas-cli
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios)
|
||||
--id=<value> ID of the simulator/emulator build to run
|
||||
--latest Run the latest simulator/emulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator/emulator build archive or app
|
||||
--url=<value> Simulator/Emulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -463,7 +520,7 @@ FLAGS
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=<value> Path to the .apk/.aab/.ipa file
|
||||
--url=<value> App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--verbose Always print logs from EAS Submit
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
@@ -490,7 +547,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -509,7 +566,7 @@ DESCRIPTION
|
||||
Update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -526,7 +583,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -547,7 +604,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -569,7 +626,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -589,7 +646,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -612,7 +669,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -631,7 +688,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -648,7 +705,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -662,7 +719,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -682,7 +739,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -703,7 +760,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -717,7 +774,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -731,7 +788,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -751,7 +808,7 @@ DESCRIPTION
|
||||
display help for eas-cli
|
||||
```
|
||||
|
||||
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.3.0/src/commands/help.ts)_
|
||||
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.16/src/commands/help.ts)_
|
||||
|
||||
## `eas init`
|
||||
|
||||
@@ -803,6 +860,24 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
## `eas metadata:lint`
|
||||
|
||||
validate the local store configuration
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:lint [--json] [--profile <value>]
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
generate the local store configuration from the app stores
|
||||
@@ -818,7 +893,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -835,7 +910,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas open`
|
||||
|
||||
@@ -849,7 +924,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -863,7 +938,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -885,7 +960,55 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas run`
|
||||
|
||||
run simulator build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the simulator build to run
|
||||
--latest Run the latest simulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator build file file
|
||||
--url=<value> Simulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator build
|
||||
```
|
||||
|
||||
_See code: [src/commands/run/index.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/run/index.ts)_
|
||||
|
||||
## `eas run:run`
|
||||
|
||||
run simulator build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas run:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the simulator build to run
|
||||
--latest Run the latest simulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator build file file
|
||||
--url=<value> Simulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator build
|
||||
```
|
||||
|
||||
_See code: [src/commands/run/run.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/run/run.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -908,7 +1031,7 @@ DESCRIPTION
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -926,7 +1049,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -940,7 +1063,27 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas secret:push`
|
||||
|
||||
read environment secrets from env file and store on the server
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:push [--scope account|project] [--env-file <value>] [--force] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
--env-file=<value> Env file with secrets
|
||||
--force Delete and recreate existing secrets
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--scope=(account|project) [default: project] Scope for the secrets
|
||||
|
||||
DESCRIPTION
|
||||
read environment secrets from env file and store on the server
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -960,7 +1103,7 @@ FLAGS
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=<value> Path to the .apk/.aab/.ipa file
|
||||
--url=<value> App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--verbose Always print logs from EAS Submit
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
@@ -970,7 +1113,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -978,14 +1121,16 @@ publish an update group
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
|
||||
[--group <value> | | ] [-p android|ios|all] [--auto] [--private-key-path <value>] [--json --non-interactive]
|
||||
$ eas update [--branch <value>] [--channel <value>] [--message <value>] [--republish | --input-dir <value>
|
||||
| --skip-bundler] [--group <value> | | ] [-p android|ios|all] [--auto] [--private-key-path <value>] [--json
|
||||
--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
--auto Use the current git branch and commit message for the EAS branch and update message
|
||||
--branch=<value> Branch to publish the update group on
|
||||
--group=<value> Update group to republish
|
||||
--channel=<value> Channel that the published update should affect
|
||||
--group=<value> Update group to republish (deprecated, see republish command)
|
||||
--input-dir=<value> [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--message=<value> A short message describing the update
|
||||
@@ -993,14 +1138,14 @@ FLAGS
|
||||
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
|
||||
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
|
||||
certificate's directory.
|
||||
--republish Republish an update group
|
||||
--republish Republish an update group (deprecated, see republish command)
|
||||
--skip-bundler Skip running Expo CLI to bundle the app before publishing
|
||||
|
||||
DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1008,16 +1153,17 @@ configure the project to support EAS Update
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:configure [-p android|ios|all]
|
||||
$ eas update:configure [-p android|ios|all] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all] Platform to configure
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1038,7 +1184,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1060,7 +1206,31 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
rollback to an existing update
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [--message <value>] [-p
|
||||
android|ios|all] [--json --non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
--branch=<value> Branch name to select an update to republish from
|
||||
--channel=<value> Channel name to select an update to republish from
|
||||
--group=<value> Update group ID to republish
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--message=<value> Short message describing the republished update
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
rollback to an existing update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -1080,7 +1250,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1101,7 +1271,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1121,7 +1291,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1129,16 +1299,17 @@ list webhooks
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:list [--event BUILD|SUBMIT]
|
||||
$ eas webhook:list [--event BUILD|SUBMIT] [--json]
|
||||
|
||||
FLAGS
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
|
||||
DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1160,7 +1331,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1177,7 +1348,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.2.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ documents:
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
- 'src/branch/**/*.ts'
|
||||
- 'src/channel/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
|
||||
Generated
+1239
-84
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "2.4.1",
|
||||
"version": "3.2.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -9,50 +9,53 @@
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.34",
|
||||
"@expo/code-signing-certificates": "0.0.2",
|
||||
"@expo/code-signing-certificates": "0.0.3",
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.94",
|
||||
"@expo/eas-json": "2.2.0",
|
||||
"@expo/eas-build-job": "0.2.97",
|
||||
"@expo/eas-json": "3.0.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
"@expo/package-manager": "0.0.56",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.18",
|
||||
"@expo/plugin-autocomplete": "1.4.0",
|
||||
"@expo/plugin-help": "5.3.0",
|
||||
"@expo/plugin-warn-if-update-available": "2.3.0",
|
||||
"@expo/plugin-help": "5.1.16",
|
||||
"@expo/plugin-warn-if-update-available": "2.4.0",
|
||||
"@expo/prebuild-config": "5.0.3",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.13.10",
|
||||
"@urql/core": "2.6.1",
|
||||
"@urql/exchange-retry": "0.3.3",
|
||||
"ajv": "^6.12.6",
|
||||
"@oclif/core": "1.19.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.4",
|
||||
"@segment/ajv-human-errors": "^2.1.2",
|
||||
"@urql/core": "3.0.5",
|
||||
"@urql/exchange-retry": "1.0.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"better-opn": "3.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.11.2",
|
||||
"cli-table3": "0.6.2",
|
||||
"dateformat": "4.6.3",
|
||||
"dotenv": "16.0.3",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.2.11",
|
||||
"fast-glob": "3.2.12",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"getenv": "1.0.0",
|
||||
"gradle-to-js": "2.0.1",
|
||||
"graphql": "16.5.0",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"ignore": "5.2.0",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.2",
|
||||
"joi": "17.6.0",
|
||||
"jks-js": "1.1.0",
|
||||
"joi": "17.6.4",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
@@ -63,51 +66,55 @@
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"pngjs": "6.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-retry": "2.0.1",
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.7",
|
||||
"semver": "7.3.8",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"uuid": "9.0.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.11.5",
|
||||
"@graphql-codegen/cli": "2.13.7",
|
||||
"@graphql-codegen/introspection": "2.2.1",
|
||||
"@graphql-codegen/typescript": "2.7.3",
|
||||
"@graphql-codegen/typescript-operations": "2.5.3",
|
||||
"@graphql-codegen/typescript": "2.8.0",
|
||||
"@graphql-codegen/typescript-operations": "2.5.5",
|
||||
"@types/cli-progress": "3.11.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.0.4",
|
||||
"@types/node-forge": "1.3.0",
|
||||
"@types/pngjs": "6.0.1",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.0.14",
|
||||
"@types/semver": "7.3.10",
|
||||
"@types/tar": "6.1.2",
|
||||
"@types/prompts": "2.4.1",
|
||||
"@types/semver": "7.3.12",
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.18.2",
|
||||
"memfs": "3.4.7",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.9",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-deepmerge": "2.0.5",
|
||||
"ts-deepmerge": "4.0.0",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -129,7 +136,7 @@
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@expo/plugin-autocomplete",
|
||||
"@oclif/plugin-autocomplete",
|
||||
"@expo/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
],
|
||||
|
||||
@@ -778,7 +778,7 @@
|
||||
},
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"const": 0,
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fixS3Url } from '../uploads';
|
||||
|
||||
describe(fixS3Url, () => {
|
||||
it('fixes broken links', () => {
|
||||
const input =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
const expectedOutput =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
expect(fixS3Url(input)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
it('does not change correct urls', () => {
|
||||
const correctUrl =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
expect(fixS3Url(correctUrl)).toBe(correctUrl);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
export type AnalyticsEvent = CommandEvent | BuildEvent | SubmissionEvent | MetadataEvent;
|
||||
|
||||
export enum CommandEvent {
|
||||
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
|
||||
}
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export enum MetadataEvent {
|
||||
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
||||
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
||||
}
|
||||
|
||||
export type AnalyticsEventProperties = Record<string, string | number | boolean>;
|
||||
|
||||
/**
|
||||
* The interface for commands to use to log events to analytics.
|
||||
*/
|
||||
export interface Analytics {
|
||||
logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for commands to use to orchestrate the analytics system. Should only be necessary to use
|
||||
* this within EASCommand.
|
||||
*/
|
||||
export interface AnalyticsWithOrchestration extends Analytics {
|
||||
setActor(actor: Actor): void;
|
||||
flushAsync(): Promise<void>;
|
||||
}
|
||||
|
||||
const USER_SETTINGS_KEY_AMPLITUDE_ENABLED = 'amplitudeEnabled';
|
||||
const USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID = 'amplitudeDeviceId';
|
||||
const USER_SETTINGS_KEY_ANALYTICS_ENABLED = 'analyticsEnabled';
|
||||
const USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID = 'analyticsDeviceId';
|
||||
|
||||
/**
|
||||
* Sets the user's analytics enabled preference. Note that this will only take effect
|
||||
* upon the next run of the CLI.
|
||||
*/
|
||||
export async function setAnalyticsEnabledAsync(enabled: boolean): Promise<void> {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's analytics enabled preference.
|
||||
*/
|
||||
export async function getAnalyticsEnabledAsync(): Promise<boolean> {
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, null);
|
||||
return !!analyticsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of Analytics based on the user's analytics enabled preferences.
|
||||
*/
|
||||
export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED, null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED);
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync(
|
||||
USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID,
|
||||
null
|
||||
);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID);
|
||||
}
|
||||
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
|
||||
if (!analyticsEnabled) {
|
||||
return new NoOpAnalytics();
|
||||
}
|
||||
|
||||
const persistedDeviceId = await UserSettings.getAsync(
|
||||
USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID,
|
||||
null
|
||||
);
|
||||
const deviceId = persistedDeviceId ?? uuidv4();
|
||||
if (!persistedDeviceId) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, deviceId);
|
||||
}
|
||||
return new RudderstackAnalytics(deviceId);
|
||||
}
|
||||
|
||||
class NoOpAnalytics implements AnalyticsWithOrchestration {
|
||||
logEvent(): void {}
|
||||
setActor(): void {}
|
||||
async flushAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
const RudderstackAnalyticsConfig =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
class RudderstackAnalytics implements AnalyticsWithOrchestration {
|
||||
private readonly rudderstackClient = new RudderAnalytics(
|
||||
RudderstackAnalyticsConfig.rudderstackWriteKey,
|
||||
new URL('/v1/batch', RudderstackAnalyticsConfig.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
|
||||
private identifiedActor: Actor | null = null;
|
||||
|
||||
constructor(private readonly persistentDeviceId: string) {
|
||||
// identify once with just anonymous ID. Once the actor is fetched, re-indentify with
|
||||
// both so that they can be associated
|
||||
this.rudderstackClient.identify({
|
||||
anonymousId: persistentDeviceId,
|
||||
});
|
||||
}
|
||||
|
||||
public setActor(actor: Actor): void {
|
||||
if (this.identifiedActor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rudderstackClient.identify({
|
||||
userId: actor.id,
|
||||
anonymousId: this.persistentDeviceId,
|
||||
traits: {
|
||||
username: getActorDisplayName(actor),
|
||||
user_id: actor.id,
|
||||
user_type: actor.__typename,
|
||||
},
|
||||
});
|
||||
this.identifiedActor = actor;
|
||||
}
|
||||
|
||||
public logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void {
|
||||
const userId = this.identifiedActor?.id;
|
||||
const deviceId = this.persistentDeviceId;
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
this.rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: this.getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
public async flushAsync(): Promise<void> {
|
||||
await this.rudderstackClient.flush();
|
||||
}
|
||||
|
||||
private getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
import { Analytics, Event } from './events';
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
|
||||
|
||||
export async function withAnalyticsAsync<Result>(
|
||||
analytics: Analytics,
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
attemptEvent: Event;
|
||||
successEvent: Event;
|
||||
failureEvent: Event;
|
||||
trackingCtx: TrackingContext;
|
||||
{
|
||||
attemptEvent,
|
||||
successEvent,
|
||||
failureEvent,
|
||||
properties,
|
||||
}: {
|
||||
attemptEvent: AnalyticsEvent;
|
||||
successEvent: AnalyticsEvent;
|
||||
failureEvent: AnalyticsEvent;
|
||||
properties: AnalyticsEventProperties;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
analytics.logEvent(attemptEvent, properties);
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
analytics.logEvent(successEvent, properties);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
analytics.logEvent(failureEvent, {
|
||||
...properties,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { logEvent } from './rudderstackClient';
|
||||
export type Event = BuildEvent | SubmissionEvent | MetadataEvent;
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export enum MetadataEvent {
|
||||
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
||||
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
||||
}
|
||||
|
||||
export class Analytics {
|
||||
static logEvent(name: Event, properties: Record<string, any>): void {
|
||||
logEvent(name, properties);
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
let rudderstackClient: RudderAnalytics | null = null;
|
||||
let userIdentified = false;
|
||||
let identifyData: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
traits: Record<string, any>;
|
||||
} | null = null;
|
||||
|
||||
export async function initAsync(): Promise<void> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync('amplitudeEnabled');
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
|
||||
}
|
||||
// TODO: cut here
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync('analyticsEnabled', false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
|
||||
if (analyticsEnabled) {
|
||||
const config =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
rudderstackClient = new RudderAnalytics(
|
||||
config.rudderstackWriteKey,
|
||||
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
|
||||
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
|
||||
const deviceId = savedDeviceId ?? uuidv4();
|
||||
if (!savedDeviceId) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', deviceId);
|
||||
}
|
||||
|
||||
identifyData = {
|
||||
userId,
|
||||
deviceId,
|
||||
traits,
|
||||
};
|
||||
|
||||
ensureUserIdentified();
|
||||
}
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (rudderstackClient) {
|
||||
await rudderstackClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent(name: string, properties: Record<string, any> = {}): void {
|
||||
if (!rudderstackClient) {
|
||||
return;
|
||||
}
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
function ensureUserIdentified(): void {
|
||||
if (!rudderstackClient || userIdentified || !identifyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
traits: identifyData.traits,
|
||||
});
|
||||
userIdentified = true;
|
||||
}
|
||||
|
||||
function getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
|
||||
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,17 +1,25 @@
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { UpdateBranchFragment } from '../graphql/generated';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
UpdateBranchFragment,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import Log from '../log';
|
||||
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
|
||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
SelectPromptEntry,
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { BranchNotFoundError } from './utils';
|
||||
|
||||
export const BRANCHES_LIMIT = 50;
|
||||
|
||||
@@ -24,7 +32,7 @@ export async function selectBranchOnAppAsync(
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
|
||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
|
||||
promptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
@@ -41,7 +49,7 @@ export async function selectBranchOnAppAsync(
|
||||
promptOptions: {
|
||||
title: promptTitle,
|
||||
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
|
||||
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
|
||||
makePartialChoiceObject: displayTextForListItem,
|
||||
},
|
||||
});
|
||||
if (!selectedBranch) {
|
||||
@@ -102,34 +110,75 @@ function renderPageOfBranches(
|
||||
if (json) {
|
||||
printJsonOnlyOutput(currentPage);
|
||||
} else {
|
||||
const table = new CliTable({
|
||||
head: ['Branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
table.push(
|
||||
...currentPage.map(branch => {
|
||||
if (branch.updates.length === 0) {
|
||||
return [branch.name, 'N/A', 'N/A', 'N/A', 'N/A'];
|
||||
}
|
||||
|
||||
const latestUpdateOnBranch = branch.updates[0];
|
||||
return [
|
||||
branch.name,
|
||||
formatUpdateMessage(latestUpdateOnBranch),
|
||||
latestUpdateOnBranch.runtimeVersion,
|
||||
latestUpdateOnBranch.group,
|
||||
getPlatformsForGroup({
|
||||
group: latestUpdateOnBranch.group,
|
||||
updates: branch.updates,
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branches:'));
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(table.toString());
|
||||
Log.log(
|
||||
currentPage
|
||||
.map(branch => formatBranch(getBranchDescription(branch)))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
||||
if (!newBranch) {
|
||||
throw new Error(`Could not create branch ${name}.`);
|
||||
}
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
export async function ensureBranchExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchName: string;
|
||||
}
|
||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
const { id } = updateBranch;
|
||||
return { branchId: id, createdBranch: false };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,142 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { ExitError } from '@oclif/core/lib/errors';
|
||||
import path from 'path';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { CommonContext } from '../context';
|
||||
import { validatePNGsForManagedProjectAsync } from '../validate';
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
|
||||
describe(validatePNGsForManagedProjectAsync, () => {
|
||||
it('does not validate PNGs for generic projects', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
||||
const ctx = instance(ctxMock);
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('does not validate PNGs for projects with SDK >= 47', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
||||
sdkVersion: '47.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
describe(Platform.ANDROID, () => {
|
||||
it('exits if foregroundImage is not a file with .png extension', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if foregroundImage is not a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('does not throw if foregroundImage is a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
xdescribe(Platform.IOS, () => {
|
||||
it('exits if icon is not a file with .png extension', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if icon is not a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if icon has alpha channel (transparency)', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('does not throw if icon is a png file and does not have alpha channel', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
@@ -53,6 +57,7 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import cliProgress from 'cli-progress';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { BuildEvent } from '../analytics/events';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { uploadFileAtPathToS3Async } from '../uploads';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
@@ -78,21 +78,26 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
|
||||
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.syncProjectConfigurationAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -108,7 +113,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
type: ArchiveSourceType.GCS,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
@@ -192,6 +197,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
let projectTarballPath;
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
@@ -213,9 +219,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
const { bucketKey } = await uploadFileAtPathToS3Async(
|
||||
const bucketKey = await uploadFileAtPathToGCSAsync(
|
||||
ctx.graphqlClient,
|
||||
UploadSessionType.EasBuildProjectSources,
|
||||
UploadSessionType.EasBuildGcsProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
@@ -232,7 +238,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
@@ -250,6 +256,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||
@@ -269,12 +276,12 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
type MaybeBuildFragment = BuildFragment | null;
|
||||
export type MaybeBuildFragment = BuildFragment | null;
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
@@ -355,7 +362,7 @@ async function handleSingleBuildProgressAsync(
|
||||
spinner.succeed('Build finished');
|
||||
return { refetch: false };
|
||||
case BuildStatus.New:
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}.`;
|
||||
break;
|
||||
@@ -376,7 +383,7 @@ async function handleSingleBuildProgressAsync(
|
||||
Log.newLine();
|
||||
Log.log('Start builds sooner in the priority queue.');
|
||||
Log.log(
|
||||
`Sign up for EAS Production or Enterprise at ${chalk.underline(
|
||||
`Sign up for EAS Production or Enterprise at ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
@@ -416,8 +423,8 @@ async function handleSingleBuildProgressAsync(
|
||||
}
|
||||
|
||||
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
|
||||
[BuildPriority.Normal]: 'queue',
|
||||
[BuildPriority.NormalPlus]: 'queue',
|
||||
[BuildPriority.Normal]: 'Free tier queue',
|
||||
[BuildPriority.NormalPlus]: 'Free tier queue',
|
||||
[BuildPriority.High]: 'priority queue',
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
@@ -46,9 +46,10 @@ export interface BuildContext<T extends Platform> {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
message?: string;
|
||||
trackingCtx: TrackingContext;
|
||||
analyticsEventProperties: AnalyticsEventProperties;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
workflow: Workflow;
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
|
||||
@@ -5,8 +5,7 @@ import getenv from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
@@ -33,6 +32,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
@@ -48,6 +48,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
message?: string;
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
|
||||
@@ -64,6 +65,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
});
|
||||
@@ -73,9 +75,10 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
const analyticsEventProperties = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
@@ -83,7 +86,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
no_wait: noWait,
|
||||
run_from_ci: runFromCI,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName: account.name,
|
||||
@@ -101,9 +104,10 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
trackingCtx,
|
||||
analyticsEventProperties,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
workflow,
|
||||
message,
|
||||
runFromCI,
|
||||
@@ -131,7 +135,7 @@ function getDevClientEventProperties({
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
}): Partial<AnalyticsEventProperties> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
|
||||
@@ -16,6 +16,11 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
type: ProjectArchiveSourceType.S3,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.GCS) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Gcs,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Url,
|
||||
|
||||
@@ -10,7 +10,11 @@ import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/ta
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
@@ -28,6 +32,7 @@ export async function createIosContextAsync(
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
@@ -31,6 +33,27 @@ export async function ensureIosCredentialsAsync(
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureIosCredentialsForBuildResignAsync(
|
||||
credentialsCtx: CredentialsContext,
|
||||
targets: Target[],
|
||||
buildProfile: BuildProfile<Platform.IOS>
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(credentialsCtx, {
|
||||
app: await getAppFromContextAsync(credentialsCtx),
|
||||
targets,
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
const { credentialsSource } = buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
};
|
||||
}
|
||||
|
||||
function transformIosSecrets(secrets: {
|
||||
export function transformIosSecrets(secrets: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
}): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
@@ -79,6 +81,22 @@ export async function prepareJobAsync(
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
for (const targetName of Object.keys(credentials ?? {})) {
|
||||
buildCredentials.push({
|
||||
targetName,
|
||||
provisioningProfileBase64: nullthrows(credentials?.[targetName].provisioningProfile),
|
||||
distributionCertificate: {
|
||||
dataBase64: nullthrows(credentials?.[targetName].distributionCertificate.certificateP12),
|
||||
password: nullthrows(credentials?.[targetName].distributionCertificate.certificatePassword),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { buildCredentials };
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
|
||||
@@ -6,7 +6,7 @@ import semver from 'semver';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.114';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.120';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
? 'simulator'
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata: Metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
trackingContext: ctx.analyticsEventProperties,
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
|
||||
@@ -2,11 +2,15 @@ import chalk from 'chalk';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { BuildFilter, BuildFragment } from '../graphql/generated';
|
||||
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { fromNow } from '../utils/date';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { formatGraphQLBuild } from './utils/formatBuild';
|
||||
|
||||
export const BUILDS_LIMIT = 50;
|
||||
@@ -53,6 +57,110 @@ export async function listAndRenderBuildsOnAppAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAndSelectBuildOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
title,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
selectPromptDisabledFunction,
|
||||
selectPromptWarningMessage,
|
||||
}: {
|
||||
projectId: string;
|
||||
title: string;
|
||||
filter?: BuildFilter;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
|
||||
selectPromptWarningMessage?: string;
|
||||
}
|
||||
): Promise<BuildFragment | null> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select a build in non-interactive mode.');
|
||||
} else {
|
||||
const selectedBuild = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit,
|
||||
offset,
|
||||
filter,
|
||||
}),
|
||||
promptOptions: {
|
||||
title,
|
||||
getIdentifierForQueryItem: build => build.id,
|
||||
makePartialChoiceObject: createBuildToPartialChoiceMaker(selectPromptDisabledFunction),
|
||||
selectPromptWarningMessage,
|
||||
},
|
||||
});
|
||||
return selectedBuild ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBuildChoiceValue(value: string | undefined | null): string {
|
||||
return value ? chalk.bold(value) : 'Unknown';
|
||||
}
|
||||
|
||||
function createBuildToPartialChoiceMaker(
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
|
||||
) {
|
||||
return (
|
||||
build: BuildFragment
|
||||
): {
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
} => {
|
||||
const splitCommitMessage = build.gitCommitMessage?.split('\n');
|
||||
const formattedCommitData =
|
||||
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
|
||||
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
|
||||
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
|
||||
)}"`
|
||||
: 'Unknown';
|
||||
|
||||
return {
|
||||
title: `${chalk.bold(`ID:`)} ${build.id} (${chalk.bold(
|
||||
`${fromNow(new Date(build.completedAt))} ago`
|
||||
)})`,
|
||||
description: [
|
||||
`\t${chalk.bold(`Version:`)} ${formatBuildChoiceValue(build.appVersion)}`,
|
||||
`\t${chalk.bold(
|
||||
build.platform === AppPlatform.Ios ? 'Build number:' : 'Version code:'
|
||||
)} ${formatBuildChoiceValue(build.appBuildVersion)}`,
|
||||
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
|
||||
].join('\n'),
|
||||
disabled: selectPromptDisabledFunction?.(build),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLatestBuildAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
filter,
|
||||
}: {
|
||||
projectId: string;
|
||||
filter?: BuildFilter;
|
||||
}
|
||||
): Promise<BuildFragment | null> {
|
||||
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
filter,
|
||||
});
|
||||
|
||||
if (builds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return builds[0];
|
||||
}
|
||||
|
||||
function renderPageOfBuilds({
|
||||
builds,
|
||||
projectDisplayName,
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
EasJsonUtils,
|
||||
SubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { Analytics } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -36,6 +38,9 @@ import {
|
||||
validateAppConfigForRemoteVersionSource,
|
||||
validateBuildProfileVersionSettings,
|
||||
} from '../project/remoteVersionSource';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { runAsync } from '../run/run';
|
||||
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
|
||||
@@ -45,11 +50,13 @@ import {
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
|
||||
import { Actor } from '../user/User';
|
||||
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
|
||||
import { truthy } from '../utils/expodash/filter';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from './build';
|
||||
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build';
|
||||
import { ensureProjectConfiguredAsync } from './configure';
|
||||
import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
@@ -116,6 +123,7 @@ function resolveResourceClass(
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
@@ -169,6 +177,7 @@ export async function runBuildAndSubmitAsync(
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
@@ -239,6 +248,9 @@ export async function runBuildAndSubmitAsync(
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
|
||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
|
||||
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
@@ -271,6 +283,7 @@ async function prepareAndStartBuildAsync({
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
projectDir: string;
|
||||
@@ -281,6 +294,7 @@ async function prepareAndStartBuildAsync({
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
@@ -297,6 +311,7 @@ async function prepareAndStartBuildAsync({
|
||||
message: flags.message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
|
||||
@@ -378,6 +393,7 @@ async function prepareAndStartSubmissionAsync({
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
actor: buildCtx.user,
|
||||
graphqlClient: buildCtx.graphqlClient,
|
||||
analytics: buildCtx.analytics,
|
||||
projectId: buildCtx.projectId,
|
||||
exp: buildCtx.exp,
|
||||
});
|
||||
@@ -402,3 +418,35 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
||||
assert(build.artifacts?.applicationArchiveUrl);
|
||||
const buildPath = await downloadAndMaybeExtractAppAsync(
|
||||
build.artifacts.applicationArchiveUrl,
|
||||
build.platform
|
||||
);
|
||||
await runAsync(buildPath, build.platform);
|
||||
}
|
||||
|
||||
async function maybeDownloadAndRunSimulatorBuildsAsync(
|
||||
builds: MaybeBuildFragment[],
|
||||
flags: BuildFlags
|
||||
): Promise<void> {
|
||||
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
|
||||
|
||||
if (simBuilds.length > 0 && !flags.autoSubmit && !flags.nonInteractive) {
|
||||
for (const simBuild of simBuilds) {
|
||||
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
|
||||
Log.newLine();
|
||||
const confirm = await confirmAsync({
|
||||
message: `Install and run the ${
|
||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
||||
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
|
||||
});
|
||||
if (confirm) {
|
||||
await downloadAndRunAsync(simBuild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
@@ -12,20 +11,20 @@ import {
|
||||
EasBuildDeprecationInfo,
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build details: ${chalk.underline(logsUrl)}`
|
||||
} build details: ${link(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -79,13 +78,13 @@ function printBuildResult(build: BuildFragment): void {
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} devices (or scan the QR code) to install the app:`
|
||||
);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
Log.log(`${link(logsUrl)}`);
|
||||
} else {
|
||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
||||
// we should account for this case better if it is possible
|
||||
const url = build.artifacts?.buildUrl ?? '';
|
||||
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
|
||||
Log.log(`${chalk.underline(url)}`);
|
||||
Log.log(`${link(url)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Errors } from '@oclif/core';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { isPNGAsync } from '../utils/image';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { CommonContext } from './context';
|
||||
|
||||
@@ -50,3 +53,137 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
function isInsideDirectory(file: string, directory: string): boolean {
|
||||
return file.startsWith(directory);
|
||||
}
|
||||
|
||||
export async function validatePNGsForManagedProjectAsync<T extends Platform>(
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow !== Workflow.MANAGED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't run PNG checks on SDK 47 and newer
|
||||
// see https://github.com/expo/eas-cli/pull/1477#issuecomment-1293914917
|
||||
if (!ctx.exp.sdkVersion || semver.satisfies(ctx.exp.sdkVersion, '>= 47.0.0')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
await validateAndroidPNGsAsync(ctx as CommonContext<Platform.ANDROID>);
|
||||
}
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
//
|
||||
// else {
|
||||
// await validateIosPNGsAsync(ctx as CommonContext<Platform.IOS>);
|
||||
// }
|
||||
}
|
||||
|
||||
type ConfigPng = { configPath: string; pngPath: string | undefined };
|
||||
|
||||
async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): Promise<void> {
|
||||
const pngs: ConfigPng[] = [
|
||||
{
|
||||
configPath: 'exp.icon',
|
||||
pngPath: ctx.exp.icon,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.icon',
|
||||
pngPath: ctx.exp.android?.icon,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.adaptiveIcon.foregroundImage',
|
||||
pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.adaptiveIcon.backgroundImage',
|
||||
pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.splash.image',
|
||||
pngPath: ctx.exp.splash?.image,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.notification.icon',
|
||||
pngPath: ctx.exp.notification?.icon,
|
||||
},
|
||||
];
|
||||
await validatePNGsAsync(pngs);
|
||||
}
|
||||
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
//
|
||||
// async function validateIosPNGsAsync(ctx: CommonContext<Platform.IOS>): Promise<void> {
|
||||
// const pngs: ConfigPng[] = [
|
||||
// {
|
||||
// configPath: 'exp.icon',
|
||||
// pngPath: ctx.exp.icon,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.ios.icon',
|
||||
// pngPath: ctx.exp.ios?.icon,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.splash.image',
|
||||
// pngPath: ctx.exp.splash?.image,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.notification.icon',
|
||||
// pngPath: ctx.exp.notification?.icon,
|
||||
// },
|
||||
// ];
|
||||
// await validatePNGsAsync(pngs);
|
||||
|
||||
// const icon = ctx.exp.ios?.icon ?? ctx.exp.icon;
|
||||
// if (!icon) {
|
||||
// return;
|
||||
// }
|
||||
// const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`;
|
||||
|
||||
// try {
|
||||
// await ensurePNGIsNotTransparentAsync(icon);
|
||||
// } catch (err: any) {
|
||||
// if (err instanceof ImageTransparencyError) {
|
||||
// Log.error(
|
||||
// `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.`
|
||||
// );
|
||||
// Log.error(learnMore('https://expo.fyi/remove-alpha-channel', { dim: false }));
|
||||
// Errors.exit(1);
|
||||
// } else {
|
||||
// throw err;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
async function validatePNGsAsync(configPngs: ConfigPng[]): Promise<void> {
|
||||
const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng));
|
||||
const validationResults = await Promise.allSettled(validationPromises);
|
||||
const failedValidations = validationResults.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected'
|
||||
);
|
||||
|
||||
if (failedValidations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.error('PNG images validation failed:');
|
||||
for (const { reason } of failedValidations) {
|
||||
const error: Error = reason;
|
||||
Log.error(`- ${error.message}`);
|
||||
}
|
||||
Errors.exit(1);
|
||||
}
|
||||
|
||||
async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise<void> {
|
||||
if (!pngPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pngPath.endsWith('.png')) {
|
||||
throw new Error(`"${configPath}" is not a PNG file`);
|
||||
}
|
||||
|
||||
if (!(await isPNGAsync(pngPath))) {
|
||||
throw new Error(`"${configPath}" is not valid PNG`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class ChannelNotFoundError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Channel not found.');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
ViewBranchesOnUpdateChannelQueryVariables,
|
||||
ViewUpdateChannelsOnAppQueryVariables,
|
||||
} from '../graphql/generated';
|
||||
@@ -42,7 +46,7 @@ export async function selectChannelOnAppAsync(
|
||||
queryChannelsOnAppAsync(graphqlClient, { appId: projectId, limit, offset }),
|
||||
promptOptions: {
|
||||
title: selectionPromptTitle,
|
||||
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
|
||||
makePartialChoiceObject: updateChannel => ({ title: updateChannel.name }),
|
||||
getIdentifierForQueryItem: updateChannel => updateChannel.id,
|
||||
},
|
||||
});
|
||||
@@ -153,14 +157,13 @@ function renderPageOfChannels(
|
||||
printJsonOnlyOutput({ currentPage });
|
||||
} else {
|
||||
for (const channel of currentPage) {
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
|
||||
if (currentPage.indexOf(channel) < currentPage.length - 1) {
|
||||
Log.log(`\n${chalk.dim('———')}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,13 +177,8 @@ function renderPageOfBranchesOnChannel(
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
|
||||
} else {
|
||||
// The channel details contain both the branch and latest update group
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channelWithNewBranches);
|
||||
}
|
||||
}
|
||||
@@ -203,3 +201,64 @@ function renderChannelHeaderContent({
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
}
|
||||
|
||||
export async function createChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}
|
||||
): Promise<CreateUpdateChannelOnAppMutation> {
|
||||
// Point the new channel at a branch with its same name.
|
||||
const branchMapping = JSON.stringify({
|
||||
data: [{ branchId, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
});
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name: channelName,
|
||||
branchMapping,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureChannelExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, branchId, channelName }: { appId: string; branchId: string; channelName: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
await createChannelOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import CliTable from 'cli-table3';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import Log from '../log';
|
||||
import { UPDATE_COLUMNS, getUpdateGroupDescriptionsWithBranch } from '../update/utils';
|
||||
import {
|
||||
FormattedBranchDescription,
|
||||
formatBranch,
|
||||
getUpdateGroupDescriptionsWithBranch,
|
||||
} from '../update/utils';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -75,38 +79,39 @@ export function getBranchMapping(branchMappingString?: string): {
|
||||
|
||||
export function logChannelDetails(channel: UpdateChannelObject): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const updateChannelsTable = new CliTable({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
if (branchMapping.data.length > 2) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
|
||||
|
||||
for (const currentBranch of channel.updateBranches) {
|
||||
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(
|
||||
currentBranch.updateGroups
|
||||
const branchDescription = channel.updateBranches.flatMap(branch => {
|
||||
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
|
||||
branch.updateGroups
|
||||
);
|
||||
|
||||
const isRolloutBranch = isRollout && rolloutBranchIds.includes(currentBranch.id);
|
||||
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === currentBranch.id;
|
||||
const isRolloutBranch = isRollout && rolloutBranchIds.includes(branch.id);
|
||||
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === branch.id;
|
||||
let rolloutPercentNumber: number | undefined = undefined;
|
||||
if (isRolloutBranch) {
|
||||
rolloutPercentNumber = isBaseBranch ? rolloutPercent! * 100 : (1 - rolloutPercent!) * 100;
|
||||
}
|
||||
|
||||
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
|
||||
updateChannelsTable.push([
|
||||
return updateGroupWithBranchDescriptions.map(
|
||||
({ branch, ...updateGroup }): FormattedBranchDescription => ({
|
||||
branch,
|
||||
...(isRolloutBranch
|
||||
? [isBaseBranch ? `${rolloutPercent! * 100}%` : `${(1 - rolloutPercent!) * 100}%`]
|
||||
: []),
|
||||
message,
|
||||
runtimeVersion,
|
||||
group,
|
||||
platforms,
|
||||
]);
|
||||
});
|
||||
branchRolloutPercentage: rolloutPercentNumber,
|
||||
update: updateGroup,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (branchDescription.length === 0) {
|
||||
Log.log(chalk.dim('No branches are pointed to this channel.'));
|
||||
} else {
|
||||
Log.log(
|
||||
branchDescription
|
||||
.map(description => formatBranch(description))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
Log.log(updateChannelsTable.toString());
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Command } from '@oclif/core';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
AnalyticsWithOrchestration,
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import AnalyticsContextField from './context/AnalyticsContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField';
|
||||
import LoggedInContextField from './context/LoggedInContextField';
|
||||
@@ -76,10 +77,19 @@ export default abstract class EasCommand extends Command {
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server. Returns the project config in the context.
|
||||
* This also requires the user to be logged in (getProjectIdAsync requires logged in), so also expose that context.
|
||||
* Exposing the loggedIn context here helps us guarantee user identification for logging purposes.
|
||||
*/
|
||||
ProjectConfig: {
|
||||
loggedIn: new LoggedInContextField(),
|
||||
projectConfig: new ProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Analytics manager. Returns the analytics manager in the context for use by the command.
|
||||
*/
|
||||
Analytics: {
|
||||
analytics: new AnalyticsContextField(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -106,37 +116,52 @@ export default abstract class EasCommand extends Command {
|
||||
): Promise<ContextOutput<C>> {
|
||||
const contextDefinition = commandClass.contextDefinition;
|
||||
|
||||
const contextValuePairs = await Promise.all(
|
||||
Object.keys(contextDefinition).map(async contextKey => {
|
||||
return [
|
||||
contextKey,
|
||||
await contextDefinition[contextKey].getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager: this.sessionManager,
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
// do these serially so that they don't do things like ask for login twice in parallel
|
||||
const contextValuePairs = [];
|
||||
for (const [contextKey, contextField] of Object.entries(contextDefinition)) {
|
||||
contextValuePairs.push([
|
||||
contextKey,
|
||||
await contextField.getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager: this.sessionManager,
|
||||
analytics: this.analytics,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return Object.fromEntries(contextValuePairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user session manager. Responsible for coordinating all user session related state.
|
||||
* If needed in a subclass, SessionManager ContextOption.
|
||||
* If needed in a subclass, use the SessionManager ContextOption.
|
||||
*/
|
||||
private readonly sessionManager = new SessionManager();
|
||||
private sessionManagerInternal?: SessionManager;
|
||||
private get sessionManager(): SessionManager {
|
||||
return nullthrows(this.sessionManagerInternal);
|
||||
}
|
||||
|
||||
/**
|
||||
* The analytics manager. Used for logging analytics.
|
||||
* It is set up here to ensure a consistent set up.
|
||||
*/
|
||||
private analyticsInternal?: AnalyticsWithOrchestration;
|
||||
private get analytics(): AnalyticsWithOrchestration {
|
||||
return nullthrows(this.analyticsInternal);
|
||||
}
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
await initAnalyticsAsync();
|
||||
this.analyticsInternal = await createAnalyticsAsync();
|
||||
this.sessionManagerInternal = new SessionManager(this.analytics);
|
||||
|
||||
// this is needed for logEvent call below as it identifies the user in the analytics system
|
||||
// if possible
|
||||
await this.sessionManager.getUserAsync();
|
||||
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
this.analytics.logEvent(CommandEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
@@ -147,7 +172,7 @@ export default abstract class EasCommand extends Command {
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
override async finally(err: Error): Promise<any> {
|
||||
await flushAnalyticsAsync();
|
||||
await this.analytics.flushAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
|
||||
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/SessionManager');
|
||||
jest.mock('../../analytics/rudderstackClient', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
|
||||
jest.mock('../../analytics/AnalyticsManager', () => {
|
||||
const { CommandEvent } = jest.requireActual('../../analytics/AnalyticsManager');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
CommandEvent,
|
||||
createAnalyticsAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -25,10 +23,16 @@ afterAll(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
});
|
||||
|
||||
const analytics: AnalyticsWithOrchestration = {
|
||||
logEvent: jest.fn((): void => {}),
|
||||
setActor: jest.fn((): void => {}),
|
||||
flushAsync: jest.fn(async (): Promise<void> => {}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(initAsync).mockReset();
|
||||
jest.mocked(flushAsync).mockReset();
|
||||
jest.mocked(logEvent).mockReset();
|
||||
jest.resetAllMocks();
|
||||
|
||||
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
|
||||
});
|
||||
|
||||
const createTestEasCommand = (): typeof EasCommand => {
|
||||
@@ -55,27 +59,27 @@ describe(EasCommand.name, () => {
|
||||
|
||||
const sessionManagerSpy = jest.spyOn(SessionManager.prototype, 'getUserAsync');
|
||||
expect(sessionManagerSpy).toBeCalledTimes(1);
|
||||
}, 15_000);
|
||||
}, 30_000);
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
expect(createAnalyticsAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
expect(analytics.logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
@@ -90,7 +94,7 @@ describe(EasCommand.name, () => {
|
||||
});
|
||||
} catch {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
|
||||
export default class AnalyticsContextField extends ContextField<Analytics> {
|
||||
async getValueAsync({ analytics }: ContextOptions): Promise<Analytics> {
|
||||
return analytics;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
|
||||
export interface ContextOptions {
|
||||
sessionManager: SessionManager;
|
||||
analytics: Analytics;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
|
||||
export default class LoggedInContextField extends ContextField<{
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
return { actor, graphqlClient };
|
||||
|
||||
return {
|
||||
actor,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
|
||||
export default class MaybeLoggedInContextField extends ContextField<{
|
||||
actor: Actor | null;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
|
||||
actor: Actor | null;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
const authenticationInfo = {
|
||||
@@ -17,7 +21,12 @@ export default class MaybeLoggedInContextField extends ContextField<{
|
||||
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const actor = await sessionManager.getUserAsync();
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
return { actor: actor ?? null, graphqlClient };
|
||||
return {
|
||||
actor: actor ?? null,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
};
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -51,6 +51,7 @@ describe(getProjectIdAsync, () => {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
},
|
||||
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
|
||||
});
|
||||
@@ -157,9 +158,13 @@ describe(getProjectIdAsync, () => {
|
||||
expect(projectId).toEqual('2345');
|
||||
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
});
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith(
|
||||
'/app',
|
||||
{
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if writing the ID back to the config fails', async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AnyVariables,
|
||||
Client,
|
||||
OperationContext,
|
||||
OperationResult,
|
||||
@@ -17,9 +18,9 @@ import { getExpoApiBaseUrl } from '../../../api';
|
||||
import { httpsProxyAgent } from '../../../fetch';
|
||||
|
||||
export interface ExpoGraphqlClient extends Client {
|
||||
query<Data = any, Variables extends object = object>(
|
||||
query<Data = any, Variables extends AnyVariables = AnyVariables>(
|
||||
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
|
||||
variables: Variables | undefined,
|
||||
variables: Variables,
|
||||
context: Partial<OperationContext> & { additionalTypenames: string[] }
|
||||
): PromisifiedSource<OperationResult<Data, Variables>>;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,13 @@ export async function saveProjectIdToAppConfigAsync(
|
||||
options: { env?: Env } = {}
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir, options);
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
});
|
||||
const result = await modifyConfigAsync(
|
||||
projectDir,
|
||||
{
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import env from '../../env';
|
||||
import { FeatureGateKey } from './FeatureGateKey';
|
||||
|
||||
function parseFeatureGateEnvVariableValue(value: string | undefined): string[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',').map(v => v.trim());
|
||||
}
|
||||
|
||||
export default class FeatureGateEnvOverrides {
|
||||
private readonly map = new Map<string, boolean>();
|
||||
|
||||
constructor() {
|
||||
const overrideEnableGateKeys = new Set(parseFeatureGateEnvVariableValue(env.featureGateEnable));
|
||||
const overrideDisableGateKeys = new Set(
|
||||
parseFeatureGateEnvVariableValue(env.featureGateDisable)
|
||||
);
|
||||
|
||||
for (const overrideEnableKey of overrideEnableGateKeys) {
|
||||
if (overrideDisableGateKeys.has(overrideEnableKey)) {
|
||||
continue;
|
||||
}
|
||||
this.map.set(overrideEnableKey, true);
|
||||
}
|
||||
for (const overrideDisableGateKey of overrideDisableGateKeys) {
|
||||
this.map.set(overrideDisableGateKey, false);
|
||||
}
|
||||
}
|
||||
|
||||
public isOverridden(key: FeatureGateKey): boolean {
|
||||
return this.map.has(key) ?? false;
|
||||
}
|
||||
|
||||
public getOverride(key: FeatureGateKey): boolean {
|
||||
return nullthrows(this.map.get(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum FeatureGateKey {
|
||||
// for tests
|
||||
TEST = 'test',
|
||||
}
|
||||
|
||||
export const featureGateDefaultValueWhenNoServerValue: Record<FeatureGateKey, boolean> = {
|
||||
[FeatureGateKey.TEST]: true,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FeatureGateKey } from './FeatureGateKey';
|
||||
|
||||
export const setOverride = (_key: FeatureGateKey, _enabled: boolean): void => {
|
||||
throw new Error('Must use mocked FeatureGateTestOverrides');
|
||||
};
|
||||
|
||||
export const removeOverride = (_key: FeatureGateKey): void => {
|
||||
throw new Error('Must use mocked FeatureGateTestOverrides');
|
||||
};
|
||||
|
||||
export const isOverridden = (_key: FeatureGateKey): boolean => false;
|
||||
|
||||
export const getOverride = (_key: FeatureGateKey): boolean => {
|
||||
throw new Error('Must use mocked FeatureGateTestOverrides');
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import FeatureGateEnvOverrides from './FeatureGateEnvOverrides';
|
||||
import { FeatureGateKey, featureGateDefaultValueWhenNoServerValue } from './FeatureGateKey';
|
||||
import * as FeatureGateTestOverrides from './FeatureGateTestOverrides';
|
||||
|
||||
export default class FeatureGating {
|
||||
constructor(
|
||||
private readonly serverValues: { [key: string]: boolean },
|
||||
private readonly envOverrides: FeatureGateEnvOverrides
|
||||
) {}
|
||||
|
||||
public isEnabled(experimentKey: FeatureGateKey): boolean {
|
||||
if (process.env.NODE_ENV === 'test' && FeatureGateTestOverrides.isOverridden(experimentKey)) {
|
||||
return FeatureGateTestOverrides.getOverride(experimentKey);
|
||||
}
|
||||
|
||||
if (this.envOverrides.isOverridden(experimentKey)) {
|
||||
return this.envOverrides.getOverride(experimentKey);
|
||||
}
|
||||
|
||||
if (experimentKey in this.serverValues) {
|
||||
return this.serverValues[experimentKey];
|
||||
}
|
||||
|
||||
return featureGateDefaultValueWhenNoServerValue[experimentKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test gate override methods
|
||||
*/
|
||||
|
||||
public static overrideKeyForScope(
|
||||
key: FeatureGateKey,
|
||||
enabled: boolean,
|
||||
scope: () => void
|
||||
): void {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error(`Cannot overrideKeyForScope outside of test environment`);
|
||||
}
|
||||
|
||||
FeatureGateTestOverrides.setOverride(key, enabled);
|
||||
try {
|
||||
scope();
|
||||
} finally {
|
||||
FeatureGateTestOverrides.removeOverride(key);
|
||||
}
|
||||
}
|
||||
|
||||
public static async overrideKeyForScopeAsync(
|
||||
key: FeatureGateKey,
|
||||
enabled: boolean,
|
||||
scope: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error(`Cannot overrideKeyForScopeAsync outside of test environment`);
|
||||
}
|
||||
|
||||
FeatureGateTestOverrides.setOverride(key, enabled);
|
||||
try {
|
||||
await scope();
|
||||
} finally {
|
||||
FeatureGateTestOverrides.removeOverride(key);
|
||||
}
|
||||
}
|
||||
|
||||
public static overrideKeyForEachInTest(key: FeatureGateKey, enabled: boolean): void {
|
||||
beforeEach(() => FeatureGateTestOverrides.setOverride(key, enabled));
|
||||
afterEach(() => FeatureGateTestOverrides.removeOverride(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FeatureGateKey } from '../FeatureGateKey';
|
||||
|
||||
const map = new Map();
|
||||
|
||||
export const setOverride = (key: FeatureGateKey, enabled: boolean): void => {
|
||||
map.set(key, enabled);
|
||||
};
|
||||
|
||||
export const removeOverride = (key: FeatureGateKey): void => {
|
||||
map.delete(key);
|
||||
};
|
||||
|
||||
export const isOverridden = (key: FeatureGateKey): boolean => map.has(key);
|
||||
|
||||
export const getOverride = (key: FeatureGateKey): boolean => {
|
||||
if (map.has(key)) {
|
||||
return map.get(key);
|
||||
}
|
||||
throw new Error(`Key ${key} not overridden`);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import env from '../../../env';
|
||||
import FeatureGateEnvOverrides from '../FeatureGateEnvOverrides';
|
||||
import { FeatureGateKey } from '../FeatureGateKey';
|
||||
import FeatureGating from '../FeatureGating';
|
||||
|
||||
jest.mock('../FeatureGateTestOverrides');
|
||||
|
||||
describe(FeatureGating, () => {
|
||||
it('supports scoped overriding', () => {
|
||||
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
|
||||
FeatureGating.overrideKeyForScope(FeatureGateKey.TEST, false, () => {
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
it('supports async scoped overriding', async () => {
|
||||
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
|
||||
await FeatureGating.overrideKeyForScopeAsync(FeatureGateKey.TEST, false, async () => {
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
it('uses value from the server, falls back to defined value', () => {
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
|
||||
const featureGating2 = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: false },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating2.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
|
||||
const featureGating3 = new FeatureGating({}, new FeatureGateEnvOverrides());
|
||||
expect(featureGating3.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
describe('environment variable overriding', () => {
|
||||
afterAll(() => {
|
||||
env.featureGateEnable = undefined;
|
||||
env.featureGateDisable = undefined;
|
||||
});
|
||||
|
||||
it('supports env overrides affirmative', () => {
|
||||
env.featureGateEnable = [FeatureGateKey.TEST].join(',');
|
||||
env.featureGateDisable = [].join(',');
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: false },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
it('supports env overrides negative', () => {
|
||||
env.featureGateEnable = [].join(',');
|
||||
env.featureGateDisable = [FeatureGateKey.TEST].join(',');
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('jest scoped overriding', () => {
|
||||
FeatureGating.overrideKeyForEachInTest(FeatureGateKey.TEST, false);
|
||||
|
||||
it('supports jest scoped overriding', () => {
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removal of jest scoped overriding', () => {
|
||||
it('jest scoped overriding is only for describe scope above', () => {
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { OptionFlag } from '@oclif/core/lib/interfaces';
|
||||
|
||||
import { EasNonInteractiveAndJsonFlags } from './flags';
|
||||
|
||||
@@ -37,7 +38,7 @@ export const getLimitFlagWithCustomValues = ({
|
||||
}: {
|
||||
defaultTo: number;
|
||||
limit: number;
|
||||
}): any =>
|
||||
}): OptionFlag<number | undefined> =>
|
||||
Flags.integer({
|
||||
description: `The number of items to fetch each query. Defaults to ${defaultTo} and is capped at ${limit}.`,
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getAnalyticsEnabledAsync, setAnalyticsEnabledAsync } from '../analytics/AnalyticsManager';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
|
||||
export default class AnalyticsView extends EasCommand {
|
||||
static override description = 'display or change analytics settings';
|
||||
@@ -10,10 +10,10 @@ export default class AnalyticsView extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
|
||||
if (status) {
|
||||
await UserSettings.setAsync('analyticsEnabled', status === 'on');
|
||||
setAnalyticsEnabledAsync(status === 'on');
|
||||
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
|
||||
} else {
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', null);
|
||||
const analyticsEnabled = await getAnalyticsEnabledAsync();
|
||||
Log.log(
|
||||
`Analytics are ${
|
||||
analyticsEnabled === false ? 'disabled' : 'enabled'
|
||||
|
||||
@@ -1,52 +1,14 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
gql`
|
||||
mutation createUpdateBranchForApp($appId: ID!, $name: String!) {
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
||||
if (!newBranch) {
|
||||
throw new Error(`Could not create branch ${name}.`);
|
||||
}
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
export default class BranchCreate extends EasCommand {
|
||||
static override description = 'create a branch';
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export default class BranchDelete extends EasCommand {
|
||||
}
|
||||
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
|
||||
promptTitle: 'Which branch would you like to delete?',
|
||||
paginatedQueryOptions,
|
||||
}));
|
||||
|
||||
@@ -57,7 +57,9 @@ export default class BranchView extends EasCommand {
|
||||
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: 'Which branch would you like to view?',
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: updateBranch.name,
|
||||
}),
|
||||
// discard limit and offset because this query is not their intended target
|
||||
paginatedQueryOptions: {
|
||||
json: paginatedQueryOptions.json,
|
||||
|
||||
@@ -100,6 +100,7 @@ export default class Build extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -114,6 +115,7 @@ export default class Build extends EasCommand {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
} = await this.getContextAsync(Build, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
@@ -133,6 +135,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectDir,
|
||||
flagsWithPlatform,
|
||||
actor,
|
||||
@@ -242,7 +245,7 @@ export default class Build extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeprecatedEasJsonAsync(
|
||||
export async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
|
||||
@@ -65,6 +65,7 @@ export default class BuildInspect extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -73,6 +74,7 @@ export default class BuildInspect extends EasCommand {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
} = await this.getContextAsync(BuildInspect, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -97,6 +99,7 @@ export default class BuildInspect extends EasCommand {
|
||||
try {
|
||||
await runBuildAndSubmitAsync(
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectDir,
|
||||
{
|
||||
nonInteractive: false,
|
||||
|
||||
@@ -9,13 +9,10 @@ import {
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import {
|
||||
AppPlatform,
|
||||
DistributionType,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildList extends EasCommand {
|
||||
@@ -82,7 +79,8 @@ export default class BuildList extends EasCommand {
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
|
||||
const graphqlBuildDistribution =
|
||||
buildDistributionTypeToGraphQLDistributionType(buildDistribution);
|
||||
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
await listAndRenderBuildsOnAppAsync(graphqlClient, {
|
||||
@@ -133,17 +131,3 @@ const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | u
|
||||
return GraphQLBuildStatus.Canceled;
|
||||
}
|
||||
};
|
||||
|
||||
const toGraphQLBuildDistribution = (
|
||||
buildDistribution?: BuildDistributionType
|
||||
): DistributionType | undefined => {
|
||||
if (buildDistribution === BuildDistributionType.STORE) {
|
||||
return DistributionType.Store;
|
||||
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
|
||||
return DistributionType.Internal;
|
||||
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { handleDeprecatedEasJsonAsync } from '.';
|
||||
import { waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
|
||||
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
|
||||
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags } from '../../commandUtils/pagination';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
BuildFragment,
|
||||
BuildMode,
|
||||
BuildStatus,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
IosJobOverridesInput,
|
||||
ProjectArchiveSourceType,
|
||||
StatuspageServiceName,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildMutation } from '../../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames, selectPlatformAsync } from '../../platform';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
|
||||
interface BuildResignFlags {
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
maybeBuildId?: string;
|
||||
wait: boolean;
|
||||
}
|
||||
interface RawBuildResignFlags {
|
||||
json: boolean;
|
||||
'non-interactive': boolean;
|
||||
offset: number | undefined;
|
||||
limit: number | undefined;
|
||||
platform: 'android' | 'ios' | undefined;
|
||||
profile: string | undefined;
|
||||
wait: boolean;
|
||||
id: string | undefined;
|
||||
}
|
||||
|
||||
export default class BuildResign extends EasCommand {
|
||||
static override description = 're-sign a build archive';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
wait: Flags.boolean({
|
||||
default: true,
|
||||
allowNo: true,
|
||||
description: 'Wait for build(s) to complete.',
|
||||
}),
|
||||
id: Flags.string({
|
||||
description: 'ID of the build to re-sign.',
|
||||
}),
|
||||
...EasPaginatedQueryFlags,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(BuildResign);
|
||||
if (rawFlags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const flags = this.sanitizeFlags(rawFlags as RawBuildResignFlags);
|
||||
const { limit, offset, nonInteractive } = flags;
|
||||
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
} = await this.getContextAsync(BuildResign, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
const maybeBuild = flags.maybeBuildId
|
||||
? await this.maybeGetBuildAsync(graphqlClient, flags.maybeBuildId)
|
||||
: undefined;
|
||||
const platform =
|
||||
(maybeBuild?.platform.toLowerCase() as Platform | undefined) ??
|
||||
(await selectPlatformAsync(flags.platform));
|
||||
if (platform === Platform.ANDROID) {
|
||||
throw new Error('Re-signing archives is not supported on Android yet.');
|
||||
}
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
easJsonAccessor,
|
||||
platform,
|
||||
flags.profile ?? 'production'
|
||||
);
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
const build = await this.ensureBuildSelectedAsync(
|
||||
{ graphqlClient, projectId, platform, nonInteractive, limit, offset },
|
||||
maybeBuild
|
||||
);
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
});
|
||||
if (buildProfile.credentialsSource !== CredentialsSource.LOCAL && !nonInteractive) {
|
||||
await credentialsCtx.appStore.ensureAuthenticatedAsync();
|
||||
}
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir,
|
||||
nonInteractive,
|
||||
exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
xcodeBuildContext,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
|
||||
credentialsCtx,
|
||||
targets,
|
||||
buildProfile
|
||||
);
|
||||
assert(build.artifacts?.applicationArchiveUrl, 'Missing application archive.');
|
||||
const jobOverrides: IosJobOverridesInput = {
|
||||
mode: BuildMode.Resign,
|
||||
type: BuildWorkflow.Unknown,
|
||||
resign: {
|
||||
applicationArchiveSource: {
|
||||
type: ProjectArchiveSourceType.Url,
|
||||
url: build.artifacts?.applicationArchiveUrl,
|
||||
},
|
||||
},
|
||||
secrets: prepareCredentialsToResign(credentialsResult.credentials),
|
||||
builderEnvironment: { image: 'default' },
|
||||
};
|
||||
const newBuild = await BuildMutation.retryIosBuildAsync(graphqlClient, {
|
||||
buildId: build.id,
|
||||
jobOverrides,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
printLogsUrls([newBuild]);
|
||||
Log.newLine();
|
||||
if (!flags.wait) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(newBuild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const buildResult = await waitForBuildEndAsync(graphqlClient, {
|
||||
buildIds: [newBuild.id],
|
||||
accountName: account.name,
|
||||
});
|
||||
if (!flags.json) {
|
||||
printBuildResults(buildResult);
|
||||
} else {
|
||||
assert(buildResult[0], 'missing build results');
|
||||
printJsonOnlyOutput(buildResult[0]);
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeFlags(flags: RawBuildResignFlags): BuildResignFlags {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (nonInteractive && !flags.id) {
|
||||
throw new Error(
|
||||
`${chalk.bold('--id')} is required when running with ${chalk.bold(
|
||||
'--non-interactive'
|
||||
)} flag.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
json: flags.json,
|
||||
nonInteractive,
|
||||
offset: flags.offset,
|
||||
limit: flags.limit,
|
||||
platform: flags.platform as Platform | undefined,
|
||||
profile: flags.profile,
|
||||
maybeBuildId: flags.id,
|
||||
wait: flags.wait,
|
||||
};
|
||||
}
|
||||
|
||||
async ensureBuildSelectedAsync(
|
||||
{
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform,
|
||||
nonInteractive,
|
||||
limit,
|
||||
offset,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
platform: Platform;
|
||||
nonInteractive: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
},
|
||||
maybeBuild?: BuildFragment
|
||||
): Promise<BuildFragment> {
|
||||
if (maybeBuild) {
|
||||
return maybeBuild;
|
||||
}
|
||||
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
title: 'Which build would you like to re-sign with new credentials?',
|
||||
paginatedQueryOptions: {
|
||||
limit,
|
||||
offset: offset ?? 0,
|
||||
nonInteractive,
|
||||
json: false,
|
||||
},
|
||||
filter: {
|
||||
distribution: DistributionType.Internal,
|
||||
platform: toAppPlatform(platform),
|
||||
status: BuildStatus.Finished,
|
||||
},
|
||||
});
|
||||
if (!build) {
|
||||
throw new Error('There are no builds that can be re-signed on this project.');
|
||||
}
|
||||
return build;
|
||||
}
|
||||
|
||||
async maybeGetBuildAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
maybeBuildId?: string,
|
||||
maybePlatform?: Platform
|
||||
): Promise<BuildFragment | undefined> {
|
||||
if (maybeBuildId) {
|
||||
const build = await BuildQuery.byIdAsync(graphqlClient, maybeBuildId);
|
||||
if (build.distribution !== DistributionType.Internal) {
|
||||
throw new Error('This is not an internal distribution build.');
|
||||
}
|
||||
if (build.status !== BuildStatus.Finished) {
|
||||
throw new Error('Only builds that finished successfully can be re-signed.');
|
||||
}
|
||||
if (maybePlatform && build.platform !== toAppPlatform(maybePlatform)) {
|
||||
throw new Error(
|
||||
`Build with ID ${maybeBuildId} was not created for platform ${requestedPlatformDisplayNames[maybePlatform]}.`
|
||||
);
|
||||
}
|
||||
return build;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { getLatestBuildAsync, listAndSelectBuildOnAppAsync } from '../../build/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
PaginatedQueryOptions,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { RunArchiveFlags, runAsync } from '../../run/run';
|
||||
import { isRunnableOnSimulatorOrEmulator } from '../../run/utils';
|
||||
import {
|
||||
downloadAndMaybeExtractAppAsync,
|
||||
extractAppFromLocalArchiveAsync,
|
||||
} from '../../utils/download';
|
||||
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
|
||||
|
||||
interface RawRunFlags {
|
||||
latest?: boolean;
|
||||
id?: string;
|
||||
url?: string;
|
||||
path?: string;
|
||||
platform?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
interface RunCommandFlags {
|
||||
selectedPlatform: AppPlatform;
|
||||
runArchiveFlags: RunArchiveFlags;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export default class Run extends EasCommand {
|
||||
static override description = 'run simulator/emulator builds from eas-cli';
|
||||
|
||||
static override flags = {
|
||||
latest: Flags.boolean({
|
||||
description: 'Run the latest simulator/emulator build for specified platform',
|
||||
exclusive: ['id', 'path', 'url'],
|
||||
}),
|
||||
url: Flags.string({
|
||||
description: 'Simulator/Emulator build archive url',
|
||||
exclusive: ['latest', 'id', 'path'],
|
||||
}),
|
||||
path: Flags.string({
|
||||
description: 'Path to the simulator/emulator build archive or app',
|
||||
exclusive: ['latest', 'id', 'url'],
|
||||
}),
|
||||
id: Flags.string({
|
||||
description: 'ID of the simulator/emulator build to run',
|
||||
exclusive: ['latest, path, url'],
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
...EasPaginatedQueryFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(Run);
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const queryOptions = getPaginatedQueryOptions(flags);
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
projectConfig: { projectId },
|
||||
} = await this.getContextAsync(Run, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const simulatorBuildPath = await getPathToSimulatorBuildAppAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
flags,
|
||||
queryOptions
|
||||
);
|
||||
|
||||
await runAsync(simulatorBuildPath, flags.selectedPlatform);
|
||||
}
|
||||
|
||||
private async sanitizeFlagsAsync(flags: RawRunFlags): Promise<RunCommandFlags> {
|
||||
const { platform, limit, offset, ...runArchiveFlags } = flags;
|
||||
|
||||
const selectedPlatform = await resolvePlatformAsync(platform);
|
||||
|
||||
if (platform === 'ios' && process.platform !== 'darwin') {
|
||||
Errors.error('You can only use an iOS simulator to run apps on macOS devices', {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
runArchiveFlags.path &&
|
||||
!(
|
||||
(runArchiveFlags.path.endsWith('.tar.gz') ||
|
||||
runArchiveFlags.path.endsWith('.app') ||
|
||||
runArchiveFlags.path.endsWith('.apk')) &&
|
||||
(await pathExists(runArchiveFlags.path))
|
||||
)
|
||||
) {
|
||||
Errors.error('The path must point to a .tar.gz archive, .apk file, or .app directory', {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
selectedPlatform,
|
||||
runArchiveFlags,
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function resolvePlatformAsync(platform?: string): Promise<AppPlatform> {
|
||||
if (process.platform !== 'darwin') {
|
||||
return AppPlatform.Android;
|
||||
}
|
||||
|
||||
if (platform && Object.values(AppPlatform).includes(platform.toUpperCase() as AppPlatform)) {
|
||||
return platform.toUpperCase() as AppPlatform;
|
||||
}
|
||||
|
||||
const { selectedPlatform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Select platform',
|
||||
name: 'selectedPlatform',
|
||||
choices: [
|
||||
{ title: 'Android', value: AppPlatform.Android },
|
||||
{ title: 'iOS', value: AppPlatform.Ios },
|
||||
],
|
||||
});
|
||||
return selectedPlatform;
|
||||
}
|
||||
|
||||
function validateChosenBuild(
|
||||
maybeBuild: BuildFragment | null,
|
||||
selectedPlatform: AppPlatform
|
||||
): BuildFragment {
|
||||
if (!maybeBuild) {
|
||||
throw new Error('There are no simulator/emulator builds that can be run for this project.');
|
||||
}
|
||||
|
||||
if (selectedPlatform !== maybeBuild.platform) {
|
||||
throw new Error(
|
||||
`The selected build is for ${
|
||||
appPlatformDisplayNames[maybeBuild.platform]
|
||||
}, but you selected ${appPlatformDisplayNames[selectedPlatform]}`
|
||||
);
|
||||
}
|
||||
|
||||
if (maybeBuild.status !== BuildStatus.Finished) {
|
||||
throw new Error('The selected build is not finished.');
|
||||
}
|
||||
|
||||
if (!isRunnableOnSimulatorOrEmulator(maybeBuild)) {
|
||||
throw new Error(
|
||||
'Artifacts for the latest build have expired and are no longer available, or this is not a simulator/emulator build.'
|
||||
);
|
||||
}
|
||||
|
||||
return maybeBuild;
|
||||
}
|
||||
|
||||
async function maybeGetBuildAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
flags: RunCommandFlags,
|
||||
projectId: string,
|
||||
paginatedQueryOptions: PaginatedQueryOptions
|
||||
): Promise<BuildFragment | null> {
|
||||
const distributionType =
|
||||
flags.selectedPlatform === AppPlatform.Ios ? DistributionType.Simulator : undefined;
|
||||
|
||||
if (flags.runArchiveFlags.id) {
|
||||
const build = await BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
|
||||
return validateChosenBuild(build, flags.selectedPlatform);
|
||||
} else if (
|
||||
!flags.runArchiveFlags.id &&
|
||||
!flags.runArchiveFlags.path &&
|
||||
!flags.runArchiveFlags.url &&
|
||||
!flags.runArchiveFlags.latest
|
||||
) {
|
||||
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
title: `Select ${appPlatformDisplayNames[flags.selectedPlatform]} ${
|
||||
flags.selectedPlatform === AppPlatform.Ios ? 'simulator' : 'emulator'
|
||||
} build to run for ${await getDisplayNameForProjectIdAsync(graphqlClient, projectId)} app`,
|
||||
filter: {
|
||||
platform: flags.selectedPlatform,
|
||||
distribution: distributionType,
|
||||
status: BuildStatus.Finished,
|
||||
},
|
||||
paginatedQueryOptions,
|
||||
selectPromptDisabledFunction: build => !isRunnableOnSimulatorOrEmulator(build),
|
||||
selectPromptWarningMessage:
|
||||
'Artifacts for this build have expired and are no longer available, or this is not a simulator/emulator build.',
|
||||
});
|
||||
return validateChosenBuild(build, flags.selectedPlatform);
|
||||
} else if (flags.runArchiveFlags.latest) {
|
||||
const latestBuild = await getLatestBuildAsync(graphqlClient, {
|
||||
projectId,
|
||||
filter: {
|
||||
platform: flags.selectedPlatform,
|
||||
distribution: distributionType,
|
||||
status: BuildStatus.Finished,
|
||||
},
|
||||
});
|
||||
|
||||
return validateChosenBuild(latestBuild, flags.selectedPlatform);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getEasBuildRunCachedAppPath(
|
||||
projectId: string,
|
||||
buildId: string,
|
||||
platform: AppPlatform
|
||||
): string {
|
||||
return path.join(
|
||||
getEasBuildRunCacheDirectoryPath(),
|
||||
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
|
||||
);
|
||||
}
|
||||
|
||||
async function getPathToSimulatorBuildAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
flags: RunCommandFlags,
|
||||
queryOptions: PaginatedQueryOptions
|
||||
): Promise<string> {
|
||||
const maybeBuild = await maybeGetBuildAsync(graphqlClient, flags, projectId, queryOptions);
|
||||
|
||||
if (maybeBuild) {
|
||||
const cachedAppPath = getEasBuildRunCachedAppPath(
|
||||
projectId,
|
||||
maybeBuild.id,
|
||||
flags.selectedPlatform
|
||||
);
|
||||
|
||||
if (await pathExists(cachedAppPath)) {
|
||||
Log.newLine();
|
||||
Log.log(`Using cached app...`);
|
||||
return cachedAppPath;
|
||||
}
|
||||
|
||||
if (!maybeBuild.artifacts?.applicationArchiveUrl) {
|
||||
throw new Error('Build does not have an application archive url');
|
||||
}
|
||||
|
||||
return await downloadAndMaybeExtractAppAsync(
|
||||
maybeBuild.artifacts.applicationArchiveUrl,
|
||||
flags.selectedPlatform,
|
||||
cachedAppPath
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.runArchiveFlags.url) {
|
||||
return await downloadAndMaybeExtractAppAsync(flags.runArchiveFlags.url, flags.selectedPlatform);
|
||||
}
|
||||
|
||||
if (flags.runArchiveFlags.path?.endsWith('.tar.gz')) {
|
||||
return await extractAppFromLocalArchiveAsync(
|
||||
flags.runArchiveFlags.path,
|
||||
flags.selectedPlatform
|
||||
);
|
||||
}
|
||||
|
||||
// this should never fail, due to the validation in sanitizeFlagsAsync
|
||||
assert(flags.runArchiveFlags.path);
|
||||
return flags.runArchiveFlags.path;
|
||||
}
|
||||
@@ -1,63 +1,16 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
|
||||
import { BranchNotFoundError } from '../../branch/utils';
|
||||
import { createChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
|
||||
export async function createUpdateChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
}: {
|
||||
appId: string;
|
||||
channelName: string;
|
||||
branchId: string;
|
||||
}
|
||||
): Promise<CreateUpdateChannelOnAppMutation> {
|
||||
// Point the new channel at a branch with its same name.
|
||||
const branchMapping = JSON.stringify({
|
||||
data: [{ branchId, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
});
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name: channelName,
|
||||
branchMapping,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class ChannelCreate extends EasCommand {
|
||||
static override description = 'create a channel';
|
||||
@@ -132,7 +85,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
|
||||
const {
|
||||
updateChannel: { createUpdateChannelForApp: newChannel },
|
||||
} = await createUpdateChannelOnAppAsync(graphqlClient, {
|
||||
} = await createChannelOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName,
|
||||
branchId,
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
: await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like ${existingChannel.name} to point at?`,
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
|
||||
paginatedQueryOptions: {
|
||||
json,
|
||||
nonInteractive,
|
||||
|
||||
@@ -13,6 +13,8 @@ export default class Credentials extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.OptionalProjectConfig,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -20,6 +22,8 @@ export default class Credentials extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig,
|
||||
getDynamicProjectConfigAsync,
|
||||
analytics,
|
||||
} = await this.getContextAsync(Credentials, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -27,7 +31,9 @@ export default class Credentials extends EasCommand {
|
||||
await new SelectPlatform(
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectConfig ?? null,
|
||||
getDynamicProjectConfigAsync,
|
||||
flags.platform
|
||||
).runAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { loadConfigAsync } from '../../metadata/config/resolve';
|
||||
import { MetadataValidationError, logMetadataValidationError } from '../../metadata/errors';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class MetadataLint extends EasCommand {
|
||||
static override description = 'validate the local store configuration';
|
||||
|
||||
static override flags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
default: false,
|
||||
}),
|
||||
profile: Flags.string({
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
// The metadata lint command is created to integrate in other dev tooling, like vscode-expo.
|
||||
// These integrations might spam this command, so we avoid communicating with our services here.
|
||||
// Note that this is an exception and you should normally use `ProjectConfig` instead.
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(MetadataLint);
|
||||
const { projectDir } = await this.getContextAsync(MetadataLint, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
} else {
|
||||
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
|
||||
}
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
if (submitProfiles.length !== 1) {
|
||||
throw new Error('Metadata only supports iOS and a single submit profile.');
|
||||
}
|
||||
|
||||
const submitProfile = submitProfiles[0].profile;
|
||||
|
||||
try {
|
||||
await loadConfigAsync({ projectDir, profile: submitProfile });
|
||||
|
||||
if (flags.json) {
|
||||
return printJsonOnlyOutput([]);
|
||||
}
|
||||
|
||||
Log.log('✅ Store configuration is valid.');
|
||||
} catch (error) {
|
||||
if (!(error instanceof MetadataValidationError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
return printJsonOnlyOutput(error.errors);
|
||||
}
|
||||
|
||||
logMetadataValidationError(error);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
@@ -6,9 +8,9 @@ import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { downloadMetadataAsync } from '../../metadata/download';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class MetadataPull extends EasCommand {
|
||||
static override description = 'generate the local store configuration from the app stores';
|
||||
@@ -24,6 +26,7 @@ export default class MetadataPull extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -33,6 +36,7 @@ export default class MetadataPull extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
analytics,
|
||||
} = await this.getContextAsync(MetadataPull, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -40,23 +44,35 @@ export default class MetadataPull extends EasCommand {
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
if (submitProfiles.length !== 1) {
|
||||
throw new Error('Metadata only supports iOS and a single submit profile.');
|
||||
}
|
||||
|
||||
const submitProfile = submitProfiles[0].profile;
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const metadataCtx = await createMetadataContextAsync({
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
exp,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const filePath = await downloadMetadataAsync(metadataCtx);
|
||||
const filePath = await downloadMetadataAsync({
|
||||
analytics,
|
||||
exp,
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
profile: submitProfile,
|
||||
});
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { uploadMetadataAsync } from '../../metadata/upload';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class MetadataPush extends EasCommand {
|
||||
static override description = 'sync the local store configuration to the app stores';
|
||||
@@ -22,6 +24,7 @@ export default class MetadataPush extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -31,6 +34,7 @@ export default class MetadataPush extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
analytics,
|
||||
} = await this.getContextAsync(MetadataPush, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -38,23 +42,36 @@ export default class MetadataPush extends EasCommand {
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
if (submitProfiles.length !== 1) {
|
||||
throw new Error('Metadata only supports iOS and a single submit profile.');
|
||||
}
|
||||
|
||||
const submitProfile = submitProfiles[0].profile;
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const metadataCtx = await createMetadataContextAsync({
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
exp,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const { appleLink } = await uploadMetadataAsync(metadataCtx);
|
||||
const { appleLink } = await uploadMetadataAsync({
|
||||
analytics,
|
||||
exp,
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
profile: submitProfile,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Store configuration is synced with the app stores.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
|
||||
import { AppJSONConfig, PackageJSONConfig, getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import { vol } from 'memfs';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
@@ -7,8 +7,11 @@ import LoggedInContextField from '../../../commandUtils/context/LoggedInContextF
|
||||
import ProjectDirContextField from '../../../commandUtils/context/ProjectDirContextField';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { saveProjectIdToAppConfigAsync } from '../../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester, jester2 } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import ProjectInit from '../init';
|
||||
@@ -18,6 +21,7 @@ jest.mock('@expo/config');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../user/actions');
|
||||
jest.mock('../../../graphql/mutations/AppMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
@@ -77,9 +81,11 @@ function mockTestProject(options: {
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest.spyOn(ProjectDirContextField.prototype, 'getValueAsync').mockResolvedValue('/test-project');
|
||||
jest
|
||||
.spyOn(LoggedInContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue({ actor: jester, graphqlClient });
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
});
|
||||
}
|
||||
|
||||
const commandOptions = { root: '/test-project' } as any;
|
||||
@@ -97,11 +103,23 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
describe('interactive', () => {
|
||||
it('is no-op if already configured for id', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to overwrite when different', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '12345');
|
||||
@@ -109,8 +127,16 @@ describe(ProjectInit.name, () => {
|
||||
});
|
||||
|
||||
it('aborts when prompt to overwrite is declined', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
await expect(
|
||||
new ProjectInit(['--id', '12345'], commandOptions).run()
|
||||
).rejects.toThrowError('Aborting');
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
});
|
||||
@@ -118,6 +144,12 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
describe('force', () => {
|
||||
it('does not prompt to overwrite when different', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
await new ProjectInit(['--id', '12345', '--force'], {
|
||||
root: '/test-project',
|
||||
} as any).run();
|
||||
@@ -140,17 +172,171 @@ describe(ProjectInit.name, () => {
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checks owner and slug consistency', () => {
|
||||
it('is a no-op if already consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to configure if not consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('overrides if force flag is present and it is not consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234', '--force'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('throws if non-interactive is present and it is not consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await expect(
|
||||
new ProjectInit(['--id', '1234', '--non-interactive'], commandOptions).run()
|
||||
).rejects.toThrow(
|
||||
'Project config error: Project owner (jester2) does not match the value configured in the "owner" field (jester). Use --force flag to overwrite.'
|
||||
);
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is not yet configured', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({});
|
||||
});
|
||||
|
||||
it('configures', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '1234');
|
||||
});
|
||||
|
||||
describe('checks owner and slug consistency', () => {
|
||||
it('is a no-op if already consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to configure if not consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('overrides if force flag is present and it is not consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234', '--force'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('throws if non-interactive is present and it is not consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await expect(
|
||||
new ProjectInit(['--id', '1234', '--non-interactive'], commandOptions).run()
|
||||
).rejects.toThrow(
|
||||
'Project config error: Project owner (jester2) does not match the value configured in the "owner" field (jester). Use --force flag to overwrite.'
|
||||
);
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
import { getProjectDashboardUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -9,7 +10,8 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
|
||||
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { AppPrivacy, Role } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import Log from '../../log';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
@@ -54,7 +56,99 @@ export default class ProjectInit extends EasCommand {
|
||||
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
|
||||
}
|
||||
|
||||
private static async initializeWithExplicitIDAsync(
|
||||
private static async modifyExpoConfigAsync(
|
||||
projectDir: string,
|
||||
modifications: Partial<ExpoConfig>
|
||||
): Promise<void> {
|
||||
const result = await modifyConfigAsync(projectDir, modifications);
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`Warning: Your project uses dynamic app configuration, and cannot be automatically modified.`
|
||||
);
|
||||
Log.warn(
|
||||
chalk.dim(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
|
||||
)
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`To complete the setup process, add the following in your ${chalk.bold(
|
||||
getProjectConfigDescription(projectDir)
|
||||
)}:`
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(chalk.bold(JSON.stringify(modifications, null, 2)));
|
||||
Log.warn();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
}
|
||||
|
||||
private static async ensureOwnerSlugConsistencyAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const correctOwner = appForProjectId.ownerAccount.name;
|
||||
const correctSlug = appForProjectId.slug;
|
||||
|
||||
if (exp.owner && exp.owner !== correctOwner) {
|
||||
if (force) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
|
||||
} else {
|
||||
const message = `Project owner (${correctOwner}) does not match the value configured in the "owner" field (${exp.owner}).`;
|
||||
if (nonInteractive) {
|
||||
throw new Error(`Project config error: ${message} Use --force flag to overwrite.`);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${message}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting');
|
||||
}
|
||||
|
||||
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
|
||||
}
|
||||
} else if (!exp.owner) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
|
||||
}
|
||||
|
||||
if (exp.slug && exp.slug !== correctSlug) {
|
||||
if (force) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
|
||||
} else {
|
||||
const message = `Project slug (${correctSlug}) does not match the value configured in the "slug" field (${exp.slug}).`;
|
||||
if (nonInteractive) {
|
||||
throw new Error(`Project config error: ${message} Use --force flag to overwrite.`);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${message}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting');
|
||||
}
|
||||
|
||||
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
|
||||
}
|
||||
} else if (!exp.slug) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
|
||||
}
|
||||
}
|
||||
|
||||
private static async setExplicitIDAsync(
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
@@ -92,14 +186,29 @@ export default class ProjectInit extends EasCommand {
|
||||
)}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
throw new Error('Aborting');
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async initializeWithExplicitIDAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
): Promise<void> {
|
||||
await this.setExplicitIDAsync(projectId, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
await this.ensureOwnerSlugConsistencyAsync(graphqlClient, projectId, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
private static async initializeWithInteractiveSelectionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
@@ -193,10 +302,7 @@ export default class ProjectInit extends EasCommand {
|
||||
}
|
||||
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const projectLink = terminalLink(projectFullName, projectDashboardUrl, {
|
||||
// https://github.com/sindresorhus/terminal-link/issues/18#issuecomment-1068020361
|
||||
fallback: () => `${projectFullName} (${projectDashboardUrl})`,
|
||||
});
|
||||
const projectLink = link(projectDashboardUrl, { text: projectFullName });
|
||||
|
||||
const account = nullthrows(allAccounts.find(a => a.name === accountName));
|
||||
|
||||
@@ -227,7 +333,10 @@ export default class ProjectInit extends EasCommand {
|
||||
} = await this.getContextAsync(ProjectInit, { nonInteractive });
|
||||
|
||||
if (id) {
|
||||
await ProjectInit.initializeWithExplicitIDAsync(id, projectDir, { force, nonInteractive });
|
||||
await ProjectInit.initializeWithExplicitIDAsync(graphqlClient, id, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
} else {
|
||||
await ProjectInit.initializeWithInteractiveSelectionAsync(graphqlClient, actor, projectDir);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async secretValue => {
|
||||
if (!secretValue) {
|
||||
return 'Secret value may not be empty.';
|
||||
return validationMessage;
|
||||
}
|
||||
if (secretType === SecretType.FILE) {
|
||||
const secretFilePath = path.resolve(secretValue);
|
||||
@@ -208,13 +208,13 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
|
||||
if (secretType === SecretType.STRING) {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
secretValue
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
secretFilePath
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
@@ -252,13 +252,13 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
|
||||
if (secretType === SecretType.STRING) {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
secretValue
|
||||
)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
secretFilePath
|
||||
)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
|
||||
import {
|
||||
EnvironmentSecretWithScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
export default class EnvironmentSecretList extends EasCommand {
|
||||
static override description = 'list environment secrets available for your current app';
|
||||
@@ -25,23 +27,18 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(graphqlClient, projectId);
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Type', 'Scope', 'ID', 'Updated at'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const secret of secrets) {
|
||||
const { name, createdAt: updatedAt, scope, id, type } = secret;
|
||||
table.push([
|
||||
name,
|
||||
EnvironmentSecretTypeToSecretType[type],
|
||||
scope,
|
||||
id,
|
||||
dateFormat(updatedAt, 'mmm dd HH:MM:ss'),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Secrets for this account and project:}`);
|
||||
Log.log(table.toString());
|
||||
Log.log(secrets.map(secret => formatSecret(secret)).join(`\n\n${chalk.dim('———')}\n\n`));
|
||||
}
|
||||
}
|
||||
|
||||
function formatSecret(secret: EnvironmentSecretWithScope): string {
|
||||
return formatFields([
|
||||
{ label: 'ID', value: secret.id },
|
||||
{ label: 'Name', value: secret.name },
|
||||
{ label: 'Scope', value: secret.scope },
|
||||
{ label: 'Type', value: secret.type },
|
||||
// TODO: Figure out why do we name it updated, while it's created at?
|
||||
{ label: 'Updated at', value: dateFormat(secret.createdAt, 'mmm dd HH:MM:ss') },
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentSecretFragment, EnvironmentSecretType } from '../../graphql/generated';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import {
|
||||
getDisplayNameForProjectIdAsync,
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import intersection from '../../utils/expodash/intersection';
|
||||
|
||||
export default class EnvironmentSecretPush extends EasCommand {
|
||||
static override description = 'read environment secrets from env file and store on the server';
|
||||
|
||||
static override flags = {
|
||||
scope: Flags.enum({
|
||||
description: 'Scope for the secrets',
|
||||
options: [EnvironmentSecretScope.ACCOUNT, EnvironmentSecretScope.PROJECT],
|
||||
default: EnvironmentSecretScope.PROJECT,
|
||||
}),
|
||||
'env-file': Flags.string({
|
||||
description: 'Env file with secrets',
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Delete and recreate existing secrets',
|
||||
default: false,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { scope, force, 'env-file': maybeEnvFilePath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentSecretPush);
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentSecretPush, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
const ownerAccount = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
const envFilePath = await resolveEnvFilePathAsync(maybeEnvFilePath, nonInteractive);
|
||||
if (!(await fs.pathExists(envFilePath))) {
|
||||
throw new Error(`File "${envFilePath}" does not exist`);
|
||||
}
|
||||
|
||||
const newSecrets: Record<string, string> = dotenv.parse(await fs.readFile(envFilePath));
|
||||
|
||||
await ensureSecretsDontExistOnServerAsync(
|
||||
graphqlClient,
|
||||
{ projectId, scope, force },
|
||||
newSecrets
|
||||
);
|
||||
|
||||
await createSecretsAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
scope,
|
||||
accountId: ownerAccount.id,
|
||||
accountName: ownerAccount.name,
|
||||
projectId,
|
||||
projectDisplayName,
|
||||
},
|
||||
newSecrets
|
||||
);
|
||||
|
||||
Log.withTick(
|
||||
`Created the following secrets on ${scope} ${chalk.bold(
|
||||
scope === EnvironmentSecretScope.ACCOUNT ? ownerAccount.name : projectDisplayName
|
||||
)}:`
|
||||
);
|
||||
for (const secretName of Object.keys(newSecrets)) {
|
||||
Log.log(`- ${secretName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveEnvFilePathAsync(
|
||||
maybeEnvFilePath: string | undefined,
|
||||
nonInteractive: boolean
|
||||
): Promise<string> {
|
||||
if (maybeEnvFilePath) {
|
||||
return maybeEnvFilePath;
|
||||
}
|
||||
|
||||
const validationMessage = 'Env file must be passed.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const { envFilePath } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'envFilePath',
|
||||
message: 'Path to the env file with secrets:',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async secretValueRaw => {
|
||||
if (!secretValueRaw) {
|
||||
return validationMessage;
|
||||
}
|
||||
const envFilePath = path.resolve(secretValueRaw);
|
||||
if (!(await fs.pathExists(envFilePath))) {
|
||||
return `File "${envFilePath}" does not exist.`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return envFilePath;
|
||||
}
|
||||
|
||||
async function ensureSecretsDontExistOnServerAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ projectId, scope, force }: { projectId: string; scope: EnvironmentSecretScope; force: boolean },
|
||||
secrets: Record<string, string>
|
||||
): Promise<void> {
|
||||
const serverSecrets = await readAllSecretsFromServerAsync(graphqlClient, projectId, scope);
|
||||
const commonSecretNames = findCommonSecretNames(serverSecrets, secrets);
|
||||
|
||||
if (commonSecretNames.length > 0) {
|
||||
if (!force) {
|
||||
Log.log(`This ${scope} already has environment secrets with the following names:`);
|
||||
for (const name of commonSecretNames) {
|
||||
Log.log(`- ${name}`);
|
||||
}
|
||||
Log.error('Run with --force flag to proceed');
|
||||
Errors.exit(1);
|
||||
} else {
|
||||
const spinner = ora('Deleting secrets already present on server...').start();
|
||||
const commonSecretNameSet = new Set(commonSecretNames);
|
||||
const commonServerSecrets = serverSecrets.filter(({ name }) => commonSecretNameSet.has(name));
|
||||
await deleteSecretsAsync(graphqlClient, commonServerSecrets);
|
||||
spinner.succeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readAllSecretsFromServerAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
scope: EnvironmentSecretScope
|
||||
): Promise<EnvironmentSecretFragment[]> {
|
||||
const { appSecrets, accountSecrets } = await EnvironmentSecretsQuery.byAppIdAsync(
|
||||
graphqlClient,
|
||||
projectId
|
||||
);
|
||||
|
||||
return scope === EnvironmentSecretScope.ACCOUNT ? accountSecrets : appSecrets;
|
||||
}
|
||||
|
||||
function findCommonSecretNames(
|
||||
serverSecrets: EnvironmentSecretFragment[],
|
||||
newSecrets: Record<string, string>
|
||||
): string[] {
|
||||
const serverSecretKeys = serverSecrets.map(({ name }) => name);
|
||||
const newSecretKeys = Object.keys(newSecrets);
|
||||
const commonKeys = intersection(serverSecretKeys, newSecretKeys);
|
||||
return commonKeys;
|
||||
}
|
||||
|
||||
async function deleteSecretsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
secrets: EnvironmentSecretFragment[]
|
||||
): Promise<void> {
|
||||
const promises = secrets.map(secret =>
|
||||
EnvironmentSecretMutation.deleteAsync(graphqlClient, secret.id)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async function createSecretsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
scope,
|
||||
accountId,
|
||||
accountName,
|
||||
projectId,
|
||||
projectDisplayName,
|
||||
}: {
|
||||
scope: EnvironmentSecretScope;
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
},
|
||||
secrets: Record<string, string>
|
||||
): Promise<void> {
|
||||
const promises = [];
|
||||
|
||||
const spinner = ora(
|
||||
`Creating secrets on ${scope} ${chalk.bold(
|
||||
scope === EnvironmentSecretScope.ACCOUNT ? accountName : projectDisplayName
|
||||
)}...`
|
||||
).start();
|
||||
for (const [secretName, secretValue] of Object.entries(secrets)) {
|
||||
if (scope === EnvironmentSecretScope.ACCOUNT) {
|
||||
promises.push(
|
||||
EnvironmentSecretMutation.createForAccountAsync(
|
||||
graphqlClient,
|
||||
{ name: secretName, value: secretValue, type: EnvironmentSecretType.String },
|
||||
accountId
|
||||
)
|
||||
);
|
||||
} else {
|
||||
promises.push(
|
||||
EnvironmentSecretMutation.createForAppAsync(
|
||||
graphqlClient,
|
||||
{ name: secretName, value: secretValue, type: EnvironmentSecretType.String },
|
||||
projectId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
spinner.succeed();
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export default class Submit extends EasCommand {
|
||||
exclusive: ['latest', 'id', 'path'],
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
description: 'Always print logs from Submission Service',
|
||||
description: 'Always print logs from EAS Submit',
|
||||
default: false,
|
||||
}),
|
||||
wait: Flags.boolean({
|
||||
@@ -93,6 +93,7 @@ export default class Submit extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -100,6 +101,7 @@ export default class Submit extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
analytics,
|
||||
} = await this.getContextAsync(Submit, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -129,6 +131,7 @@ export default class Submit extends EasCommand {
|
||||
nonInteractive: flagsWithPlatform.nonInteractive,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
exp,
|
||||
projectId,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import UpdatePublish from '..';
|
||||
import { ensureBranchExistsAsync } from '../../../branch/queries';
|
||||
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
|
||||
|
||||
const projectRoot = '/test-project';
|
||||
const commandOptions = { root: projectRoot } as any;
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('@expo/config-plugins');
|
||||
jest.mock('../../../branch/queries');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
|
||||
jest.mock('../../../update/configure');
|
||||
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
|
||||
jest.mock('../../../graphql/mutations/PublishMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/UpdateQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/publish', () => ({
|
||||
...jest.requireActual('../../../project/publish'),
|
||||
buildBundlesAsync: jest.fn(),
|
||||
collectAssetsAsync: jest.fn(),
|
||||
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
|
||||
uploadAssetsAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe(UpdatePublish.name, () => {
|
||||
afterEach(() => vol.reset());
|
||||
|
||||
// Deprecated and split to a new command: update:republish
|
||||
it('errors with --republish', async () => {
|
||||
await expect(new UpdatePublish(['--republish'], commandOptions).run()).rejects.toThrow(
|
||||
'--group and --republish flags are deprecated'
|
||||
);
|
||||
});
|
||||
|
||||
// Deprecated and split to a new command: update:republish
|
||||
it('errors with --group', async () => {
|
||||
await expect(new UpdatePublish(['--group=abc123'], commandOptions).run()).rejects.toThrow(
|
||||
'--group and --republish flags are deprecated'
|
||||
);
|
||||
});
|
||||
|
||||
it('errors with both --channel and --branch', async () => {
|
||||
const flags = ['--channel=channel123', '--branch=branch123'];
|
||||
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a new update with --non-interactive, --branch, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject();
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
|
||||
platforms.map(platform => ({
|
||||
id: 'update123',
|
||||
group: 'group123',
|
||||
runtimeVersion,
|
||||
platform,
|
||||
manifestPermalink: 'https://example.com/update123/manifest',
|
||||
}))
|
||||
);
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a new update with --non-interactive, --channel, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
|
||||
|
||||
const { projectId } = mockTestProject();
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
|
||||
platforms.map(platform => ({
|
||||
id: 'update123',
|
||||
group: 'group123',
|
||||
runtimeVersion,
|
||||
platform,
|
||||
manifestPermalink: 'https://example.com/update123/manifest',
|
||||
}))
|
||||
);
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
{
|
||||
appId: projectId,
|
||||
branchName: 'branchFromChannel',
|
||||
}
|
||||
);
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
} = {}): { projectId: string } {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const appJSON: AppJSONConfig = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: jester.accounts[0].name,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'package.json': JSON.stringify(packageJSON),
|
||||
'app.json': JSON.stringify(appJSON),
|
||||
},
|
||||
projectRoot
|
||||
);
|
||||
|
||||
const mockManifest = { exp: appJSON.expo };
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest
|
||||
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue(async () => ({
|
||||
exp: mockManifest.exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
}));
|
||||
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
});
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
return { projectId: configuredProjectId };
|
||||
}
|
||||
|
||||
/** Create a new in-memory export of the project */
|
||||
function mockTestExport({
|
||||
exportDir = 'dist',
|
||||
platforms = ['android', 'ios'],
|
||||
runtimeVersion = 'exposdk:47.0.0',
|
||||
}: {
|
||||
exportDir?: string;
|
||||
platforms?: Platform[];
|
||||
runtimeVersion?: string;
|
||||
} = {}): {
|
||||
inputDir: string;
|
||||
platforms: Platform[];
|
||||
runtimeVersion: string;
|
||||
} {
|
||||
/* eslint-disable node/no-sync */
|
||||
vol.mkdirpSync(path.join(projectRoot, exportDir, 'bundles'));
|
||||
for (const platform of platforms) {
|
||||
vol.writeFileSync(
|
||||
path.join(projectRoot, exportDir, 'bundles', `${platform}-fake.js`),
|
||||
`console.log("fake bundle for ${platform}");`
|
||||
);
|
||||
}
|
||||
|
||||
jest.mocked(collectAssetsAsync).mockResolvedValue(
|
||||
Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
{
|
||||
assets: [],
|
||||
launchAsset: {
|
||||
contentType: 'application/javascript',
|
||||
path: path.join(projectRoot, exportDir, 'bundles', `${platform}-fake.js`),
|
||||
},
|
||||
},
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
jest.mocked(uploadAssetsAsync).mockResolvedValue({
|
||||
// platforms are mocked all containing only the launch asset
|
||||
assetCount: platforms.length,
|
||||
uniqueAssetCount: platforms.length,
|
||||
uniqueUploadedAssetCount: platforms.length,
|
||||
assetLimitPerUpdateGroup: 9001,
|
||||
});
|
||||
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
return { inputDir: exportDir, platforms, runtimeVersion };
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
|
||||
import { vol } from 'memfs';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
|
||||
import ProjectConfigContextField from '../../../commandUtils/context/ProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { CodeSigningInfo, UpdateFragment } from '../../../graphql/generated';
|
||||
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../../update/queries';
|
||||
import UpdateRepublish from '../republish';
|
||||
|
||||
const projectRoot = '/test-project';
|
||||
const commandOptions = { root: projectRoot } as any;
|
||||
const updateStub: UpdateFragment = {
|
||||
id: 'update-1234',
|
||||
group: 'group-1234',
|
||||
branch: { id: 'branch-1234', name: 'main' },
|
||||
message: 'test message',
|
||||
runtimeVersion: 'exposdk:47.0.0',
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
};
|
||||
|
||||
const codeSigningStub: CodeSigningInfo = {
|
||||
keyid: 'keyid',
|
||||
alg: 'alg',
|
||||
sig: 'sig',
|
||||
};
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
|
||||
jest.mock('../../../graphql/mutations/PublishMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/UpdateQuery');
|
||||
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
|
||||
jest.mock('../../../update/queries');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe(UpdateRepublish.name, () => {
|
||||
afterEach(() => vol.reset());
|
||||
|
||||
it('errors without --channel, --branch, or --group', async () => {
|
||||
await expect(new UpdateRepublish([], commandOptions).run()).rejects.toThrow(
|
||||
'--channel, --branch, or --group must be specified'
|
||||
);
|
||||
});
|
||||
|
||||
it('errors when providing both --group and --branch', async () => {
|
||||
const flags = ['--group=1234', '--branch=main'];
|
||||
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
/--branch=main cannot also be provided when using --group/
|
||||
);
|
||||
});
|
||||
|
||||
it('errors when providing both --channel and --branch', async () => {
|
||||
const flags = ['--channel=main', '--branch=main'];
|
||||
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
/--branch=main cannot also be provided when using --channel/
|
||||
);
|
||||
});
|
||||
|
||||
it('errors when providing both --group and --channel', async () => {
|
||||
const flags = ['--group=1234', '--channel=main'];
|
||||
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
/--channel=main cannot also be provided when using --group/
|
||||
);
|
||||
});
|
||||
|
||||
it('errors when update group is empty', async () => {
|
||||
const flags = ['--group=1234'];
|
||||
|
||||
mockTestProject();
|
||||
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([]);
|
||||
|
||||
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'There are no published updates found'
|
||||
);
|
||||
});
|
||||
|
||||
it('errors when update group has no update for requested platform', async () => {
|
||||
const flags = ['--group=1234', '--platform=android'];
|
||||
|
||||
mockTestProject();
|
||||
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([
|
||||
{
|
||||
...updateStub,
|
||||
platform: 'ios',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'There are no updates on branch "main" published for the platform(s) "android" with group ID "1234". Did you mean to publish a new update instead?'
|
||||
);
|
||||
});
|
||||
|
||||
it('re-creates update with --group and --message', async () => {
|
||||
const flags = ['--group=1234', '--message=test-republish'];
|
||||
|
||||
mockTestProject();
|
||||
// Mock queries to retrieve the update and code signing info
|
||||
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([updateStub]);
|
||||
// Mock mutations to store the new update
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
|
||||
{
|
||||
...updateStub,
|
||||
id: 'update-new',
|
||||
platform: 'ios',
|
||||
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
|
||||
},
|
||||
]);
|
||||
|
||||
await new UpdateRepublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
branchId: updateStub.branch.id,
|
||||
runtimeVersion: updateStub.runtimeVersion,
|
||||
updateInfoGroup: expect.objectContaining({
|
||||
ios: expect.any(Object),
|
||||
}),
|
||||
gitCommitHash: updateStub.gitCommitHash,
|
||||
awaitingCodeSigningInfo: false,
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
expect(PublishMutation.setCodeSigningInfoAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-creates update using codesigning with --group and --message', async () => {
|
||||
const flags = ['--group=1234', '--message=test-republish'];
|
||||
const codeSigning = {
|
||||
alg: 'alg',
|
||||
keyid: 'keyid',
|
||||
sig: 'sig',
|
||||
};
|
||||
|
||||
mockTestProject();
|
||||
// Mock queries to retrieve the update and code signing info
|
||||
jest
|
||||
.mocked(UpdateQuery.viewUpdateGroupAsync)
|
||||
.mockResolvedValue([{ ...updateStub, codeSigningInfo: codeSigningStub }]);
|
||||
// Mock mutations to store the new update
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
|
||||
{
|
||||
...updateStub,
|
||||
id: 'update-new',
|
||||
platform: 'ios',
|
||||
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
|
||||
},
|
||||
]);
|
||||
jest.mocked(PublishMutation.setCodeSigningInfoAsync).mockResolvedValue({} as any);
|
||||
|
||||
await new UpdateRepublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
branchId: updateStub.branch.id,
|
||||
runtimeVersion: updateStub.runtimeVersion,
|
||||
updateInfoGroup: expect.objectContaining({
|
||||
ios: expect.any(Object),
|
||||
}),
|
||||
gitCommitHash: updateStub.gitCommitHash,
|
||||
awaitingCodeSigningInfo: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
expect(PublishMutation.setCodeSigningInfoAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
'update-new',
|
||||
expect.objectContaining(codeSigning)
|
||||
);
|
||||
});
|
||||
|
||||
it('re-creates update with --branch and --message', async () => {
|
||||
const flags = ['--branch=branch123', '--message=test-republish'];
|
||||
|
||||
mockTestProject();
|
||||
// Mock the prompt to ask the user which update to republish, from branch
|
||||
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
|
||||
// Mock mutations to store the new update
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
|
||||
{
|
||||
...updateStub,
|
||||
id: 'update-new',
|
||||
platform: 'ios',
|
||||
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
|
||||
},
|
||||
]);
|
||||
|
||||
await new UpdateRepublish(flags, commandOptions).run();
|
||||
|
||||
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
expect.objectContaining({ branchName: 'branch123' })
|
||||
);
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
branchId: updateStub.branch.id,
|
||||
runtimeVersion: updateStub.runtimeVersion,
|
||||
updateInfoGroup: expect.objectContaining({
|
||||
ios: expect.any(Object),
|
||||
}),
|
||||
gitCommitHash: updateStub.gitCommitHash,
|
||||
awaitingCodeSigningInfo: false,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('re-creates update with --channel and --message', async () => {
|
||||
const flags = ['--channel=channel123', '--message=test-republish'];
|
||||
|
||||
mockTestProject();
|
||||
// Mock resolving the channel to branch name, only valid for a single branch connected
|
||||
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
|
||||
// Mock the prompt to ask the user which update to republish, from branch
|
||||
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
|
||||
// Mock mutations to store the new update
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
|
||||
{
|
||||
...updateStub,
|
||||
id: 'update-new',
|
||||
platform: 'ios',
|
||||
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
|
||||
},
|
||||
]);
|
||||
|
||||
await new UpdateRepublish(flags, commandOptions).run();
|
||||
|
||||
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
expect.objectContaining({ branchName: 'branchFromChannel' })
|
||||
);
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
branchId: updateStub.branch.id,
|
||||
runtimeVersion: updateStub.runtimeVersion,
|
||||
updateInfoGroup: expect.objectContaining({
|
||||
ios: expect.any(Object),
|
||||
}),
|
||||
gitCommitHash: updateStub.gitCommitHash,
|
||||
awaitingCodeSigningInfo: false,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
} = {}): void {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const appJSON: AppJSONConfig = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: jester.accounts[0].name,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'package.json': JSON.stringify(packageJSON),
|
||||
'app.json': JSON.stringify(appJSON),
|
||||
},
|
||||
projectRoot
|
||||
);
|
||||
|
||||
const mockManifest = { exp: appJSON.expo };
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest.spyOn(ProjectConfigContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
exp: mockManifest.exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
});
|
||||
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
});
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
}
|
||||
@@ -1,24 +1,14 @@
|
||||
import { ExpoConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import {
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
|
||||
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
|
||||
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
|
||||
ensureEASUpdateIsConfiguredAsync,
|
||||
ensureEASUpdateIsConfiguredInEasJsonAsync,
|
||||
} from '../../update/configure';
|
||||
|
||||
export default class UpdateConfigure extends EasCommand {
|
||||
static override description = 'configure the project to support EAS Update';
|
||||
@@ -30,6 +20,7 @@ export default class UpdateConfigure extends EasCommand {
|
||||
options: ['android', 'ios', 'all'],
|
||||
default: 'all',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -38,280 +29,46 @@ export default class UpdateConfigure extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
const { flags } = await this.parse(UpdateConfigure);
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
const {
|
||||
projectConfig: { projectId, exp, projectDir },
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(UpdateConfigure, {
|
||||
nonInteractive: true,
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
|
||||
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
}
|
||||
Log.log(
|
||||
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
|
||||
const [androidWorkflow, iosWorkflow] = await Promise.all([
|
||||
resolveWorkflowAsync(projectDir, Platform.ANDROID),
|
||||
resolveWorkflowAsync(projectDir, Platform.IOS),
|
||||
]);
|
||||
|
||||
const updatedExp = await configureAppJSONForEASUpdateAsync({
|
||||
projectDir,
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
exp,
|
||||
platform,
|
||||
workflows: {
|
||||
android: androidWorkflow,
|
||||
ios: iosWorkflow,
|
||||
},
|
||||
projectId,
|
||||
projectDir,
|
||||
platform,
|
||||
});
|
||||
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
|
||||
|
||||
// configure native files for EAS Update
|
||||
if (
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
|
||||
androidWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId);
|
||||
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
|
||||
}
|
||||
if (
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
|
||||
iosWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId);
|
||||
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
|
||||
}
|
||||
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Your app is configured to run EAS Update!`);
|
||||
}
|
||||
}
|
||||
|
||||
async function configureAppJSONForEASUpdateAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
platform,
|
||||
workflows,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
platform: RequestedPlatform;
|
||||
workflows: {
|
||||
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
|
||||
};
|
||||
projectId: string;
|
||||
}): Promise<ExpoConfig> {
|
||||
// this command is non-interactive in the way it was designed
|
||||
const easUpdateURL = getEASUpdateURL(projectId);
|
||||
const updates = { ...exp.updates, url: easUpdateURL };
|
||||
|
||||
const androidDefaultRuntimeVersion =
|
||||
workflows['android'] === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
const iosDefaultRuntimeVersion =
|
||||
workflows['ios'] === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
|
||||
const newAndroidRuntimeVersion =
|
||||
exp.android?.runtimeVersion ?? exp.runtimeVersion ?? androidDefaultRuntimeVersion;
|
||||
const newIosRuntimeVersion =
|
||||
exp.ios?.runtimeVersion ?? exp.runtimeVersion ?? iosDefaultRuntimeVersion;
|
||||
|
||||
let newConfig: Partial<ExpoConfig>;
|
||||
let newConfigOnlyAddedValues: Partial<ExpoConfig>;
|
||||
switch (platform) {
|
||||
case RequestedPlatform.All: {
|
||||
if (isRuntimeEqual(newAndroidRuntimeVersion, newIosRuntimeVersion)) {
|
||||
newConfig = {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: undefined,
|
||||
},
|
||||
ios: { ...exp.ios, runtimeVersion: undefined },
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
...(exp.android && 'runtimeVersion' in exp.android
|
||||
? {
|
||||
android: {
|
||||
runtimeVersion: '<remove this key>',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(exp.ios && 'runtimeVersion' in exp.ios
|
||||
? {
|
||||
ios: {
|
||||
runtimeVersion: '<remove this key>',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newConfig = {
|
||||
runtimeVersion: undefined, // top level runtime is redundant if it is specified in both android and ios
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
ios: {
|
||||
...exp.ios,
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
...('runtimeVersion' in exp
|
||||
? {
|
||||
runtimeVersion: '<remove this key>', // top level runtime is redundant if it is specified in both android and ios
|
||||
}
|
||||
: {}),
|
||||
android: {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
ios: {
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestedPlatform.Android: {
|
||||
newConfig = {
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
android: {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
case RequestedPlatform.Ios: {
|
||||
newConfig = {
|
||||
ios: {
|
||||
...exp.ios,
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
ios: {
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await modifyConfigAsync(projectDir, newConfig);
|
||||
|
||||
const preexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
|
||||
const preexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
if (exp.updates?.url) {
|
||||
if (exp.updates.url !== easUpdateURL) {
|
||||
Log.withTick(
|
||||
`Overwrote "${exp.updates?.url}" with "${easUpdateURL}" for the updates.url value in app.json`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
|
||||
}
|
||||
|
||||
if (
|
||||
!preexistingAndroidRuntimeVersion &&
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform)
|
||||
) {
|
||||
Log.withTick(
|
||||
`Set ${appPlatformDisplayNames[AppPlatform.Android]} runtimeVersion to "${JSON.stringify(
|
||||
newConfig.android?.runtimeVersion ?? newConfig.runtimeVersion
|
||||
)}" in app.json`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!preexistingIosRuntimeVersion &&
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)
|
||||
) {
|
||||
Log.withTick(
|
||||
`Set ${appPlatformDisplayNames[AppPlatform.Ios]} runtimeVersion to "${JSON.stringify(
|
||||
newConfig.ios?.runtimeVersion ?? newConfig.runtimeVersion
|
||||
)}" in app.json`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`It looks like you are using a dynamic configuration! ${learnMore(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore(
|
||||
'https://expo.fyi/eas-update-config.md'
|
||||
)}\n`
|
||||
);
|
||||
Log.log(chalk.bold(JSON.stringify(newConfigOnlyAddedValues, null, 2)));
|
||||
Log.addNewLineIfNone();
|
||||
if (workflows['android'] === Workflow.GENERIC || workflows['ios'] === Workflow.GENERIC) {
|
||||
Log.warn(
|
||||
`You will also have to manually edit the projects ${chalk.bold(
|
||||
'Expo.plist/AndroidManifest.xml'
|
||||
)}. ${learnMore('https://expo.fyi/eas-update-config.md#native-configuration')}`
|
||||
);
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
assert(result.config, 'A successful result should have a config');
|
||||
|
||||
return result.config.expo;
|
||||
}
|
||||
|
||||
function isRuntimeEqual(
|
||||
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' },
|
||||
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' }
|
||||
): boolean {
|
||||
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
|
||||
return runtimeVersionA === runtimeVersionB;
|
||||
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
|
||||
return runtimeVersionA.policy === runtimeVersionB.policy;
|
||||
} else {
|
||||
return false;
|
||||
Log.log(`🎉 Your app is configured with EAS Update!`);
|
||||
Log.newLine();
|
||||
Log.log(`${chalk.bold('Next steps')}:`);
|
||||
Log.newLine();
|
||||
Log.log('Update a production build:');
|
||||
Log.log(`1. Create a new build. Example: ${chalk.bold('eas build --profile production')}.`);
|
||||
Log.log('2. Make changes in your project.');
|
||||
Log.log(`3. Publish an update. Example: ${chalk.bold('eas update --channel production')}.`);
|
||||
Log.log('4. Force close and reopen the app at least twice to view the update.');
|
||||
|
||||
Log.newLine();
|
||||
Log.log('Preview an update:');
|
||||
Log.log(
|
||||
`1. Publish an update to a branch. Example: ${chalk.bold('eas update --branch new-feature')}.`
|
||||
);
|
||||
Log.log(
|
||||
'2. In Expo Go or a development build, navigate to Projects > [project name] > Branch > Open.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
@@ -6,123 +6,96 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import { ensureChannelExistsAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
UpdatePublishMutation,
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
ExpoCLIExportPlatformFlag,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
filterExportedPlatformsByFlag,
|
||||
isUploadedAssetCountAboveWarningThreshold,
|
||||
resolveInputDirectoryAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
|
||||
import { formatUpdateMessage } from '../../update/utils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
|
||||
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getManifestBodyAsync,
|
||||
signManifestBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
export type PublishPlatformFlag = PublishPlatform | 'all';
|
||||
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
|
||||
|
||||
async function ensureChannelExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
try {
|
||||
await createUpdateChannelOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
Log.withTick(
|
||||
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
|
||||
);
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
channel?: string;
|
||||
message?: string;
|
||||
platform: string;
|
||||
'input-dir': string;
|
||||
'skip-bundler': boolean;
|
||||
'private-key-path'?: string;
|
||||
'non-interactive': boolean;
|
||||
json: boolean;
|
||||
/** @deprecated see UpdateRepublish command */
|
||||
group?: string;
|
||||
/** @deprecated see UpdateRepublish command */
|
||||
republish?: boolean;
|
||||
};
|
||||
|
||||
export async function ensureBranchExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name: branchName }: ViewBranchQueryVariables
|
||||
): Promise<{
|
||||
branchId: string;
|
||||
}> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
type UpdateFlags = {
|
||||
auto: boolean;
|
||||
platform: ExpoCLIExportPlatformFlag;
|
||||
branchName?: string;
|
||||
channelName?: string;
|
||||
updateMessage?: string;
|
||||
inputDir: string;
|
||||
skipBundler: boolean;
|
||||
privateKeyPath?: string;
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
const { id } = updateBranch;
|
||||
await ensureChannelExistsAsync(graphqlClient, { appId, branchId: id, channelName: branchName });
|
||||
return { branchId: id };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId,
|
||||
branchId: newUpdateBranch.id,
|
||||
channelName: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
return RequestedPlatform.Android;
|
||||
case 'ios':
|
||||
return RequestedPlatform.Ios;
|
||||
case 'web':
|
||||
return null;
|
||||
case 'all':
|
||||
return RequestedPlatform.All;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,16 +107,20 @@ export default class UpdatePublish extends EasCommand {
|
||||
description: 'Branch to publish the update group on',
|
||||
required: false,
|
||||
}),
|
||||
channel: Flags.string({
|
||||
description: 'Channel that the published update should affect',
|
||||
required: false,
|
||||
}),
|
||||
message: Flags.string({
|
||||
description: 'A short message describing the update',
|
||||
required: false,
|
||||
}),
|
||||
republish: Flags.boolean({
|
||||
description: 'Republish an update group',
|
||||
description: 'Republish an update group (deprecated, see republish command)',
|
||||
exclusive: ['input-dir', 'skip-bundler'],
|
||||
}),
|
||||
group: Flags.string({
|
||||
description: 'Update group to republish',
|
||||
description: 'Update group to republish (deprecated, see republish command)',
|
||||
exclusive: ['input-dir', 'skip-bundler'],
|
||||
}),
|
||||
'input-dir': Flags.string({
|
||||
@@ -157,7 +134,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
options: [
|
||||
// TODO: Add web when it's fully supported
|
||||
...defaultPublishPlatforms,
|
||||
'all',
|
||||
],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
@@ -179,21 +160,21 @@ export default class UpdatePublish extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { flags: rawFlags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
let {
|
||||
branch: branchName,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
platform,
|
||||
'private-key-path': privateKeyPath,
|
||||
'non-interactive': nonInteractive,
|
||||
platform: platformFlag,
|
||||
channelName,
|
||||
updateMessage,
|
||||
inputDir,
|
||||
skipBundler,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
} = flags;
|
||||
nonInteractive,
|
||||
} = this.sanitizeFlags(rawFlags);
|
||||
let branchName = this.sanitizeFlags(rawFlags).branchName;
|
||||
|
||||
const {
|
||||
getDynamicProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -205,11 +186,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const platformFlag = platform as PublishPlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = republish || !!group;
|
||||
|
||||
const { exp, projectId, projectDir } = await getDynamicProjectConfigAsync({
|
||||
const {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
projectId,
|
||||
projectDir,
|
||||
} = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
@@ -219,53 +200,43 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
platform: getRequestedPlatform(platformFlag),
|
||||
projectDir,
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync();
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
|
||||
if (!hasExpoUpdates && nonInteractive) {
|
||||
Errors.error(
|
||||
`${chalk.bold(
|
||||
'expo-updates'
|
||||
)} must already be installed when executing in non-interactive mode`,
|
||||
{ exit: 1 }
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
|
||||
if (channelName && branchName) {
|
||||
throw new Error(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasExpoUpdates) {
|
||||
const install = await confirmAsync({
|
||||
message: `You are creating an update which requires ${chalk.bold(
|
||||
'expo-updates'
|
||||
)} to be installed in your app.\n Do you want EAS CLI to install it for you?`,
|
||||
instructions: 'The command will abort unless you agree.',
|
||||
});
|
||||
if (install) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
} else {
|
||||
Errors.error(`Install ${chalk.bold('expo-updates')} manually and come back later.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
if (channelName) {
|
||||
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
await checkEASUpdateURLIsSetAsync(exp, projectId);
|
||||
|
||||
if (!branchName) {
|
||||
if (autoFlag) {
|
||||
branchName = await getDefaultBranchNameAsync();
|
||||
} else if (nonInteractive) {
|
||||
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
|
||||
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
|
||||
} else {
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to ${
|
||||
republish ? 'republish' : 'publish'
|
||||
} on?`,
|
||||
displayTextForListItem: updateBranch =>
|
||||
`${updateBranch.name} ${chalk.grey(
|
||||
promptTitle: `Which branch would you like to publish on?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: `${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
)}`,
|
||||
}),
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
branchName = branch.name;
|
||||
@@ -284,167 +255,113 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
if (!updateMessage && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (republish) {
|
||||
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
|
||||
// Instead we get the `updateInfoGroup` from the update we wish to republish.
|
||||
let updatesToRepublish: Pick<
|
||||
Update,
|
||||
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
|
||||
>[];
|
||||
if (group) {
|
||||
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
|
||||
groupId: group,
|
||||
});
|
||||
updatesToRepublish = updatesByGroup;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
|
||||
updatesToRepublish = await selectUpdateGroupOnBranchAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
}
|
||||
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 "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
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}`;
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
|
||||
if (!message) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: `Republish "${oldMessage!}" - group: ${group}`,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
if (!message && autoFlag) {
|
||||
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
const bundleSpinner = ora().start('Building bundle...');
|
||||
try {
|
||||
await buildBundlesAsync({ projectDir, inputDir });
|
||||
bundleSpinner.succeed('Built bundle!');
|
||||
} catch (e) {
|
||||
bundleSpinner.fail('Failed to build bundle!');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const assetSpinner = ora().start('Uploading assets...');
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
const bundleSpinner = ora().start('Exporting...');
|
||||
try {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
|
||||
const uploadResults = await uploadAssetsAsync(
|
||||
graphqlClient,
|
||||
assets,
|
||||
projectId,
|
||||
(totalAssets, missingAssets) => {
|
||||
assetSpinner.text = `Uploading assets. Finished (${
|
||||
totalAssets - missingAssets
|
||||
}/${totalAssets})`;
|
||||
}
|
||||
);
|
||||
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
|
||||
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
|
||||
: `Uploading assets skipped -- no new assets found!`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
|
||||
bundleSpinner.succeed('Exported bundle(s)');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
bundleSpinner.fail('Export failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const truncatedMessage = truncatePublishUpdateMessage(message!);
|
||||
// After possibly bundling, assert that the input directory can be found.
|
||||
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
|
||||
|
||||
const runtimeToPlatformMapping: Record<string, string[]> = {};
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
|
||||
.filter(pair => pair[1] === runtime)
|
||||
.map(pair => pair[0]);
|
||||
const assetSpinner = ora().start('Uploading...');
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
|
||||
try {
|
||||
const collectedAssets = await collectAssetsAsync(distRoot);
|
||||
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
|
||||
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
|
||||
|
||||
const uploadResults = await uploadAssetsAsync(
|
||||
graphqlClient,
|
||||
assets,
|
||||
projectId,
|
||||
(totalAssets, missingAssets) => {
|
||||
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
|
||||
}
|
||||
);
|
||||
|
||||
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
|
||||
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
|
||||
: `Uploaded: No changes detected`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload');
|
||||
throw e;
|
||||
}
|
||||
|
||||
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
|
||||
if (truncatedMessage !== updateMessage) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
|
||||
|
||||
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
|
||||
for (const runtime of runtimeVersions) {
|
||||
const platforms = runtimeVersions
|
||||
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
|
||||
.map(({ platform }) => platform);
|
||||
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
|
||||
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
|
||||
}
|
||||
}
|
||||
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
branchName,
|
||||
});
|
||||
if (createdBranch) {
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchId,
|
||||
channelName: branchName,
|
||||
});
|
||||
}
|
||||
|
||||
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
|
||||
|
||||
const vcsClient = getVcsClient();
|
||||
|
||||
const gitCommitHash = await vcsClient.getCommitHashAsync();
|
||||
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
|
||||
([runtime, platforms]) => {
|
||||
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
|
||||
({ runtimeVersion, platforms }) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
@@ -452,16 +369,13 @@ export default class UpdatePublish extends EasCommand {
|
||||
])
|
||||
);
|
||||
|
||||
if (republish && !oldRuntimeVersion) {
|
||||
throw new Error(
|
||||
'Cannot find the runtime version of the update group that is being republished.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
runtimeVersion,
|
||||
message: truncatedMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
@@ -530,12 +444,15 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
|
||||
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime
|
||||
update => update.runtimeVersion === runtime.runtimeVersion
|
||||
);
|
||||
if (newUpdatesForRuntimeVersion.length === 0) {
|
||||
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
|
||||
throw new Error(
|
||||
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
|
||||
);
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
@@ -552,7 +469,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime },
|
||||
{ label: 'Runtime version', value: runtime.runtimeVersion },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate
|
||||
@@ -560,6 +477,14 @@ export default class UpdatePublish extends EasCommand {
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: truncatedMessage },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Website link', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
@@ -579,19 +504,64 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
|
||||
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
|
||||
Errors.error(
|
||||
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.group || flags.republish) {
|
||||
// Pick the first flag set that is defined, in this specific order
|
||||
const args = [
|
||||
['--group', flags.group],
|
||||
['--branch', flags.branch],
|
||||
].filter(([_, value]) => value)[0];
|
||||
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
'The --group and --republish flags are deprecated, use the republish command instead:'
|
||||
);
|
||||
Log.warn(` ${chalk.bold([`eas update:republish`, ...(args ?? [])].join(' '))}`);
|
||||
Log.newLine();
|
||||
|
||||
Errors.error('--group and --republish flags are deprecated', { exit: 1 });
|
||||
}
|
||||
|
||||
return {
|
||||
auto,
|
||||
branchName,
|
||||
channelName,
|
||||
updateMessage,
|
||||
inputDir: flags['input-dir'],
|
||||
skipBundler: flags['skip-bundler'],
|
||||
platform: flags.platform as RequestedPlatform,
|
||||
privateKeyPath: flags['private-key-path'],
|
||||
nonInteractive,
|
||||
json: flags.json ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: PublishPlatformFlag,
|
||||
platforms: PublishPlatform[],
|
||||
projectDir: string
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
|
||||
|
||||
): Promise<{ platform: string; runtimeVersion: string }[]> {
|
||||
for (const platform of platforms) {
|
||||
if (platform === 'web') {
|
||||
continue;
|
||||
}
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
|
||||
const isManaged =
|
||||
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
@@ -600,34 +570,18 @@ async function getRuntimeVersionObjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
nullthrows(
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${platform}`
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
])
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig, projectId: string): Promise<void> {
|
||||
const configuredURL = exp.updates?.url;
|
||||
const expectedURL = getEASUpdateURL(projectId);
|
||||
|
||||
if (configuredURL !== expectedURL) {
|
||||
throw new Error(
|
||||
`The update URL is incorrectly configured for EAS Update. Set updates.url to ${expectedURL} in your ${chalk.bold(
|
||||
'app.json'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const truncatePublishUpdateMessage = (originalMessage: string): string => {
|
||||
if (originalMessage.length > 1024) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
return originalMessage.substring(0, 1021) + '...';
|
||||
}
|
||||
return originalMessage;
|
||||
};
|
||||
@@ -70,7 +70,9 @@ export default class UpdateList extends EasCommand {
|
||||
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: 'Which branch would you like to view?',
|
||||
displayTextForListItem: updateBranch => updateBranch.name,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: updateBranch.name,
|
||||
}),
|
||||
paginatedQueryOptions:
|
||||
// discard limit and offset because this query is not those flag's intended target
|
||||
{
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import { Platform } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { Update } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
|
||||
import { truncateString as truncateUpdateMessage } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
const defaultRepublishPlatforms: Platform[] = ['android', 'ios'];
|
||||
|
||||
type UpdateRepublishRawFlags = {
|
||||
branch?: string;
|
||||
channel?: string;
|
||||
group?: string;
|
||||
message?: string;
|
||||
platform: string;
|
||||
'non-interactive': boolean;
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type UpdateRepublishFlags = {
|
||||
branchName?: string;
|
||||
channelName?: string;
|
||||
groupId?: string;
|
||||
updateMessage?: string;
|
||||
platform: Platform[];
|
||||
nonInteractive: boolean;
|
||||
json: boolean;
|
||||
};
|
||||
|
||||
type UpdateToRepublish = {
|
||||
groupId: string;
|
||||
branchId: string;
|
||||
branchName: string;
|
||||
} & Pick<
|
||||
Update,
|
||||
| 'message'
|
||||
| 'runtimeVersion'
|
||||
| 'manifestFragment'
|
||||
| 'platform'
|
||||
| 'gitCommitHash'
|
||||
| 'codeSigningInfo'
|
||||
>;
|
||||
|
||||
export default class UpdateRepublish extends EasCommand {
|
||||
static override description = 'rollback to an existing update';
|
||||
|
||||
static override flags = {
|
||||
channel: Flags.string({
|
||||
description: 'Channel name to select an update to republish from',
|
||||
exclusive: ['branch', 'group'],
|
||||
}),
|
||||
branch: Flags.string({
|
||||
description: 'Branch name to select an update to republish from',
|
||||
exclusive: ['channel', 'group'],
|
||||
}),
|
||||
group: Flags.string({
|
||||
description: 'Update group ID to republish',
|
||||
exclusive: ['branch', 'channel'],
|
||||
}),
|
||||
message: Flags.string({
|
||||
description: 'Short message describing the republished update',
|
||||
required: false,
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: [...defaultRepublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(UpdateRepublish);
|
||||
const flags = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const {
|
||||
projectConfig: { exp, projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(UpdateRepublish, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const existingUpdates = await getOrAskUpdatesAsync(graphqlClient, projectId, flags);
|
||||
const updatesToPublish = existingUpdates.filter(update =>
|
||||
flags.platform.includes(update.platform as Platform)
|
||||
);
|
||||
|
||||
if (existingUpdates.length === 0) {
|
||||
throw new Error(`There are no published updates found`);
|
||||
}
|
||||
if (updatesToPublish.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${
|
||||
existingUpdates[0].branchName
|
||||
}" published for the platform(s) "${rawFlags.platform}" with group ID "${
|
||||
flags.groupId ? flags.groupId : updatesToPublish[0].groupId
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
if (rawFlags.platform === 'all') {
|
||||
Log.withTick(`The republished update will appear only on: ${rawFlags.platform}`);
|
||||
} else {
|
||||
const platformsFromUpdates = updatesToPublish.map(update => update.platform);
|
||||
if (platformsFromUpdates.length < defaultRepublishPlatforms.length) {
|
||||
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`The republished update will appear on the same platforms it was originally published on: ${platformsFromUpdates.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
// This command only republishes a single update group
|
||||
// The update group properties are the same for all updates
|
||||
const { branchId, branchName, runtimeVersion } = updatesToPublish[0];
|
||||
|
||||
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
|
||||
|
||||
// If codesigning was created for the original update, we need to add it to the republish
|
||||
const shouldRepublishWithCodesigning = updatesToPublish.some(update => update.codeSigningInfo);
|
||||
if (shouldRepublishWithCodesigning) {
|
||||
Log.withTick(
|
||||
`The republished update will be signed with the same codesigning as the original update.`
|
||||
);
|
||||
}
|
||||
|
||||
const publishIndicator = ora('Republishing...').start();
|
||||
let updatesRepublished: Awaited<ReturnType<typeof PublishMutation.publishUpdateGroupAsync>>;
|
||||
|
||||
try {
|
||||
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
|
||||
{
|
||||
branchId,
|
||||
runtimeVersion,
|
||||
message: updateMessage,
|
||||
updateInfoGroup: Object.fromEntries(
|
||||
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
|
||||
),
|
||||
gitCommitHash: updatesToPublish[0].gitCommitHash,
|
||||
awaitingCodeSigningInfo: shouldRepublishWithCodesigning,
|
||||
},
|
||||
]);
|
||||
|
||||
if (shouldRepublishWithCodesigning) {
|
||||
const codeSigningByPlatform = Object.fromEntries(
|
||||
updatesToPublish.map(update => [update.platform, update.codeSigningInfo])
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
updatesRepublished.map(async update => {
|
||||
const codeSigning = codeSigningByPlatform[update.platform];
|
||||
if (codeSigning) {
|
||||
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, update.id, codeSigning);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
publishIndicator.succeed('Republished update');
|
||||
} catch (error: any) {
|
||||
publishIndicator.fail('Failed to republish update');
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
return printJsonOnlyOutput(updatesRepublished);
|
||||
}
|
||||
|
||||
const updatesRepublishedByPlatform = Object.fromEntries(
|
||||
updatesRepublished.map(update => [update.platform, update])
|
||||
);
|
||||
|
||||
const updateGroupUrl = getUpdateGroupUrl(
|
||||
(await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name,
|
||||
exp.slug,
|
||||
updatesRepublished[0].group
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: updatesRepublished[0].runtimeVersion },
|
||||
{ label: 'Platform', value: updatesRepublished.map(update => update.platform).join(', ') },
|
||||
{ label: 'Update Group ID', value: updatesRepublished[0].id },
|
||||
...(updatesRepublishedByPlatform.android
|
||||
? [{ label: 'Android update ID', value: updatesRepublishedByPlatform.android.id }]
|
||||
: []),
|
||||
...(updatesRepublishedByPlatform.ios
|
||||
? [{ label: 'iOS update ID', value: updatesRepublishedByPlatform.ios.id }]
|
||||
: []),
|
||||
{ label: 'Message', value: updateMessage },
|
||||
{ label: 'Website link', value: link(updateGroupUrl, { dim: false }) },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
sanitizeFlags(rawFlags: UpdateRepublishRawFlags): UpdateRepublishFlags {
|
||||
const branchName = rawFlags.branch;
|
||||
const channelName = rawFlags.channel;
|
||||
const groupId = rawFlags.group;
|
||||
const nonInteractive = rawFlags['non-interactive'];
|
||||
|
||||
if (nonInteractive && !groupId) {
|
||||
throw new Error('Only --group can be used in non-interactive mode');
|
||||
}
|
||||
if (!groupId && !(branchName || channelName)) {
|
||||
throw new Error(`--channel, --branch, or --group must be specified`);
|
||||
}
|
||||
|
||||
const platform =
|
||||
rawFlags.platform === 'all' ? defaultRepublishPlatforms : ([rawFlags.platform] as Platform[]);
|
||||
|
||||
return {
|
||||
branchName,
|
||||
channelName,
|
||||
groupId,
|
||||
platform,
|
||||
updateMessage: rawFlags.message,
|
||||
json: rawFlags.json ?? false,
|
||||
nonInteractive,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrieve the update group from either the update group id, or select from branch name. */
|
||||
async function getOrAskUpdatesAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
flags: UpdateRepublishFlags
|
||||
): Promise<UpdateToRepublish[]> {
|
||||
if (flags.groupId) {
|
||||
const updateGroups = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
|
||||
groupId: flags.groupId,
|
||||
});
|
||||
|
||||
return updateGroups.map(group => ({
|
||||
...group,
|
||||
groupId: group.group,
|
||||
branchId: group.branch.id,
|
||||
branchName: group.branch.name,
|
||||
}));
|
||||
}
|
||||
|
||||
if (flags.branchName) {
|
||||
return await askUpdatesFromBranchNameAsync(graphqlClient, {
|
||||
...flags,
|
||||
branchName: flags.branchName,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
if (flags.channelName) {
|
||||
return await askUpdatesFromChannelNameAsync(graphqlClient, {
|
||||
...flags,
|
||||
channelName: flags.channelName,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('--channel, --branch, or --group is required');
|
||||
}
|
||||
|
||||
/** Ask the user which update needs to be republished by branch name, this requires interactive mode */
|
||||
async function askUpdatesFromBranchNameAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
branchName,
|
||||
json,
|
||||
nonInteractive,
|
||||
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
|
||||
): Promise<UpdateToRepublish[]> {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
|
||||
const updateGroups = await selectUpdateGroupOnBranchAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName,
|
||||
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
|
||||
});
|
||||
|
||||
return updateGroups.map(group => ({
|
||||
...group,
|
||||
groupId: group.id,
|
||||
branchId: group.branch.id,
|
||||
branchName: group.branch.name,
|
||||
}));
|
||||
}
|
||||
/** Ask the user which update needs to be republished by channel name, this requires interactive mode */
|
||||
async function askUpdatesFromChannelNameAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
channelName,
|
||||
json,
|
||||
nonInteractive,
|
||||
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
|
||||
): Promise<UpdateToRepublish[]> {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
|
||||
const branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
|
||||
|
||||
return await askUpdatesFromBranchNameAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName,
|
||||
json,
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
/** Get or ask the user for the update (group) message for the republish */
|
||||
async function getOrAskUpdateMessageAsync(
|
||||
updates: UpdateToRepublish[],
|
||||
flags: UpdateRepublishFlags
|
||||
): Promise<string> {
|
||||
if (flags.updateMessage) {
|
||||
return sanitizeUpdateMessage(flags.updateMessage);
|
||||
}
|
||||
|
||||
if (flags.nonInteractive || flags.json) {
|
||||
throw new Error('Must supply --message when in non-interactive mode');
|
||||
}
|
||||
|
||||
// This command only uses a single update group to republish, meaning these values are always identical
|
||||
const oldGroupId = updates[0].groupId;
|
||||
const oldUpdateMessage = updates[0].message;
|
||||
|
||||
const { updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: 'Provide an update message.',
|
||||
initial: `Republish "${oldUpdateMessage!}" - group: ${oldGroupId}`,
|
||||
validate: (value: any) => (value ? true : 'Update message may not be empty.'),
|
||||
});
|
||||
|
||||
return sanitizeUpdateMessage(updateMessage);
|
||||
}
|
||||
|
||||
function sanitizeUpdateMessage(updateMessage: string): string {
|
||||
if (updateMessage !== truncateUpdateMessage(updateMessage, 1024)) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit, truncated update message.');
|
||||
return truncateUpdateMessage(updateMessage, 1024);
|
||||
}
|
||||
|
||||
return updateMessage;
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, getUpdateGroupDescriptions } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { formatUpdateGroup, getUpdateGroupDescriptions } from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class UpdateView extends EasCommand {
|
||||
@@ -48,20 +46,9 @@ export default class UpdateView extends EasCommand {
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(updateGroupDescription);
|
||||
} else {
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
Log.log(chalk.bold('Update group:'));
|
||||
Log.log(formatFields([{ label: 'ID', value: updateGroupDescription.group }]));
|
||||
groupTable.push([
|
||||
updateGroupDescription.message,
|
||||
updateGroupDescription.runtimeVersion,
|
||||
updateGroupDescription.group,
|
||||
updateGroupDescription.platforms,
|
||||
]);
|
||||
Log.log(groupTable.toString());
|
||||
|
||||
Log.log(formatUpdateGroup(updateGroupDescription));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import { WebhookType } from '../../graphql/generated';
|
||||
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { formatWebhook } from '../../webhooks/formatWebhook';
|
||||
|
||||
export default class WebhookList extends EasCommand {
|
||||
@@ -17,6 +19,7 @@ export default class WebhookList extends EasCommand {
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
...EasJsonOnlyFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -26,8 +29,12 @@ export default class WebhookList extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { event },
|
||||
flags: { event, json },
|
||||
} = await this.parse(WebhookList);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -48,10 +55,15 @@ export default class WebhookList extends EasCommand {
|
||||
spinner.fail(`There are no webhooks on project ${projectDisplayName}`);
|
||||
} else {
|
||||
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectDisplayName}`);
|
||||
const list = webhooks
|
||||
.map(webhook => formatWebhook(webhook))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
Log.log(`\n${list}`);
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(webhooks);
|
||||
} else {
|
||||
const list = webhooks
|
||||
.map(webhook => formatWebhook(webhook))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
Log.log(`\n${list}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.fail(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppFragment, Role } from '../../graphql/generated';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
export const jester = {
|
||||
export const jester: Actor = {
|
||||
__typename: 'User' as const,
|
||||
id: 'jester-id',
|
||||
username: 'jester',
|
||||
@@ -22,6 +23,7 @@ export const jester = {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
export const jester2 = {
|
||||
@@ -36,6 +38,7 @@ export const jester2 = {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
export const testUsername = jester.username;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Analytics } from '../../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AccountFragment, AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
@@ -16,7 +17,7 @@ export class CreateKeystore {
|
||||
}
|
||||
|
||||
const projectId = ctx.projectId;
|
||||
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, projectId);
|
||||
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, ctx.analytics, projectId);
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(
|
||||
ctx.graphqlClient,
|
||||
this.account,
|
||||
@@ -28,6 +29,7 @@ export class CreateKeystore {
|
||||
|
||||
private async provideOrGenerateAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
|
||||
@@ -36,6 +38,6 @@ export class CreateKeystore {
|
||||
validateKeystore(providedKeystoreWithType);
|
||||
return providedKeystoreWithType;
|
||||
}
|
||||
return await generateRandomKeystoreAsync(graphqlClient, projectId);
|
||||
return await generateRandomKeystoreAsync(graphqlClient, analytics, projectId);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Analytics, BuildEvent } from '../../../analytics/events';
|
||||
import { Analytics, BuildEvent } from '../../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import fetch from '../../../fetch';
|
||||
import { AndroidKeystoreType } from '../../../graphql/generated';
|
||||
@@ -38,6 +38,7 @@ enum KeystoreCreateStep {
|
||||
|
||||
export async function generateRandomKeystoreAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
const keystoreData: KeystoreParams = {
|
||||
@@ -45,15 +46,16 @@ export async function generateRandomKeystoreAsync(
|
||||
keyPassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyAlias: crypto.randomBytes(16).toString('hex'),
|
||||
};
|
||||
return await createKeystoreAsync(graphqlClient, keystoreData, projectId);
|
||||
return await createKeystoreAsync(graphqlClient, analytics, keystoreData, projectId);
|
||||
}
|
||||
|
||||
async function createKeystoreAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
keystoreParams: KeystoreParams,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Attempt,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
@@ -74,14 +76,14 @@ async function createKeystoreAsync(
|
||||
showKeytoolDetectionMsg: !localAttempt,
|
||||
});
|
||||
}
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Success,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
return keystore;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Fail,
|
||||
reason: error.message,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Env } from '@expo/eas-build-job';
|
||||
import { EasJson } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { Analytics } from '../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
@@ -26,6 +27,7 @@ export class CredentialsContext {
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
public readonly graphqlClient: ExpoGraphqlClient;
|
||||
public readonly analytics: Analytics;
|
||||
public readonly easJsonCliConfig?: EasJson['cli'];
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
@@ -41,6 +43,7 @@ export class CredentialsContext {
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
@@ -48,6 +51,7 @@ export class CredentialsContext {
|
||||
this.projectDir = options.projectDir;
|
||||
this.user = options.user;
|
||||
this.graphqlClient = options.graphqlClient;
|
||||
this.analytics = options.analytics;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
this.projectInfo = options.projectInfo;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ async function generateAscApiKeyAsync(
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, {
|
||||
nickname: getAscApiKeyName(purpose),
|
||||
});
|
||||
return await getMinimalAscApiKeyAsync(ascApiKey);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ProfileType } from '@expo/app-store';
|
||||
|
||||
import { Analytics } from '../../../analytics/AnalyticsManager';
|
||||
import Log from '../../../log';
|
||||
import {
|
||||
AscApiKey,
|
||||
@@ -192,9 +193,12 @@ export default class AppStoreApi {
|
||||
return await getAscApiKeyAsync(userCtx, keyId);
|
||||
}
|
||||
|
||||
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
|
||||
public async createAscApiKeyAsync(
|
||||
analytics: Analytics,
|
||||
{ nickname }: { nickname: string }
|
||||
): Promise<AscApiKey> {
|
||||
const userCtx = await this.ensureUserAuthenticatedAsync();
|
||||
return await createAscApiKeyAsync(userCtx, { nickname });
|
||||
return await createAscApiKeyAsync(analytics, userCtx, { nickname });
|
||||
}
|
||||
|
||||
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApiKey, ApiKeyType, UnexpectedResponse, UserRole } from '@expo/apple-utils';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { Analytics } from '../../../../analytics/AnalyticsManager';
|
||||
import {
|
||||
createAscApiKeyAsync,
|
||||
downloadWithRetryAsync,
|
||||
@@ -72,10 +74,13 @@ test(`getAscApiKeyAsync`, async () => {
|
||||
});
|
||||
|
||||
test(`createAscApiKeyAsync`, async () => {
|
||||
const analytics = instance(mock<Analytics>());
|
||||
const mockRequestContext = {};
|
||||
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
|
||||
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
|
||||
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
|
||||
const result = await createAscApiKeyAsync(analytics, mockAuthCtx, {
|
||||
nickname: 'test-name',
|
||||
});
|
||||
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
|
||||
nickname: 'test-name',
|
||||
allAppsVisible: true,
|
||||
@@ -99,7 +104,7 @@ test(`revokeAscApiKeyAsync`, async () => {
|
||||
|
||||
test(`downloadWithRetryAsync`, async () => {
|
||||
const cacheFailureMessage = `The specified resource does not exist - There is no resource of type 'apiKeys' with id 'TEST-ID'`;
|
||||
|
||||
const analytics = instance(mock<Analytics>());
|
||||
// complete failure
|
||||
const mockApiKeyWithDownloadError = {
|
||||
...mockApiKey,
|
||||
@@ -108,7 +113,7 @@ test(`downloadWithRetryAsync`, async () => {
|
||||
}),
|
||||
} as unknown as ApiKey;
|
||||
await expect(
|
||||
downloadWithRetryAsync(mockApiKeyWithDownloadError, { minTimeout: 1 }) // stay within jest timeout window
|
||||
downloadWithRetryAsync(analytics, mockApiKeyWithDownloadError, { minTimeout: 1 }) // stay within jest timeout window
|
||||
).rejects.toThrowError(cacheFailureMessage);
|
||||
// expect to try once and retry 3 times = 4 total
|
||||
expect(mockApiKeyWithDownloadError.downloadAsync as jest.Mock).toBeCalledTimes(7);
|
||||
@@ -119,15 +124,23 @@ test(`downloadWithRetryAsync`, async () => {
|
||||
(mockApiKeyWithOneTimeDownloadError.downloadAsync as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new UnexpectedResponse(cacheFailureMessage);
|
||||
});
|
||||
const keyP8AfterOneFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
|
||||
minTimeout: 1,
|
||||
});
|
||||
const keyP8AfterOneFailure = await downloadWithRetryAsync(
|
||||
analytics,
|
||||
mockApiKeyWithOneTimeDownloadError,
|
||||
{
|
||||
minTimeout: 1,
|
||||
}
|
||||
);
|
||||
expect(keyP8AfterOneFailure).toBe('super secret');
|
||||
|
||||
// successful case
|
||||
(mockApiKey.downloadAsync as jest.Mock).mockClear();
|
||||
const keyP8NoFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
|
||||
minTimeout: 1,
|
||||
});
|
||||
const keyP8NoFailure = await downloadWithRetryAsync(
|
||||
analytics,
|
||||
mockApiKeyWithOneTimeDownloadError,
|
||||
{
|
||||
minTimeout: 1,
|
||||
}
|
||||
);
|
||||
expect(keyP8NoFailure).toBe('super secret');
|
||||
});
|
||||
+1
-1
@@ -254,7 +254,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
]);
|
||||
|
||||
expect(results.enabled).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
"HealthKit",
|
||||
"Apple Pay Payment Processing",
|
||||
"iCloud",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
|
||||
import promiseRetry from 'promise-retry';
|
||||
|
||||
import { Analytics, SubmissionEvent } from '../../../analytics/events';
|
||||
import { Analytics, SubmissionEvent } from '../../../analytics/AnalyticsManager';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
|
||||
@@ -58,6 +58,7 @@ export async function getAscApiKeyAsync(
|
||||
* is available for download.
|
||||
* */
|
||||
export async function downloadWithRetryAsync(
|
||||
analytics: Analytics,
|
||||
key: ApiKey,
|
||||
{
|
||||
minTimeout = 1000,
|
||||
@@ -81,7 +82,7 @@ export async function downloadWithRetryAsync(
|
||||
Log.log(
|
||||
`Received an unexpected response from Apple, retrying in ${secondsToRetry} seconds...`
|
||||
);
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_RETRY, {
|
||||
analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_RETRY, {
|
||||
errorName: e.name,
|
||||
reason: e.message,
|
||||
retry: number,
|
||||
@@ -97,7 +98,7 @@ export async function downloadWithRetryAsync(
|
||||
minTimeout,
|
||||
}
|
||||
);
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_SUCCESS, {});
|
||||
analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_SUCCESS, {});
|
||||
return keyP8;
|
||||
} catch (e: any) {
|
||||
if (
|
||||
@@ -110,7 +111,7 @@ export async function downloadWithRetryAsync(
|
||||
)}`
|
||||
);
|
||||
}
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_FAIL, {
|
||||
analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_FAIL, {
|
||||
errorName: e.name,
|
||||
reason: e.message,
|
||||
});
|
||||
@@ -123,6 +124,7 @@ export async function downloadWithRetryAsync(
|
||||
* **Does not support App Store Connect API (CI).**
|
||||
*/
|
||||
export async function createAscApiKeyAsync(
|
||||
analytics: Analytics,
|
||||
userAuthCtx: UserAuthCtx,
|
||||
{
|
||||
nickname,
|
||||
@@ -140,7 +142,7 @@ export async function createAscApiKeyAsync(
|
||||
roles: roles ?? [UserRole.ADMIN],
|
||||
keyType: keyType ?? ApiKeyType.PUBLIC_API,
|
||||
});
|
||||
const keyP8 = await downloadWithRetryAsync(key);
|
||||
const keyP8 = await downloadWithRetryAsync(analytics, key);
|
||||
if (!keyP8) {
|
||||
const { nickname, roles } = key.attributes;
|
||||
const humanReadableKey = `App Store Connect Key '${nickname}' (${
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import terminalLink from 'terminal-link';
|
||||
// @ts-ignore
|
||||
import TurndownService from 'turndown';
|
||||
import wrapAnsi from 'wrap-ansi';
|
||||
|
||||
import { link } from '../../../log';
|
||||
|
||||
const turndownServices: Record<string, any> = {};
|
||||
|
||||
function getService(rootUrl: string): TurndownService {
|
||||
@@ -44,10 +45,7 @@ function getService(rootUrl: string): TurndownService {
|
||||
if (href.startsWith('/')) {
|
||||
href = `${rootUrl}${href}`;
|
||||
}
|
||||
if (terminalLink.isSupported) {
|
||||
return chalk.cyan(terminalLink(content, href));
|
||||
}
|
||||
return `${chalk.cyan(content)} (${chalk.underline(href)})`;
|
||||
return chalk.cyan(link(href, { text: content }));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../../log';
|
||||
import { pressAnyKeyToContinueAsync } from '../../prompts';
|
||||
@@ -7,7 +9,9 @@ import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
export interface Action<T = void> {
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
projectInfo: CredentialsContextProjectInfo | null;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
runAsync(ctx: CredentialsContext): Promise<T>;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
displayAndroidAppCredentials,
|
||||
displayEmptyAndroidCredentials,
|
||||
} from '../android/utils/printCredentials';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
import { ActionInfo, AndroidActionType, Scope } from './Actions';
|
||||
import {
|
||||
buildCredentialsActions,
|
||||
@@ -52,11 +52,19 @@ export class ManageAndroid {
|
||||
const buildProfile = hasProjectContext
|
||||
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
|
||||
: null;
|
||||
let projectInfo: CredentialsContextProjectInfo | null = null;
|
||||
if (hasProjectContext) {
|
||||
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
|
||||
env: buildProfile?.env,
|
||||
});
|
||||
projectInfo = { exp, projectId };
|
||||
}
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
projectInfo: this.callingAction.projectInfo,
|
||||
projectInfo,
|
||||
user: this.callingAction.actor,
|
||||
graphqlClient: this.callingAction.graphqlClient,
|
||||
analytics: this.callingAction.analytics,
|
||||
env: buildProfile?.env,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { ensureActorHasPrimaryAccount } from '../../user/actions';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
selectAscApiKeysFromAccountAsync,
|
||||
@@ -61,11 +61,21 @@ export class ManageIos {
|
||||
const buildProfile = this.callingAction.projectInfo
|
||||
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
|
||||
: null;
|
||||
|
||||
let projectInfo: CredentialsContextProjectInfo | null = null;
|
||||
if (this.callingAction.projectInfo) {
|
||||
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
|
||||
env: buildProfile?.env,
|
||||
});
|
||||
projectInfo = { exp, projectId };
|
||||
}
|
||||
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
projectInfo: this.callingAction.projectInfo,
|
||||
projectInfo,
|
||||
user: this.callingAction.actor,
|
||||
graphqlClient: this.callingAction.graphqlClient,
|
||||
analytics: this.callingAction.analytics,
|
||||
env: buildProfile?.env,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { selectPlatformAsync } from '../../platform';
|
||||
import { Actor } from '../../user/User';
|
||||
@@ -9,7 +11,9 @@ export class SelectPlatform {
|
||||
constructor(
|
||||
public readonly actor: Actor,
|
||||
public readonly graphqlClient: ExpoGraphqlClient,
|
||||
public readonly analytics: Analytics,
|
||||
public readonly projectInfo: CredentialsContextProjectInfo | null,
|
||||
public readonly getDynamicProjectConfigAsync: DynamicConfigContextFn,
|
||||
private readonly flagPlatform?: string
|
||||
) {}
|
||||
|
||||
|
||||
Loaded 100 of 218 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user