Compare commits

..
Author SHA1 Message Date
Will Schurman f16e87c0a7 [24/n][eas-cli] Move graphql and session into context 2022-10-11 13:45:08 -07:00
188 changed files with 4427 additions and 9196 deletions

No files matched your search

-3
View File
@@ -1,5 +1,2 @@
* text eol=lf
*.png binary
*.jpg binary
**/yarn.lock linguist-generated
-3
View File
@@ -32,9 +32,6 @@ 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:
+30 -125
View File
@@ -12,101 +12,6 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 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))
## [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
- Remove EAS Submit errors from codebase. The errors have been moved to the server side. ([#1443](https://github.com/expo/eas-cli/pull/1443) by [@dsokal](https://github.com/dsokal))
## [2.4.0](https://github.com/expo/eas-cli/releases/tag/v2.4.0) - 2022-10-14
### 🎉 New features
- Handle more common errors for EAS Submit. ([#1440](https://github.com/expo/eas-cli/pull/1440) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Move graphql and session into command context. ([#1435](https://github.com/expo/eas-cli/pull/1435), [#1436](https://github.com/expo/eas-cli/pull/1436) by [@wschurman](https://github.com/wschurman))
## [2.3.0](https://github.com/expo/eas-cli/releases/tag/v2.3.0) - 2022-10-10
### 🎉 New features
@@ -147,10 +52,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))
@@ -326,7 +231,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))
@@ -363,13 +268,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
@@ -379,7 +284,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
@@ -550,7 +455,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
@@ -602,7 +507,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. ([#854](https://github.com/expo/eas-cli/pull/854) by [@jonsamp](https://github.com/jonsamp))
- 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 `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.
@@ -648,7 +553,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
@@ -684,7 +589,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/pull/775) by [@EvanBacon](https://github.com/EvanBacon))
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/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
@@ -782,7 +687,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), [#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) and [#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))
@@ -814,7 +719,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
@@ -860,7 +765,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
@@ -872,7 +777,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
@@ -1006,7 +911,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. ([#588](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
- 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 detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
@@ -1086,7 +991,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. ([#535](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. ([#532](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
@@ -1106,7 +1011,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/520) 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/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
@@ -1120,7 +1025,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), [#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) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
@@ -1200,15 +1105,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))
@@ -1222,7 +1127,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Android credentials setup now on Graphql API. ([#427](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
@@ -1254,7 +1159,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))
@@ -1265,7 +1170,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
@@ -1292,7 +1197,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))
@@ -1353,7 +1258,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
@@ -1378,7 +1283,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/311) by [@barthap](https://github.com/barthap))
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1462,7 +1367,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
@@ -1489,7 +1394,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
View File
@@ -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 @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/update @kgc00 @wschurman
packages/eas-cli/src/commands/update @kgc00 @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
+1 -1
View File
@@ -1,4 +1,4 @@
## Releasing a new version
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`.
1. Run `yarn release` in the repository root folder.
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.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.7.1"
"version": "2.3.0"
}
+11 -12
View File
@@ -8,29 +8,28 @@
"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 --stream --parallel",
"start": "lerna run watch --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"lint-changelog": "./scripts/bin/run lint-changelog",
"release": "./scripts/bin/run release",
"release": "./scripts/bin/run update-local-plugin && lerna version --exact",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "29.2.0",
"@types/node": "18.11.5",
"eslint": "8.26.0",
"@types/jest": "28.1.6",
"@types/node": "18.6.5",
"eslint": "8.21.0",
"eslint-config-universe": "11.1.0",
"eslint-plugin-async-protect": "2.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "29.2.2",
"jest-watch-typeahead": "2.2.0",
"lerna": "6.0.1",
"oclif": "3.2.22",
"jest": "28.1.3",
"jest-watch-typeahead": "2.0.0",
"lerna": "4.0.0",
"oclif": "3.1.2",
"prettier": "2.7.1",
"ts-jest": "29.0.3",
"typescript": "4.8.4"
"ts-jest": "28.0.7",
"typescript": "4.7.4"
},
"volta": {
"node": "18.6.0",
+50 -141
View File
@@ -89,18 +89,14 @@ 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)
@@ -129,7 +125,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -146,7 +142,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -163,7 +159,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -177,7 +173,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -206,7 +202,7 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.4/src/commands/autocomplete/index.ts)_
_See code: [@expo/plugin-autocomplete](https://github.com/expo/plugin-autocomplete/blob/v1.4.0/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
@@ -227,7 +223,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -248,7 +244,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -268,7 +264,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -288,7 +284,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -311,7 +307,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -341,7 +337,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -358,7 +354,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -375,7 +371,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -410,7 +406,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.7.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -447,7 +443,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -494,7 +490,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.7.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/v2.3.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -513,7 +509,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.7.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/v2.3.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -530,7 +526,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -551,7 +547,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -573,7 +569,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -593,7 +589,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -616,7 +612,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -635,7 +631,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -652,7 +648,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -666,7 +662,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.7.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -686,7 +682,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -707,7 +703,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -721,7 +717,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -735,7 +731,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -755,7 +751,7 @@ DESCRIPTION
display help for eas-cli
```
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.16/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.3.0/src/commands/help.ts)_
## `eas init`
@@ -807,24 +803,6 @@ 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/v2.7.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
generate the local store configuration from the app stores
@@ -840,7 +818,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.7.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -857,7 +835,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.7.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -871,7 +849,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -885,7 +863,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -907,55 +885,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.7.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/v2.7.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/v2.7.1/packages/eas-cli/src/commands/run/run.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -978,7 +908,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.7.1/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -996,7 +926,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1010,27 +940,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.7.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/v2.7.1/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -1060,7 +970,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1090,7 +1000,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1098,17 +1008,16 @@ configure the project to support EAS Update
```
USAGE
$ eas update:configure [-p android|ios|all] [--non-interactive]
$ eas update:configure [-p android|ios|all]
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.7.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1129,7 +1038,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.7.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1151,7 +1060,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -1171,7 +1080,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1192,7 +1101,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1212,7 +1121,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1229,7 +1138,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1251,7 +1160,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1268,7 +1177,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.3.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
-2
View File
@@ -5,8 +5,6 @@ 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:
+52 -607
View File
@@ -2274,18 +2274,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tokenEndpoint",
"description": "",
@@ -2454,18 +2442,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tokenEndpoint",
"description": "",
@@ -2758,18 +2734,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tokenEndpoint",
"description": "",
@@ -11949,18 +11913,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "actualResourceClass",
"description": "The actual resource class of the builder assigned to the build job",
"args": [],
"type": {
"kind": "ENUM",
"name": "BuildResourceClass",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appBuildVersion",
"description": "",
@@ -12439,16 +12391,12 @@
},
{
"name": "resourceClass",
"description": "The builder resource class requested by the developer",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "BuildResourceClass",
"ofType": null
}
"kind": "ENUM",
"name": "BuildResourceClass",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
@@ -15458,18 +15406,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "SubmissionArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": "",
@@ -15705,22 +15641,6 @@
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodeIdentifier",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
@@ -15792,18 +15712,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "SubmissionArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": "",
@@ -16172,60 +16080,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteIosAppBuildCredentialsResult",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteIosAppCredentialsResult",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeleteRobotResult",
@@ -17413,35 +17267,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "GitHubAppEnvironment",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DEVELOPMENT",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRODUCTION",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STAGING",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubAppInstallation",
@@ -17459,7 +17284,7 @@
"name": "Int",
"ofType": null
},
"defaultValue": null,
"defaultValue": "1",
"isDeprecated": false,
"deprecationReason": null
},
@@ -17471,7 +17296,7 @@
"name": "Int",
"ofType": null
},
"defaultValue": null,
"defaultValue": "50",
"isDeprecated": false,
"deprecationReason": null
}
@@ -17480,9 +17305,17 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubRepositoryPaginationResult",
"ofType": null
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
}
},
"isDeprecated": false,
@@ -17635,22 +17468,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodeId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "owner",
"description": "",
@@ -17899,71 +17716,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environment",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "GitHubAppEnvironment",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "installation",
"description": "",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "searchRepositories",
"description": "",
@@ -17984,6 +17736,30 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "page",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "1",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "perPage",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "50",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "query",
"description": "",
@@ -18005,9 +17781,17 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubRepositoryPaginationResult",
"ofType": null
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
}
},
"isDeprecated": false,
@@ -18099,22 +17883,6 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodeIdentifier",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -18127,18 +17895,6 @@
"name": "GitHubRepositoryMetadata",
"description": "",
"fields": [
{
"name": "defaultBranch",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "githubRepoDescription",
"description": "",
@@ -18231,18 +17987,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "openGraphImageUrl",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "private",
"description": "",
@@ -18417,57 +18161,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubRepositoryPaginationResult",
"description": "",
"fields": [
{
"name": "repositories",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GitHubAppInstallationAccessibleRepository",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "totalCount",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubRepositorySettings",
@@ -19781,39 +19474,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteIosAppBuildCredentials",
"description": "Disassociate the build credentials from an iOS app",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteIosAppBuildCredentialsResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setDistributionCertificate",
"description": "Set the distribution certificate to be used for an iOS app",
@@ -20227,39 +19887,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deleteIosAppCredentials",
"description": "Delete a set of credentials for an iOS app",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeleteIosAppCredentialsResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setAppStoreConnectApiKeyForSubmissions",
"description": "Set the App Store Connect Api Key to be used for submitting an iOS app",
@@ -20364,50 +19991,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "IosAppCredentialsQuery",
"description": "",
"fields": [
{
"name": "byId",
"description": "",
"args": [
{
"name": "iosAppCredentialsId",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "IosAppCredentials",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "IosBuildType",
@@ -22932,12 +22515,6 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "GCS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "S3",
"description": "",
@@ -23267,18 +22844,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitCommitHash",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "",
@@ -24829,22 +24394,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iosAppCredentials",
"description": "Top-level query object for querying IosAppCredentials.",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "IosAppCredentialsQuery",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "me",
"description": "If authenticated as a typical end user, this is the appropriate top-level\nquery object",
@@ -26895,86 +26444,6 @@
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "SubmissionArchiveSourceInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "bucketKey",
"description": "Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE or GCS_SUBMIT_ARCHIVE",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SubmissionArchiveSourceType",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "Required if the archive source type is URL",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "SubmissionArchiveSourceType",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "GCS_BUILD_APPLICATION_ARCHIVE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GCS_SUBMIT_ARCHIVE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "URL",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SubmissionError",
@@ -27615,18 +27084,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitCommitHash",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "group",
"description": "",
@@ -28765,12 +28222,6 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "EAS_BUILD_GCS_PROJECT_SOURCES",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "EAS_BUILD_PROJECT_SOURCES",
"description": "",
@@ -28782,12 +28233,6 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "EAS_SUBMIT_GCS_APP_ARCHIVE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
+29 -36
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.7.1",
"version": "2.3.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -9,53 +9,50 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.34",
"@expo/code-signing-certificates": "0.0.3",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.97",
"@expo/eas-json": "2.7.1",
"@expo/eas-build-job": "0.2.94",
"@expo/eas-json": "2.2.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-help": "5.1.16",
"@expo/plugin-warn-if-update-available": "2.4.0",
"@expo/plugin-autocomplete": "1.4.0",
"@expo/plugin-help": "5.3.0",
"@expo/plugin-warn-if-update-available": "2.3.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.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",
"@oclif/core": "1.13.10",
"@urql/core": "2.6.1",
"@urql/exchange-retry": "0.3.3",
"ajv": "^6.12.6",
"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.12",
"fast-glob": "3.2.11",
"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.6.0",
"graphql": "16.5.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.1.0",
"joi": "17.6.4",
"jks-js": "1.0.2",
"joi": "17.6.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
@@ -66,55 +63,51 @@
"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.8",
"semver": "7.3.7",
"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": "9.0.0",
"uuid": "8.3.2",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "2.13.7",
"@graphql-codegen/cli": "2.11.5",
"@graphql-codegen/introspection": "2.2.1",
"@graphql-codegen/typescript": "2.8.0",
"@graphql-codegen/typescript-operations": "2.5.5",
"@graphql-codegen/typescript": "2.7.3",
"@graphql-codegen/typescript-operations": "2.5.3",
"@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": "3.0.1",
"@types/mime": "2.0.3",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.3.0",
"@types/pngjs": "6.0.1",
"@types/node-forge": "1.0.4",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.4.1",
"@types/semver": "7.3.12",
"@types/tar": "6.1.3",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.10",
"@types/tar": "6.1.2",
"@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": "4.0.0",
"ts-deepmerge": "2.0.5",
"ts-mockito": "2.6.1",
"ts-node": "10.9.1",
"typescript": "4.8.4"
"typescript": "4.7.4"
},
"engines": {
"node": ">=14.0.0"
@@ -136,7 +129,7 @@
"bin": "eas",
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@expo/plugin-autocomplete",
"@expo/plugin-help",
"@expo/plugin-warn-if-update-available"
],
+1 -1
View File
@@ -778,7 +778,7 @@
},
"properties": {
"configVersion": {
"enum": [0],
"const": 0,
"description": "The EAS metadata store configuration version"
},
"apple": {
@@ -0,0 +1,17 @@
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);
});
});
@@ -1,232 +0,0 @@
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 },
};
}
}
+11 -16
View File
@@ -1,28 +1,23 @@
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
export async function withAnalyticsAsync<Result>(
analytics: Analytics,
fn: () => Promise<Result>,
{
attemptEvent,
successEvent,
failureEvent,
properties,
}: {
attemptEvent: AnalyticsEvent;
successEvent: AnalyticsEvent;
failureEvent: AnalyticsEvent;
properties: AnalyticsEventProperties;
analytics: {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
trackingCtx: TrackingContext;
}
): Promise<Result> {
try {
analytics.logEvent(attemptEvent, properties);
const result = await fn();
analytics.logEvent(successEvent, properties);
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error: any) {
analytics.logEvent(failureEvent, {
...properties,
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
});
throw error;
+60
View File
@@ -0,0 +1,60 @@
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);
}
}
@@ -0,0 +1,129 @@
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
}
+29 -77
View File
@@ -1,24 +1,17 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import CliTable from 'cli-table3';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
UpdateBranchFragment,
} from '../graphql/generated';
import { UpdateBranchFragment } from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { BranchNotFoundError } from './utils';
export const BRANCHES_LIMIT = 50;
@@ -109,75 +102,34 @@ 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(
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 }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
const { id } = updateBranch;
return { branchId: id };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id };
} else {
throw error;
}
Log.log(table.toString());
}
}
@@ -45,15 +45,15 @@ describe(selectBuildToCancelAsync.name, () => {
});
it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
const graphqlClient = instance(mock<ExpoGraphqlClient>());
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
});
it('returns build id when confirmation is confirmed', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
const graphqlClient = instance(mock<ExpoGraphqlClient>());
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

@@ -1,142 +0,0 @@
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();
});
});
});
+4 -7
View File
@@ -3,6 +3,7 @@ import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
@@ -24,11 +25,7 @@ import {
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
@@ -57,7 +54,6 @@ 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);
@@ -106,6 +102,7 @@ export async function prepareAndroidBuildAsync(
return await prepareJobAsync(ctx, jobData);
},
sendBuildRequestAsync: async (
graphqlClient: ExpoGraphqlClient,
appId: string,
job: Android.Job,
metadata: Metadata,
@@ -113,7 +110,7 @@ export async function prepareAndroidBuildAsync(
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
return await BuildMutation.createAndroidBuildAsync(graphqlClient, {
appId,
job: graphqlJob,
metadata: graphqlMetadata,
+16 -21
View File
@@ -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 { uploadFileAtPathToGCSAsync } from '../uploads';
import { uploadFileAtPathToS3Async } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -56,6 +56,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
job: TJob,
metadata: Metadata,
@@ -78,26 +79,21 @@ 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,
properties: ctx.analyticsEventProperties,
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,
}
);
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
@@ -113,7 +109,7 @@ export async function prepareBuildRequestForPlatformAsync<
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.GCS,
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(ctx),
} as const);
@@ -197,7 +193,6 @@ async function uploadProjectAsync<TPlatform extends Platform>(
let projectTarballPath;
try {
return await withAnalyticsAsync(
ctx.analytics,
async () => {
Log.newLine();
Log.log(
@@ -219,9 +214,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
projectTarballPath = projectTarball.path;
const bucketKey = await uploadFileAtPathToGCSAsync(
const { bucketKey } = await uploadFileAtPathToS3Async(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectSources,
UploadSessionType.EasBuildProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
@@ -238,7 +233,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
properties: ctx.analyticsEventProperties,
trackingCtx: ctx.trackingCtx,
}
);
} finally {
@@ -256,13 +251,13 @@ 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`);
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.graphqlClient,
ctx.projectId,
job,
metadata,
@@ -276,7 +271,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
properties: ctx.analyticsEventProperties,
trackingCtx: ctx.trackingCtx,
}
);
}
+2 -3
View File
@@ -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 { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
import { TrackingContext } from '../analytics/common';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types';
@@ -46,10 +46,9 @@ export interface BuildContext<T extends Platform> {
projectId: string;
projectName: string;
message?: string;
analyticsEventProperties: AnalyticsEventProperties;
trackingCtx: TrackingContext;
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
workflow: Workflow;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
+6 -10
View File
@@ -5,7 +5,8 @@ import getenv from 'getenv';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
@@ -32,7 +33,6 @@ export async function createBuildContextAsync<T extends Platform>({
message,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}: {
buildProfileName: string;
@@ -48,7 +48,6 @@ 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 });
@@ -65,7 +64,6 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir,
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
easJsonCliConfig,
});
@@ -75,10 +73,9 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir,
buildProfile,
});
const analyticsEventProperties = {
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
...(accountId && { account_id: accountId }),
project_id: projectId,
project_type: workflow,
@@ -86,7 +83,7 @@ export async function createBuildContextAsync<T extends Platform>({
no_wait: noWait,
run_from_ci: runFromCI,
};
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
const commonContext: CommonContext<T> = {
accountName: account.name,
@@ -104,10 +101,9 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir,
projectId,
projectName,
analyticsEventProperties,
trackingCtx,
user: actor,
graphqlClient,
analytics,
workflow,
message,
runFromCI,
@@ -135,7 +131,7 @@ function getDevClientEventProperties({
platform: Platform;
projectDir: string;
buildProfile: BuildProfile;
}): Partial<AnalyticsEventProperties> {
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
-5
View File
@@ -16,11 +16,6 @@ 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,
+4 -7
View File
@@ -1,6 +1,7 @@
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
@@ -10,11 +11,7 @@ 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,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
@@ -32,7 +29,6 @@ export async function createIosContextAsync(
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
@@ -98,6 +94,7 @@ export async function prepareIosBuildAsync(
});
},
sendBuildRequestAsync: async (
graphqlClient: ExpoGraphqlClient,
appId: string,
job: Ios.Job,
metadata: Metadata,
@@ -105,7 +102,7 @@ export async function prepareIosBuildAsync(
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
return await BuildMutation.createIosBuildAsync(graphqlClient, {
appId,
job: graphqlJob,
metadata: graphqlMetadata,
+1 -1
View File
@@ -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.119';
const PLUGIN_PACKAGE_VERSION = '0.0.110';
export interface LocalBuildOptions {
enable: boolean;
+1 -1
View File
@@ -30,7 +30,7 @@ export async function collectMetadataAsync<T extends Platform>(
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata: Metadata = {
trackingContext: ctx.analyticsEventProperties,
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
workflow: ctx.workflow,
-65
View File
@@ -5,8 +5,6 @@ import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { appPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
@@ -55,69 +53,6 @@ export async function listAndRenderBuildsOnAppAsync(
}
}
export async function listAndSelectBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
projectDisplayName,
filter,
queryOptions,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
queryOptions: PaginatedQueryOptions;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: queryOptions.limit ?? BUILDS_LIMIT,
offset: queryOptions.offset,
filter,
});
if (builds.length === 0) {
throw new Error('Found no builds matching the provided criteria.');
}
const { selectedSimulatorBuild } = await promptAsync({
type: 'select',
message: `Select simulator build to run for ${projectDisplayName} app`,
name: 'selectedSimulatorBuild',
choices: builds.map(build => ({
title: `id: ${build.id}, platform: ${appPlatformDisplayNames[build.platform]}, version: ${
build.appVersion
}, build number: ${build.appBuildVersion}`,
value: build,
})),
});
return selectedSimulatorBuild;
}
export async function getLatestBuildAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
filter,
}: {
projectId: string;
filter?: BuildFilter;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: 1,
offset: 0,
filter,
});
if (builds.length === 0) {
throw new Error('Found no build matching the provided criteria.');
}
return builds[0];
}
function renderPageOfBuilds({
builds,
projectDisplayName,
@@ -10,7 +10,6 @@ import {
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 {
@@ -117,7 +116,6 @@ function resolveResourceClass(
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
projectDir: string,
flags: BuildFlags,
actor: Actor,
@@ -171,7 +169,6 @@ export async function runBuildAndSubmitAsync(
easJsonCliConfig,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
});
if (maybeBuild) {
@@ -274,7 +271,6 @@ async function prepareAndStartBuildAsync({
easJsonCliConfig,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}: {
projectDir: string;
@@ -285,7 +281,6 @@ 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({
@@ -302,7 +297,6 @@ async function prepareAndStartBuildAsync({
message: flags.message,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
});
@@ -384,7 +378,6 @@ 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,
});
-137
View File
@@ -1,11 +1,8 @@
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';
@@ -53,137 +50,3 @@ 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`);
}
}
+12 -71
View File
@@ -1,12 +1,8 @@
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';
@@ -157,13 +153,14 @@ 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`);
}
}
}
}
@@ -177,8 +174,13 @@ 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);
}
}
@@ -201,64 +203,3 @@ 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;
}
}
}
+26 -31
View File
@@ -1,13 +1,9 @@
import assert from 'assert';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import {
FormattedBranchDescription,
formatBranch,
getUpdateGroupDescriptionsWithBranch,
} from '../update/utils';
import { UPDATE_COLUMNS, getUpdateGroupDescriptionsWithBranch } from '../update/utils';
export type BranchMapping = {
version: number;
@@ -79,39 +75,38 @@ 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);
const branchDescription = channel.updateBranches.flatMap(branch => {
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
for (const currentBranch of channel.updateBranches) {
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(
currentBranch.updateGroups
);
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;
}
const isRolloutBranch = isRollout && rolloutBranchIds.includes(currentBranch.id);
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === currentBranch.id;
return updateGroupWithBranchDescriptions.map(
({ branch, ...updateGroup }): FormattedBranchDescription => ({
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
updateChannelsTable.push([
branch,
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`)
);
...(isRolloutBranch
? [isBaseBranch ? `${rolloutPercent! * 100}%` : `${(1 - rolloutPercent!) * 100}%`]
: []),
message,
runtimeVersion,
group,
platforms,
]);
});
}
Log.log(updateChannelsTable.toString());
}
+21 -46
View File
@@ -1,13 +1,12 @@
import { Command } from '@oclif/core';
import nullthrows from 'nullthrows';
import {
AnalyticsWithOrchestration,
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
AnalyticsEvent,
flushAsync as flushAnalyticsAsync,
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
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';
@@ -77,19 +76,10 @@ 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(),
},
};
/**
@@ -116,52 +106,37 @@ export default abstract class EasCommand extends Command {
): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition;
// 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,
}),
]);
}
const contextValuePairs = await Promise.all(
Object.keys(contextDefinition).map(async contextKey => {
return [
contextKey,
await contextDefinition[contextKey].getValueAsync({
nonInteractive,
sessionManager: this.sessionManager,
}),
];
})
);
return Object.fromEntries(contextValuePairs);
}
/**
* The user session manager. Responsible for coordinating all user session related state.
* If needed in a subclass, use the SessionManager ContextOption.
* If needed in a subclass, SessionManager ContextOption.
*/
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);
}
private readonly sessionManager = new SessionManager();
protected abstract runAsync(): Promise<any>;
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
this.analyticsInternal = await createAnalyticsAsync();
this.sessionManagerInternal = new SessionManager(this.analytics);
await initAnalyticsAsync();
// this is needed for logEvent call below as it identifies the user in the analytics system
// if possible
await this.sessionManager.getUserAsync();
this.analytics.logEvent(CommandEvent.ACTION, {
logEvent(AnalyticsEvent.ACTION, {
// id is assigned by oclif in constructor based on the filepath:
// commands/submit === submit, commands/build/list === build:list
action: `eas ${this.id}`,
@@ -172,7 +147,7 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
override async finally(err: Error): Promise<any> {
await this.analytics.flushAsync();
await flushAnalyticsAsync();
return super.finally(err);
}
}
@@ -1,14 +1,16 @@
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import SessionManager from '../../user/SessionManager';
import EasCommand from '../EasCommand';
jest.mock('../../user/User');
jest.mock('../../user/SessionManager');
jest.mock('../../analytics/AnalyticsManager', () => {
const { CommandEvent } = jest.requireActual('../../analytics/AnalyticsManager');
jest.mock('../../analytics/rudderstackClient', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
return {
CommandEvent,
createAnalyticsAsync: jest.fn(),
AnalyticsEvent,
logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
};
});
@@ -23,16 +25,10 @@ afterAll(() => {
process.argv = originalProcessArgv;
});
const analytics: AnalyticsWithOrchestration = {
logEvent: jest.fn((): void => {}),
setActor: jest.fn((): void => {}),
flushAsync: jest.fn(async (): Promise<void> => {}),
};
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
});
const createTestEasCommand = (): typeof EasCommand => {
@@ -65,21 +61,21 @@ describe(EasCommand.name, () => {
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(createAnalyticsAsync).toHaveBeenCalled();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(analytics.flushAsync).toHaveBeenCalled();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand();
await TestEasCommand.run();
expect(analytics.logEvent).toHaveBeenCalledWith('action', {
expect(logEvent).toHaveBeenCalledWith('action', {
action: `eas ${TestEasCommand.id}`,
});
});
@@ -94,7 +90,7 @@ describe(EasCommand.name, () => {
});
} catch {}
expect(analytics.flushAsync).toHaveBeenCalled();
expect(flushAsync).toHaveBeenCalled();
});
});
});
@@ -1,8 +0,0 @@
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,9 +1,7 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import SessionManager from '../../user/SessionManager';
export interface ContextOptions {
sessionManager: SessionManager;
analytics: Analytics;
nonInteractive: boolean;
}
@@ -1,30 +1,20 @@
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,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
return { actor, graphqlClient };
}
}
@@ -1,17 +1,13 @@
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 = {
@@ -21,12 +17,7 @@ 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,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
return { actor: actor ?? null, graphqlClient };
}
}
@@ -51,7 +51,6 @@ describe(getProjectIdAsync, () => {
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
@@ -158,13 +157,9 @@ describe(getProjectIdAsync, () => {
expect(projectId).toEqual('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith(
'/app',
{
extra: { eas: { projectId: '2345' } },
},
{ skipSDKVersionRequirement: true }
);
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
extra: { eas: { projectId: '2345' } },
});
});
it('throws if writing the ID back to the config fails', async () => {
@@ -1,5 +1,4 @@
import {
AnyVariables,
Client,
OperationContext,
OperationResult,
@@ -18,9 +17,9 @@ import { getExpoApiBaseUrl } from '../../../api';
import { httpsProxyAgent } from '../../../fetch';
export interface ExpoGraphqlClient extends Client {
query<Data = any, Variables extends AnyVariables = AnyVariables>(
query<Data = any, Variables extends object = object>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
variables: Variables | undefined,
context: Partial<OperationContext> & { additionalTypenames: string[] }
): PromisifiedSource<OperationResult<Data, Variables>>;
}
@@ -25,13 +25,9 @@ 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 } },
},
{ skipSDKVersionRequirement: true }
);
const result = await modifyConfigAsync(projectDir, {
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
});
switch (result.type) {
case 'success':
@@ -1,40 +0,0 @@
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));
}
}
@@ -1,8 +0,0 @@
export enum FeatureGateKey {
// for tests
TEST = 'test',
}
export const featureGateDefaultValueWhenNoServerValue: Record<FeatureGateKey, boolean> = {
[FeatureGateKey.TEST]: true,
};
@@ -1,15 +0,0 @@
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');
};
@@ -1,69 +0,0 @@
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));
}
}
@@ -1,20 +0,0 @@
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`);
};
@@ -1,92 +0,0 @@
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,5 +1,4 @@
import { Flags } from '@oclif/core';
import { OptionFlag } from '@oclif/core/lib/interfaces';
import { EasNonInteractiveAndJsonFlags } from './flags';
@@ -38,7 +37,7 @@ export const getLimitFlagWithCustomValues = ({
}: {
defaultTo: number;
limit: number;
}): OptionFlag<number | undefined> =>
}): any =>
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
+3 -3
View File
@@ -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) {
setAnalyticsEnabledAsync(status === 'on');
await UserSettings.setAsync('analyticsEnabled', status === 'on');
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
} else {
const analyticsEnabled = await getAnalyticsEnabledAsync();
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', null);
Log.log(
`Analytics are ${
analyticsEnabled === false ? 'disabled' : 'enabled'
+39 -1
View File
@@ -1,14 +1,52 @@
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';
@@ -100,7 +100,6 @@ export default class Build extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
@@ -115,7 +114,6 @@ export default class Build extends EasCommand {
loggedIn: { actor, graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
analytics,
} = await this.getContextAsync(Build, {
nonInteractive: flags.nonInteractive,
});
@@ -135,7 +133,6 @@ export default class Build extends EasCommand {
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
projectDir,
flagsWithPlatform,
actor,
@@ -65,7 +65,6 @@ export default class BuildInspect extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
@@ -74,7 +73,6 @@ export default class BuildInspect extends EasCommand {
loggedIn: { actor, graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
analytics,
} = await this.getContextAsync(BuildInspect, {
nonInteractive: false,
});
@@ -99,7 +97,6 @@ export default class BuildInspect extends EasCommand {
try {
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
projectDir,
{
nonInteractive: false,
+20 -4
View File
@@ -9,10 +9,13 @@ import {
getLimitFlagWithCustomValues,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
DistributionType,
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 {
@@ -79,8 +82,7 @@ export default class BuildList extends EasCommand {
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution =
buildDistributionTypeToGraphQLDistributionType(buildDistribution);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
await listAndRenderBuildsOnAppAsync(graphqlClient, {
@@ -131,3 +133,17 @@ 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;
}
};
-213
View File
@@ -1,213 +0,0 @@
import { Errors, Flags } from '@oclif/core';
import { existsSync } from 'fs-extra';
import assert from 'node:assert';
import { getLatestBuildAsync, listAndSelectBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EasPaginatedQueryFlags,
PaginatedQueryOptions,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, runAsync } from '../../run/run';
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
import { downloadAndExtractAppAsync, extractAppFromLocalArchiveAsync } from '../../utils/download';
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 hidden = true;
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 simulator build 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 (
runArchiveFlags.path &&
!(
(runArchiveFlags.path.endsWith('.tar.gz') ||
runArchiveFlags.path.endsWith('.app') ||
runArchiveFlags.path.endsWith('.apk')) &&
existsSync(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 (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;
}
async function maybeGetBuildAsync(
graphqlClient: ExpoGraphqlClient,
flags: RunCommandFlags,
projectId: string,
paginatedQueryOptions: PaginatedQueryOptions
): Promise<BuildFragment | null> {
if (flags.runArchiveFlags.id) {
return BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
} else if (
!flags.runArchiveFlags.id &&
!flags.runArchiveFlags.path &&
!flags.runArchiveFlags.url &&
!flags.runArchiveFlags.latest
) {
return await listAndSelectBuildsOnAppAsync(graphqlClient, {
projectId,
projectDisplayName: await getDisplayNameForProjectIdAsync(graphqlClient, projectId),
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
status: BuildStatus.Finished,
},
queryOptions: paginatedQueryOptions,
});
} else if (flags.runArchiveFlags.latest) {
return await getLatestBuildAsync(graphqlClient, {
projectId,
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
status: BuildStatus.Finished,
},
});
} else {
return null;
}
}
async function getPathToSimulatorBuildAppAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: RunCommandFlags,
queryOptions: PaginatedQueryOptions
): Promise<string> {
const maybeBuild = await maybeGetBuildAsync(graphqlClient, flags, projectId, queryOptions);
const appExtension = flags.selectedPlatform === AppPlatform.Ios ? 'app' : 'apk';
if (maybeBuild) {
if (!maybeBuild.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndExtractAppAsync(
maybeBuild.artifacts.applicationArchiveUrl,
appExtension
);
}
if (flags.runArchiveFlags.url) {
return await downloadAndExtractAppAsync(flags.runArchiveFlags.url, appExtension);
}
if (flags.runArchiveFlags.path?.endsWith('.tar.gz')) {
return await extractAppFromLocalArchiveAsync(flags.runArchiveFlags.path!, appExtension);
}
// this should never fail, due to the validation in sanitizeFlagsAsync
assert(flags.runArchiveFlags.path);
return flags.runArchiveFlags.path;
}
@@ -1,16 +1,63 @@
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';
@@ -85,7 +132,7 @@ export default class ChannelCreate extends EasCommand {
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
} = await createUpdateChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
@@ -13,8 +13,6 @@ 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> {
@@ -22,8 +20,6 @@ export default class Credentials extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig,
getDynamicProjectConfigAsync,
analytics,
} = await this.getContextAsync(Credentials, {
nonInteractive: false,
});
@@ -31,9 +27,7 @@ export default class Credentials extends EasCommand {
await new SelectPlatform(
actor,
graphqlClient,
analytics,
projectConfig ?? null,
getDynamicProjectConfigAsync,
flags.platform
).runAsync();
}
@@ -1,79 +0,0 @@
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();
}
}
}
+9 -25
View File
@@ -1,5 +1,3 @@
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';
@@ -8,9 +6,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';
@@ -26,7 +24,6 @@ export default class MetadataPull extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
@@ -36,7 +33,6 @@ export default class MetadataPull extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig: { exp, projectId, projectDir },
analytics,
} = await this.getContextAsync(MetadataPull, {
nonInteractive: false,
});
@@ -44,35 +40,23 @@ 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({
analytics,
exp,
credentialsCtx,
projectDir,
profile: submitProfile,
});
const filePath = await downloadMetadataAsync(metadataCtx);
const relativePath = path.relative(process.cwd(), filePath);
Log.addNewLineIfNone();
+9 -26
View File
@@ -1,14 +1,12 @@
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';
@@ -24,7 +22,6 @@ export default class MetadataPush extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
@@ -34,7 +31,6 @@ export default class MetadataPush extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig: { exp, projectId, projectDir },
analytics,
} = await this.getContextAsync(MetadataPush, {
nonInteractive: false,
});
@@ -42,36 +38,23 @@ 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,
});
try {
const { appleLink } = await uploadMetadataAsync({
analytics,
exp,
credentialsCtx,
projectDir,
profile: submitProfile,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
projectDir,
exp,
profileName: flags.profile,
});
try {
const { appleLink } = await uploadMetadataAsync(metadataCtx);
Log.addNewLineIfNone();
Log.log(`🎉 Store configuration is synced with the app stores.
@@ -1,4 +1,4 @@
import { AppJSONConfig, PackageJSONConfig, getConfig, modifyConfigAsync } from '@expo/config';
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
import chalk from 'chalk';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
@@ -7,11 +7,8 @@ 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 FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester, jester2 } from '../../../credentials/__tests__/fixtures-constants';
import { jester } 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';
@@ -21,7 +18,6 @@ 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: () => {} }),
@@ -81,11 +77,9 @@ 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,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest
.spyOn(LoggedInContextField.prototype, 'getValueAsync')
.mockResolvedValue({ actor: jester, graphqlClient });
}
const commandOptions = { root: '/test-project' } as any;
@@ -103,23 +97,11 @@ 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');
@@ -127,16 +109,8 @@ 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 expect(
new ProjectInit(['--id', '12345'], commandOptions).run()
).rejects.toThrowError('Aborting');
await new ProjectInit(['--id', '12345'], commandOptions).run();
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
expect(confirmAsync).toHaveBeenCalled();
});
@@ -144,12 +118,6 @@ 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();
@@ -172,170 +140,16 @@ 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', () => {
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');
beforeEach(() => {
mockTestProject({});
});
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();
});
it('configures', async () => {
await new ProjectInit(['--id', '1234'], commandOptions).run();
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '1234');
});
});
});
+4 -117
View File
@@ -1,5 +1,3 @@
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';
@@ -11,7 +9,6 @@ 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 { AppQuery } from '../../graphql/queries/AppQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
@@ -57,99 +54,7 @@ export default class ProjectInit extends EasCommand {
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
}
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(
private static async initializeWithExplicitIDAsync(
projectId: string,
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
@@ -187,29 +92,14 @@ export default class ProjectInit extends EasCommand {
)}. Do you wish to overwrite it?`,
});
if (!confirm) {
throw new Error('Aborting');
Log.log('Aborting');
return;
}
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,
@@ -337,10 +227,7 @@ export default class ProjectInit extends EasCommand {
} = await this.getContextAsync(ProjectInit, { nonInteractive });
if (id) {
await ProjectInit.initializeWithExplicitIDAsync(graphqlClient, id, projectDir, {
force,
nonInteractive,
});
await ProjectInit.initializeWithExplicitIDAsync(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 validationMessage;
return 'Secret value may not be empty.';
}
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)}.`
);
+20 -17
View File
@@ -1,13 +1,11 @@
import chalk from 'chalk';
import Table from 'cli-table3';
import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import {
EnvironmentSecretWithScope,
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
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';
@@ -27,18 +25,23 @@ 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(secrets.map(secret => formatSecret(secret)).join(`\n\n${chalk.dim('———')}\n\n`));
Log.log(table.toString());
}
}
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') },
]);
}
@@ -1,241 +0,0 @@
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;
}
}
+1 -4
View File
@@ -75,7 +75,7 @@ export default class Submit extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
verbose: Flags.boolean({
description: 'Always print logs from EAS Submit',
description: 'Always print logs from Submission Service',
default: false,
}),
wait: Flags.boolean({
@@ -93,7 +93,6 @@ export default class Submit extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
@@ -101,7 +100,6 @@ export default class Submit extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig: { exp, projectId, projectDir },
analytics,
} = await this.getContextAsync(Submit, {
nonInteractive: false,
});
@@ -131,7 +129,6 @@ export default class Submit extends EasCommand {
nonInteractive: flagsWithPlatform.nonInteractive,
actor,
graphqlClient,
analytics,
exp,
projectId,
});
+279 -13
View File
@@ -1,10 +1,24 @@
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 { EASNonInteractiveFlag } from '../../commandUtils/flags';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { AppPlatform } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { RequestedPlatform, appPlatformDisplayNames } 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;
export default class UpdateConfigure extends EasCommand {
static override description = 'configure the project to support EAS Update';
@@ -16,7 +30,6 @@ export default class UpdateConfigure extends EasCommand {
options: ['android', 'ios', 'all'],
default: 'all',
}),
...EASNonInteractiveFlag,
};
static override contextDefinition = {
@@ -25,27 +38,280 @@ 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: flags['non-interactive'],
nonInteractive: true,
});
const platform = flags.platform as RequestedPlatform;
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.'
);
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
await installExpoUpdatesAsync(projectDir);
}
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
exp,
projectId,
const [androidWorkflow, iosWorkflow] = await Promise.all([
resolveWorkflowAsync(projectDir, Platform.ANDROID),
resolveWorkflowAsync(projectDir, Platform.IOS),
]);
const updatedExp = await configureAppJSONForEASUpdateAsync({
projectDir,
exp,
platform,
workflows: {
android: androidWorkflow,
ios: iosWorkflow,
},
projectId,
});
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`);
}
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;
}
}
+198 -205
View File
@@ -1,4 +1,4 @@
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
import { ExpoConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
@@ -6,11 +6,12 @@ import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import { getEASUpdateURL } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError, 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';
@@ -20,82 +21,108 @@ import {
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 {
ExpoCLIExportPlatformFlag,
getOwnerAccountForProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
filterExportedPlatformsByFlag,
isUploadedAssetCountAboveWarningThreshold,
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { confirmAsync, promptAsync } from '../../prompts';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
import { formatUpdateMessage } 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: Partial<PublishPlatform>[] = ['android', 'ios'];
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
export type PublishPlatformFlag = PublishPlatform | 'all';
type RawUpdateFlags = {
auto: boolean;
branch?: string;
message?: string;
group?: string;
republish?: boolean;
platform: string;
'input-dir': string;
'skip-bundler': boolean;
'private-key-path'?: string;
'non-interactive': boolean;
json: boolean;
};
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 UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
updateMessage?: string;
republish: boolean;
groupId?: string;
inputDir: string;
skipBundler: boolean;
privateKeyPath?: string;
json: boolean;
nonInteractive: boolean;
};
export async function ensureBranchExistsAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name: branchName }: ViewBranchQueryVariables
): Promise<{
branchId: string;
}> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
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}`);
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;
}
}
}
@@ -130,11 +157,7 @@ export default class UpdatePublish extends EasCommand {
}),
platform: Flags.enum({
char: 'p',
options: [
// TODO: Add web when it's fully supported
...defaultPublishPlatforms,
'all',
],
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
@@ -156,22 +179,21 @@ export default class UpdatePublish extends EasCommand {
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
const { flags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
let {
branch: branchName,
auto: autoFlag,
platform: platformFlag,
branchName,
updateMessage,
message,
republish,
groupId,
inputDir,
skipBundler,
privateKeyPath,
group,
'input-dir': inputDir,
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
json: jsonFlag,
nonInteractive,
} = this.sanitizeFlags(rawFlags);
} = flags;
const {
getDynamicProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -183,11 +205,11 @@ export default class UpdatePublish extends EasCommand {
enableJsonOutput();
}
const {
exp: expPossiblyWithoutEasUpdateConfigured,
projectId,
projectDir,
} = await getDynamicProjectConfigAsync({
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({
isPublicConfig: true,
});
@@ -197,17 +219,36 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
projectDir,
projectId,
});
const { exp } = await getDynamicProjectConfigAsync();
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
let realizedPlatforms: PublishPlatform[] = [];
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
if (!hasExpoUpdates && nonInteractive) {
Errors.error(
`${chalk.bold(
'expo-updates'
)} must already be installed when executing in non-interactive mode`,
{ exit: 1 }
);
}
if (!hasExpoUpdates) {
const install = await confirmAsync({
message: `You are creating an update which requires ${chalk.bold(
'expo-updates'
)} to be installed in your app.\n Do you want EAS CLI to install it for you?`,
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoUpdatesAsync(projectDir);
} else {
Errors.error(`Install ${chalk.bold('expo-updates')} manually and come back later.`, {
exit: 1,
});
}
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
await checkEASUpdateURLIsSetAsync(exp, projectId);
if (!branchName) {
if (autoFlag) {
@@ -255,9 +296,9 @@ export default class UpdatePublish extends EasCommand {
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (groupId) {
if (group) {
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId,
groupId: group,
});
updatesToRepublish = updatesByGroup;
} else {
@@ -278,17 +319,17 @@ export default class UpdatePublish extends EasCommand {
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
groupId ? groupId : updatesToRepublish[0].group
group ? group : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length < defaultPublishPlatforms.length) {
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 platforms it was originally published on: ${updatesToRepublishFilteredByPlatform
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(', ')}`;
} else {
@@ -302,16 +343,13 @@ export default class UpdatePublish extends EasCommand {
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
realizedPlatforms = updatesToRepublishFilteredByPlatform.map(
update => update.platform as PublishPlatform
);
// These are the same for each member of an update group
groupId = updatesToRepublishFilteredByPlatform[0].group;
group = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!updateMessage) {
if (!message) {
if (nonInteractive) {
throw new Error('Must supply --message when in non-interactive mode');
}
@@ -320,20 +358,20 @@ export default class UpdatePublish extends EasCommand {
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'updateMessage',
name: 'publishMessage',
message: `Provide an update message.`,
initial: `Republish "${oldMessage!}" - group: ${groupId}`,
initial: `Republish "${oldMessage!}" - group: ${group}`,
validate: (value: any) => (value ? true : validationMessage),
}));
}
} else {
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
if (!message && autoFlag) {
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (!message) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
@@ -342,9 +380,9 @@ export default class UpdatePublish extends EasCommand {
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
({ publishMessage: message } = await promptAsync({
type: 'text',
name: 'updateMessage',
name: 'publishMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
@@ -353,81 +391,60 @@ export default class UpdatePublish extends EasCommand {
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
const bundleSpinner = ora().start('Building bundle...');
try {
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
await buildBundlesAsync({ projectDir, inputDir });
bundleSpinner.succeed('Built bundle!');
} catch (e) {
bundleSpinner.fail('Export failed');
bundleSpinner.fail('Failed to build bundle!');
throw e;
}
}
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
const assetSpinner = ora().start('Uploading assets...');
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
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 (${totalAssets - missingAssets}/${totalAssets})`;
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 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
: `Uploading assets skipped -- no new assets found!`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload');
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
if (truncatedMessage !== updateMessage) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
const truncatedMessage = truncatePublishUpdateMessage(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 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 { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
name: branchName,
});
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await getVcsClient().getCommitHashAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
@@ -443,9 +460,8 @@ export default class UpdatePublish extends EasCommand {
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtimeVersion,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message: truncatedMessage,
gitCommitHash,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -514,15 +530,12 @@ export default class UpdatePublish extends EasCommand {
}
Log.addNewLineIfNone();
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
for (const runtime of new Set(Object.values(runtimeVersions))) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
update => update.runtimeVersion === runtime
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
@@ -539,7 +552,7 @@ export default class UpdatePublish extends EasCommand {
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Runtime version', value: runtime },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
@@ -547,7 +560,6 @@ export default class UpdatePublish extends EasCommand {
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: truncatedMessage },
...(gitCommitHash ? [{ label: 'Commit', value: gitCommitHash }] : []),
{ label: 'Website link', value: updateGroupLink },
])
);
@@ -567,54 +579,19 @@ export default class UpdatePublish extends EasCommand {
}
}
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && updateMessage)) {
Errors.error(
'--auto or both --branch and --message are required when updating in non-interactive mode',
{ exit: 1 }
);
}
const groupId = flags.group;
const republish = flags.republish || !!groupId; // When --group is defined, we are republishing
if (nonInteractive && republish && !groupId) {
Errors.error(`--group is required when updating in non-interactive mode`, { exit: 1 });
}
return {
auto,
branchName,
updateMessage,
groupId,
republish,
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,
platforms: PublishPlatform[],
platformFlag: PublishPlatformFlag,
projectDir: string
): Promise<{ platform: string; runtimeVersion: string }[]> {
): Promise<Record<string, string>> {
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
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 as Platform)) === Workflow.MANAGED;
const isManaged = (await resolveWorkflowAsync(projectDir, 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.'
@@ -623,18 +600,34 @@ async function getRuntimeVersionObjectAsync(
}
}
return [...new Set(platforms)].map(platform => {
if (platform === 'web') {
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
}
return {
return Object.fromEntries(
platforms.map(platform => [
platform,
runtimeVersion: nullthrows(
nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
`Unable to determine runtime version for ${platform}`
),
};
});
])
);
}
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;
};
+16 -3
View File
@@ -1,10 +1,12 @@
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 { formatUpdateGroup, getUpdateGroupDescriptions } from '../../update/utils';
import { UPDATE_COLUMNS, getUpdateGroupDescriptions } from '../../update/utils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class UpdateView extends EasCommand {
@@ -46,9 +48,20 @@ export default class UpdateView extends EasCommand {
if (jsonFlag) {
printJsonOnlyOutput(updateGroupDescription);
} else {
Log.log(chalk.bold('Update group:'));
const groupTable = new Table({
head: [...UPDATE_COLUMNS],
wordWrap: true,
});
Log.log(formatUpdateGroup(updateGroupDescription));
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());
}
}
}
@@ -15,14 +15,13 @@ export default class WebhookView extends EasCommand {
},
];
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
};
static override contextDefinition = { ...this.ContextOptions.LoggedIn };
async runAsync(): Promise<void> {
const {
args: { ID: webhookId },
} = await this.parse(WebhookView);
const {
loggedIn: { graphqlClient },
} = await this.getContextAsync(WebhookView, { nonInteractive: true });
@@ -1,7 +1,6 @@
import { AppFragment, Role } from '../../graphql/generated';
import { Actor } from '../../user/User';
export const jester: Actor = {
export const jester = {
__typename: 'User' as const,
id: 'jester-id',
username: 'jester',
@@ -23,7 +22,6 @@ export const jester: Actor = {
},
],
isExpoAdmin: false,
featureGates: {},
};
export const jester2 = {
@@ -38,7 +36,6 @@ export const jester2 = {
},
],
isExpoAdmin: false,
featureGates: {},
};
export const testUsername = jester.username;
@@ -1,4 +1,3 @@
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { AccountFragment, AndroidKeystoreFragment } from '../../../graphql/generated';
import Log from '../../../log';
@@ -17,7 +16,7 @@ export class CreateKeystore {
}
const projectId = ctx.projectId;
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, ctx.analytics, projectId);
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, projectId);
const keystoreFragment = await ctx.android.createKeystoreAsync(
ctx.graphqlClient,
this.account,
@@ -29,7 +28,6 @@ export class CreateKeystore {
private async provideOrGenerateAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
projectId: string
): Promise<KeystoreWithType> {
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
@@ -38,6 +36,6 @@ export class CreateKeystore {
validateKeystore(providedKeystoreWithType);
return providedKeystoreWithType;
}
return await generateRandomKeystoreAsync(graphqlClient, analytics, projectId);
return await generateRandomKeystoreAsync(graphqlClient, 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/AnalyticsManager';
import { Analytics, BuildEvent } from '../../../analytics/events';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import fetch from '../../../fetch';
import { AndroidKeystoreType } from '../../../graphql/generated';
@@ -38,7 +38,6 @@ enum KeystoreCreateStep {
export async function generateRandomKeystoreAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
projectId: string
): Promise<KeystoreWithType> {
const keystoreData: KeystoreParams = {
@@ -46,16 +45,15 @@ export async function generateRandomKeystoreAsync(
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
};
return await createKeystoreAsync(graphqlClient, analytics, keystoreData, projectId);
return await createKeystoreAsync(graphqlClient, 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,
@@ -76,14 +74,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,7 +3,6 @@ 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';
@@ -27,7 +26,6 @@ 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;
@@ -43,7 +41,6 @@ export class CredentialsContext {
projectDir: string;
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
env?: Env;
}
) {
@@ -51,7 +48,6 @@ 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(ctx.analytics, {
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
});
return await getMinimalAscApiKeyAsync(ascApiKey);
@@ -1,6 +1,5 @@
import { ProfileType } from '@expo/app-store';
import { Analytics } from '../../../analytics/AnalyticsManager';
import Log from '../../../log';
import {
AscApiKey,
@@ -193,12 +192,9 @@ export default class AppStoreApi {
return await getAscApiKeyAsync(userCtx, keyId);
}
public async createAscApiKeyAsync(
analytics: Analytics,
{ nickname }: { nickname: string }
): Promise<AscApiKey> {
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await createAscApiKeyAsync(analytics, userCtx, { nickname });
return await createAscApiKeyAsync(userCtx, { nickname });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
@@ -1,7 +1,5 @@
import { ApiKey, ApiKeyType, UnexpectedResponse, UserRole } from '@expo/apple-utils';
import { instance, mock } from 'ts-mockito';
import { Analytics } from '../../../../analytics/AnalyticsManager';
import {
createAscApiKeyAsync,
downloadWithRetryAsync,
@@ -74,13 +72,10 @@ 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(analytics, mockAuthCtx, {
nickname: 'test-name',
});
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
allAppsVisible: true,
@@ -104,7 +99,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,
@@ -113,7 +108,7 @@ test(`downloadWithRetryAsync`, async () => {
}),
} as unknown as ApiKey;
await expect(
downloadWithRetryAsync(analytics, mockApiKeyWithDownloadError, { minTimeout: 1 }) // stay within jest timeout window
downloadWithRetryAsync(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);
@@ -124,23 +119,15 @@ test(`downloadWithRetryAsync`, async () => {
(mockApiKeyWithOneTimeDownloadError.downloadAsync as jest.Mock).mockImplementationOnce(() => {
throw new UnexpectedResponse(cacheFailureMessage);
});
const keyP8AfterOneFailure = await downloadWithRetryAsync(
analytics,
mockApiKeyWithOneTimeDownloadError,
{
minTimeout: 1,
}
);
const keyP8AfterOneFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
minTimeout: 1,
});
expect(keyP8AfterOneFailure).toBe('super secret');
// successful case
(mockApiKey.downloadAsync as jest.Mock).mockClear();
const keyP8NoFailure = await downloadWithRetryAsync(
analytics,
mockApiKeyWithOneTimeDownloadError,
{
minTimeout: 1,
}
);
const keyP8NoFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
minTimeout: 1,
});
expect(keyP8NoFailure).toBe('super secret');
});
@@ -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/AnalyticsManager';
import { Analytics, SubmissionEvent } from '../../../analytics/events';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
@@ -58,7 +58,6 @@ export async function getAscApiKeyAsync(
* is available for download.
* */
export async function downloadWithRetryAsync(
analytics: Analytics,
key: ApiKey,
{
minTimeout = 1000,
@@ -82,7 +81,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,
@@ -98,7 +97,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 (
@@ -111,7 +110,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,
});
@@ -124,7 +123,6 @@ export async function downloadWithRetryAsync(
* **Does not support App Store Connect API (CI).**
*/
export async function createAscApiKeyAsync(
analytics: Analytics,
userAuthCtx: UserAuthCtx,
{
nickname,
@@ -142,7 +140,7 @@ export async function createAscApiKeyAsync(
roles: roles ?? [UserRole.ADMIN],
keyType: keyType ?? ApiKeyType.PUBLIC_API,
});
const keyP8 = await downloadWithRetryAsync(analytics, key);
const keyP8 = await downloadWithRetryAsync(key);
if (!keyP8) {
const { nickname, roles } = key.attributes;
const humanReadableKey = `App Store Connect Key '${nickname}' (${
@@ -1,5 +1,3 @@
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';
@@ -9,9 +7,7 @@ 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, CredentialsContextProjectInfo } from '../context';
import { CredentialsContext } from '../context';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
@@ -52,19 +52,11 @@ 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,
projectInfo: this.callingAction.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, CredentialsContextProjectInfo } from '../context';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
selectAscApiKeysFromAccountAsync,
@@ -61,21 +61,11 @@ 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,
projectInfo: this.callingAction.projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
analytics: this.callingAction.analytics,
env: buildProfile?.env,
nonInteractive: false,
});
@@ -1,5 +1,3 @@
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';
@@ -11,9 +9,7 @@ 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
) {}
@@ -43,7 +43,6 @@ describe(AccountResolver, () => {
},
],
isExpoAdmin: false,
featureGates: {},
};
describe('when inside project dir', () => {
@@ -61,11 +60,10 @@ describe(AccountResolver, () => {
});
it('returns the account defined in app.json/app.config.js if user confirms', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(prompts).mockImplementationOnce(async () => ({
value: true,
}));
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const resolver = new AccountResolver(graphqlClient, '1234', user);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual({
@@ -76,14 +74,13 @@ describe(AccountResolver, () => {
});
it('asks the user to choose the account from his account list if he rejects to use the one defined in app.json / app.config.js', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(prompts).mockImplementationOnce(async () => ({
useProjectAccount: false,
}));
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const resolver = new AccountResolver(graphqlClient, '1234', user);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]);
@@ -92,11 +89,10 @@ describe(AccountResolver, () => {
describe('when outside project dir', () => {
it('asks the user to choose the account from his account list', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const resolver = new AccountResolver(graphqlClient, null, user);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]);
@@ -1,4 +1,5 @@
import chalk from 'chalk';
import Table from 'cli-table3';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppleDeviceMutation } from '../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
@@ -7,7 +8,6 @@ import Log from '../../../log';
import { ora } from '../../../ora';
import { confirmAsync, promptAsync } from '../../../prompts';
import { isValidUDID, normalizeUDID } from '../../udids';
import { formatNewDevice } from '../../utils/formatDevice';
interface DeviceData {
udid: string;
@@ -15,6 +15,11 @@ interface DeviceData {
deviceClass: AppleDeviceClass | null;
}
const DEVICE_CLASS_DISPLAY_NAMES: Record<AppleDeviceClass, string> = {
[AppleDeviceClass.Iphone]: 'iPhone',
[AppleDeviceClass.Ipad]: 'iPad',
};
export async function runInputMethodAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string,
@@ -84,7 +89,7 @@ async function collectDeviceDataAsync(
This will ${chalk.bold('not')} register the device on the Apple Developer Portal yet.`
);
Log.newLine();
Log.log(formatNewDevice({ ...deviceData, identifier: deviceData.udid }, appleTeam));
printDeviceDataSummary(deviceData, appleTeam);
Log.newLine();
const registrationConfirmed = await confirmAsync({
@@ -151,3 +156,21 @@ async function promptForDeviceClassAsync(
});
return deviceClass;
}
function printDeviceDataSummary(
{ udid, name, deviceClass }: DeviceData,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>
): void {
const deviceSummary = new Table({
colWidths: [25, 55],
wordWrap: true,
});
deviceSummary.push(
['Apple Team ID', appleTeam.appleTeamIdentifier],
['Apple Team Name', appleTeam.appleTeamName ?? '(unknown)'],
['Device UDID', udid],
['Device Name', name ?? '(empty)'],
['Device Class', deviceClass ? DEVICE_CLASS_DISPLAY_NAMES[deviceClass] : '(unknown)']
);
Log.log(deviceSummary.toString());
}
+1 -1
View File
@@ -3,9 +3,9 @@ import AppStoreApi from '../credentials/ios/appstore/AppStoreApi';
import { Actor } from '../user/User';
export interface DeviceManagerContext {
graphqlClient: ExpoGraphqlClient;
appStore: AppStoreApi;
user: Actor;
graphqlClient: ExpoGraphqlClient;
projectId: string | null;
}
@@ -1,61 +1,29 @@
import { AppleDevice, AppleDeviceClass, AppleTeam } from '../../graphql/generated';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
import { AppleDevice, AppleTeam } from '../../graphql/generated';
import formatFields from '../../utils/formatFields';
type Device = Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled' | 'model'>;
type NewDevice = Pick<AppleDevice, 'identifier' | 'name' | 'deviceClass'>;
export type AppleTeamIdAndName = Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName'>;
const DEVICE_CLASS_DISPLAY_NAMES: Record<AppleDeviceClass, string> = {
[AppleDeviceClass.Iphone]: 'iPhone',
[AppleDeviceClass.Ipad]: 'iPad',
};
function formatDeviceClass(device: Device | NewDevice): string {
if (!device.deviceClass || !DEVICE_CLASS_DISPLAY_NAMES[device.deviceClass]) {
return 'Unknown';
}
return [DEVICE_CLASS_DISPLAY_NAMES[device.deviceClass], 'model' in device ? device.model : '']
.filter(value => !!value)
.join(' ');
}
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
const fields: FormatFieldsItem[] = [
const fields = [
{ label: 'ID', value: device.id },
{ label: 'Name', value: device.name ?? 'Unknown' },
{
label: 'Class',
value: formatDeviceClass(device),
value: device.deviceClass
? `${device.deviceClass}${device.model ? ` ${device.model}` : ''}`
: 'Unknown',
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
fields.push(
{ label: 'Apple Team ID', value: team.appleTeamIdentifier },
{ label: 'Apple Team Name', value: team.appleTeamName ?? 'Unknown' }
);
}
return formatFields(fields);
}
export function formatNewDevice(device: NewDevice, team?: AppleTeamIdAndName): string {
const fields: FormatFieldsItem[] = [
{ label: 'Name', value: device.name ?? '(empty)' },
{
label: 'Class',
value: formatDeviceClass(device),
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
fields.push(
{ label: 'Apple Team ID', value: team.appleTeamIdentifier },
{ label: 'Apple Team Name', value: team.appleTeamName ?? 'Unknown' }
...[
{ label: 'Apple Team ID', value: team.appleTeamIdentifier },
{ label: 'Apple Team Name', value: team.appleTeamName ?? 'Unknown' },
]
);
}
-8
View File
@@ -8,12 +8,4 @@ export default {
* ignore failures when parsing build.gradle.
*/
overrideAndroidApplicationId: process.env.EAS_DANGEROUS_OVERRIDE_ANDROID_APPLICATION_ID,
/**
* Comma separated list of feature gate keys of feature gates to evaluate override to true.
*/
featureGateEnable: process.env.EAS_FG_ENABLE,
/**
* Comma separated list of feature gate keys of feature gates to evaluate override to false.
*/
featureGateDisable: process.env.EAS_FG_DISABLE,
};
+23 -111
View File
@@ -401,7 +401,6 @@ export type AccountSsoConfiguration = {
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userInfoEndpoint?: Maybe<Scalars['String']>;
@@ -416,7 +415,6 @@ export type AccountSsoConfigurationData = {
issuer?: InputMaybe<Scalars['String']>;
jwksEndpoint?: InputMaybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: InputMaybe<Scalars['String']>;
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
};
@@ -459,7 +457,6 @@ export type AccountSsoConfigurationPublicData = {
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
userInfoEndpoint?: Maybe<Scalars['String']>;
};
@@ -1747,8 +1744,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'Build';
activityTimestamp: Scalars['DateTime'];
actor?: Maybe<Actor>;
/** The actual resource class of the builder assigned to the build job */
actualResourceClass?: Maybe<BuildResourceClass>;
appBuildVersion?: Maybe<Scalars['String']>;
appVersion?: Maybe<Scalars['String']>;
artifacts?: Maybe<BuildArtifacts>;
@@ -1789,8 +1784,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
queuePosition?: Maybe<Scalars['Int']>;
reactNativeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
/** The builder resource class requested by the developer */
resourceClass: BuildResourceClass;
resourceClass?: Maybe<BuildResourceClass>;
runFromCI?: Maybe<Scalars['Boolean']>;
runtimeVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
@@ -2225,7 +2219,6 @@ export type CreateAccessTokenResponse = {
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
archiveUrl?: InputMaybe<Scalars['String']>;
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
@@ -2252,7 +2245,6 @@ export type CreateGitHubRepositoryInput = {
appId: Scalars['ID'];
githubAppInstallationId: Scalars['ID'];
githubRepositoryIdentifier: Scalars['Int'];
nodeIdentifier: Scalars['String'];
};
export type CreateGitHubRepositorySettingsInput = {
@@ -2263,7 +2255,6 @@ export type CreateGitHubRepositorySettingsInput = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
archiveUrl?: InputMaybe<Scalars['String']>;
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
@@ -2326,16 +2317,6 @@ export type DeleteGoogleServiceAccountKeyResult = {
id: Scalars['ID'];
};
export type DeleteIosAppBuildCredentialsResult = {
__typename?: 'DeleteIosAppBuildCredentialsResult';
id: Scalars['ID'];
};
export type DeleteIosAppCredentialsResult = {
__typename?: 'DeleteIosAppCredentialsResult';
id: Scalars['ID'];
};
export type DeleteRobotResult = {
__typename?: 'DeleteRobotResult';
id: Scalars['ID'];
@@ -2523,15 +2504,9 @@ export type GetSignedAssetUploadSpecificationsResult = {
specifications: Array<Scalars['String']>;
};
export enum GitHubAppEnvironment {
Development = 'DEVELOPMENT',
Production = 'PRODUCTION',
Staging = 'STAGING'
}
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
accessibleRepositories: GitHubRepositoryPaginationResult;
accessibleRepositories: Array<GitHubAppInstallationAccessibleRepository>;
account: Account;
actor?: Maybe<Actor>;
id: Scalars['ID'];
@@ -2551,7 +2526,6 @@ export type GitHubAppInstallationAccessibleRepository = {
description?: Maybe<Scalars['String']>;
id: Scalars['Int'];
name: Scalars['String'];
nodeId: Scalars['String'];
owner: GitHubRepositoryOwner;
private: Scalars['Boolean'];
url: Scalars['String'];
@@ -2592,20 +2566,14 @@ export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
environment: GitHubAppEnvironment;
installation: GitHubAppInstallation;
name: Scalars['String'];
searchRepositories: GitHubRepositoryPaginationResult;
};
export type GitHubAppQueryInstallationArgs = {
id: Scalars['ID'];
searchRepositories: Array<GitHubAppInstallationAccessibleRepository>;
};
export type GitHubAppQuerySearchRepositoriesArgs = {
githubAppInstallationId: Scalars['ID'];
page?: InputMaybe<Scalars['Int']>;
perPage?: InputMaybe<Scalars['Int']>;
query: Scalars['String'];
};
@@ -2616,19 +2584,16 @@ export type GitHubRepository = {
githubRepositoryIdentifier: Scalars['Int'];
id: Scalars['ID'];
metadata?: Maybe<GitHubRepositoryMetadata>;
nodeIdentifier: Scalars['String'];
};
export type GitHubRepositoryMetadata = {
__typename?: 'GitHubRepositoryMetadata';
defaultBranch?: Maybe<Scalars['String']>;
githubRepoDescription?: Maybe<Scalars['String']>;
githubRepoName: Scalars['String'];
githubRepoOwnerName: Scalars['String'];
githubRepoUrl: Scalars['String'];
lastPushed: Scalars['DateTime'];
lastUpdated: Scalars['DateTime'];
openGraphImageUrl?: Maybe<Scalars['String']>;
private: Scalars['Boolean'];
};
@@ -2658,12 +2623,6 @@ export type GitHubRepositoryOwner = {
url: Scalars['String'];
};
export type GitHubRepositoryPaginationResult = {
__typename?: 'GitHubRepositoryPaginationResult';
repositories: Array<GitHubAppInstallationAccessibleRepository>;
totalCount: Scalars['Int'];
};
export type GitHubRepositorySettings = {
__typename?: 'GitHubRepositorySettings';
app: App;
@@ -2829,8 +2788,6 @@ export type IosAppBuildCredentialsMutation = {
__typename?: 'IosAppBuildCredentialsMutation';
/** Create a set of build credentials for an iOS app */
createIosAppBuildCredentials: IosAppBuildCredentials;
/** Disassociate the build credentials from an iOS app */
deleteIosAppBuildCredentials: DeleteIosAppBuildCredentialsResult;
/** Set the distribution certificate to be used for an iOS app */
setDistributionCertificate: IosAppBuildCredentials;
/** Set the provisioning profile to be used for an iOS app */
@@ -2844,11 +2801,6 @@ export type IosAppBuildCredentialsMutationCreateIosAppBuildCredentialsArgs = {
};
export type IosAppBuildCredentialsMutationDeleteIosAppBuildCredentialsArgs = {
id: Scalars['ID'];
};
export type IosAppBuildCredentialsMutationSetDistributionCertificateArgs = {
distributionCertificateId: Scalars['ID'];
id: Scalars['ID'];
@@ -2897,8 +2849,6 @@ export type IosAppCredentialsMutation = {
__typename?: 'IosAppCredentialsMutation';
/** Create a set of credentials for an iOS app */
createIosAppCredentials: IosAppCredentials;
/** Delete a set of credentials for an iOS app */
deleteIosAppCredentials: DeleteIosAppCredentialsResult;
/** Set the App Store Connect Api Key to be used for submitting an iOS app */
setAppStoreConnectApiKeyForSubmissions: IosAppCredentials;
/** Set the push key to be used in an iOS app */
@@ -2913,11 +2863,6 @@ export type IosAppCredentialsMutationCreateIosAppCredentialsArgs = {
};
export type IosAppCredentialsMutationDeleteIosAppCredentialsArgs = {
id: Scalars['ID'];
};
export type IosAppCredentialsMutationSetAppStoreConnectApiKeyForSubmissionsArgs = {
ascApiKeyId: Scalars['ID'];
id: Scalars['ID'];
@@ -2929,16 +2874,6 @@ export type IosAppCredentialsMutationSetPushKeyArgs = {
pushKeyId: Scalars['ID'];
};
export type IosAppCredentialsQuery = {
__typename?: 'IosAppCredentialsQuery';
byId: IosAppCredentials;
};
export type IosAppCredentialsQueryByIdArgs = {
iosAppCredentialsId: Scalars['ID'];
};
/** @deprecated Use developmentClient option instead. */
export enum IosBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT',
@@ -3297,7 +3232,6 @@ export type ProjectArchiveSourceInput = {
};
export enum ProjectArchiveSourceType {
Gcs = 'GCS',
S3 = 'S3',
Url = 'URL'
}
@@ -3347,7 +3281,6 @@ export type PublicArtifacts = {
export type PublishUpdateGroupInput = {
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']>;
branchId: Scalars['String'];
gitCommitHash?: InputMaybe<Scalars['String']>;
message?: InputMaybe<Scalars['String']>;
runtimeVersion: Scalars['String'];
updateInfoGroup: UpdateInfoGroup;
@@ -3566,8 +3499,6 @@ export type RootQuery = {
githubApp: GitHubAppQuery;
/** Top-level query object for querying Stripe Invoices. */
invoice: InvoiceQuery;
/** Top-level query object for querying IosAppCredentials. */
iosAppCredentials: IosAppCredentialsQuery;
/**
* If authenticated as a typical end user, this is the appropriate top-level
* query object
@@ -3898,20 +3829,6 @@ export enum SubmissionAndroidTrack {
Production = 'PRODUCTION'
}
export type SubmissionArchiveSourceInput = {
/** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE or GCS_SUBMIT_ARCHIVE */
bucketKey?: InputMaybe<Scalars['String']>;
type: SubmissionArchiveSourceType;
/** Required if the archive source type is URL */
url?: InputMaybe<Scalars['String']>;
};
export enum SubmissionArchiveSourceType {
GcsBuildApplicationArchive = 'GCS_BUILD_APPLICATION_ARCHIVE',
GcsSubmitArchive = 'GCS_SUBMIT_ARCHIVE',
Url = 'URL'
}
export type SubmissionError = {
__typename?: 'SubmissionError';
errorCode?: Maybe<Scalars['String']>;
@@ -4004,7 +3921,6 @@ export type Update = ActivityTimelineProjectActivity & {
branchId: Scalars['ID'];
codeSigningInfo?: Maybe<CodeSigningInfo>;
createdAt: Scalars['DateTime'];
gitCommitHash?: Maybe<Scalars['String']>;
group: Scalars['String'];
id: Scalars['ID'];
manifestFragment: Scalars['String'];
@@ -4177,10 +4093,8 @@ export type UploadSessionCreateUploadSessionArgs = {
};
export enum UploadSessionType {
EasBuildGcsProjectSources = 'EAS_BUILD_GCS_PROJECT_SOURCES',
EasBuildProjectSources = 'EAS_BUILD_PROJECT_SOURCES',
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE',
EasSubmitGcsAppArchive = 'EAS_SUBMIT_GCS_APP_ARCHIVE'
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
}
export type UsageMetricTotal = {
@@ -4553,15 +4467,6 @@ export type CreateUpdateBranchForAppMutationVariables = Exact<{
export type CreateUpdateBranchForAppMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', createUpdateBranchForApp: { __typename?: 'UpdateBranch', id: string, name: string } } };
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
branchMapping: Scalars['String'];
}>;
export type CreateUpdateChannelOnAppMutation = { __typename?: 'RootMutation', updateChannel: { __typename?: 'UpdateChannelMutation', createUpdateChannelForApp: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } } };
export type GetBranchInfoQueryVariables = Exact<{
appId: Scalars['String'];
name: Scalars['String'];
@@ -4591,6 +4496,15 @@ export type CancelBuildMutationVariables = Exact<{
export type CancelBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', cancel: { __typename?: 'Build', id: string, status: BuildStatus } } };
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
appId: Scalars['ID'];
name: Scalars['String'];
branchMapping: Scalars['String'];
}>;
export type CreateUpdateChannelOnAppMutation = { __typename?: 'RootMutation', updateChannel: { __typename?: 'UpdateChannelMutation', createUpdateChannelForApp: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } } };
export type DeleteUpdateChannelMutationVariables = Exact<{
channelId: Scalars['ID'];
}>;
@@ -5017,7 +4931,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
@@ -5027,7 +4941,7 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -5083,7 +4997,6 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5093,7 +5006,6 @@ export type CreateIosSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5184,14 +5096,14 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5201,7 +5113,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
}>;
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5297,7 +5209,7 @@ export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __ty
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type CurrentUserQuery = { __typename?: 'RootQuery', meActor?: { __typename: 'Robot', firstName?: string | null, id: string, featureGates: any, isExpoAdmin: boolean, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | { __typename: 'User', username: string, id: string, featureGates: any, isExpoAdmin: boolean, primaryAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | null };
export type CurrentUserQuery = { __typename?: 'RootQuery', meActor?: { __typename: 'Robot', firstName?: string | null, id: string, isExpoAdmin: boolean, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | { __typename: 'User', username: string, id: string, isExpoAdmin: boolean, primaryAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }, accounts: Array<{ __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> }> } | null };
export type WebhooksByAppIdQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5318,9 +5230,9 @@ export type AccountFragment = { __typename?: 'Account', id: string, name: string
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -18,8 +18,7 @@ export const KeystoreGenerationUrlMutation = {
}
}
}
`,
{}
`
)
.toPromise()
);
@@ -28,16 +28,10 @@ export const SubmissionMutation = {
$appId: ID!
$config: AndroidSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createAndroidSubmission(
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
) {
submission {
id
@@ -66,16 +60,10 @@ export const SubmissionMutation = {
$appId: ID!
$config: IosSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createIosSubmission(
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
) {
submission {
id
@@ -8,17 +8,16 @@ import {
UploadSessionType,
} from '../generated';
export interface SignedUrl {
export interface PresignedPost {
url: string;
headers: Record<string, string>;
bucketKey: string;
fields: Record<string, string>;
}
export const UploadSessionMutation = {
async createUploadSessionAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType
): Promise<SignedUrl> {
): Promise<PresignedPost> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
@@ -30,13 +30,12 @@ export const UserQuery = {
id
...AccountFragment
}
featureGates
isExpoAdmin
}
}
${print(AccountFragmentNode)}
`,
{},
/* variables */ undefined,
{
additionalTypenames: ['User'],
}
@@ -16,7 +16,6 @@ export const BuildFragmentNode = gql`
artifacts {
buildUrl
xcodeBuildLogsUrl
applicationArchiveUrl
}
initiatingActor {
__typename
@@ -45,7 +44,6 @@ export const BuildFragmentNode = gql`
appBuildVersion
runtimeVersion
gitCommitHash
gitCommitMessage
initialQueuePosition
queuePosition
estimatedWaitTimeLeftSeconds
-4
View File
@@ -45,10 +45,6 @@ export default class Log {
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
}
public static fail(message: string): void {
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
}
public static succeed(message: string): void {
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
}
@@ -1,149 +1,128 @@
import { SubmitProfile } from '@expo/eas-json';
import path from 'path';
import { getStaticConfigFilePath, loadConfigAsync } from '../config/resolve';
import { getStaticConfigFilePath, loadConfigAsync } from '../config';
import { MetadataValidationError } from '../errors';
describe(getStaticConfigFilePath, () => {
const projectDir = '/app';
function mockProfile(metadataPath: string): SubmitProfile {
return { metadataPath } as any;
}
it(`returns same file for store.config.json`, () => {
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.config.json') })).toBe(
path.join(projectDir, 'store.config.json')
);
const metadataPath = 'store.config.json';
const expectedFile = path.join(projectDir, metadataPath);
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
it(`returns store.config.json for store.config.js`, () => {
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.config.js') })).toBe(
path.join(projectDir, 'store.config.json')
);
const metadataPath = 'store.config.js';
const expectedFile = path.join(projectDir, 'store.config.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
it(`returns store.staging.json file for store.staging.js`, () => {
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.staging.js') })).toBe(
path.join(projectDir, 'store.staging.json')
);
const metadataPath = 'store.staging.js';
const expectedFile = path.join(projectDir, 'store.staging.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
// This shouldn't be used IRL, but it tests if this function is working properly
it(`returns custom-name.json file for custom-name.js`, () => {
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('custom-name.js') })).toBe(
path.join(projectDir, 'custom-name.json')
);
const metadataPath = 'custom-name.js';
const expectedFile = path.join(projectDir, 'custom-name.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
});
describe(loadConfigAsync, () => {
const projectDir = path.resolve(__dirname, 'fixtures');
function mockProfile(metadataPath: string): SubmitProfile {
return { metadataPath } as any;
}
it(`throws when file doesn't exist`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('doesnt-exist.json') })
loadConfigAsync({ projectDir, metadataPath: 'doesnt-exist.json' })
).rejects.toThrow('file not found');
});
it(`throws when validation errors are found without skipping validation`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('invalid.config.json') })
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json' })
).rejects.toThrow('errors found');
});
it(`returns config from "store.config.json"`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('store.config.json') })
loadConfigAsync({ projectDir, metadataPath: 'store.config.json' })
).resolves.toHaveProperty('apple.copyright', 'ACME');
});
it(`returns config from "store.config.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('store.config.js') })
loadConfigAsync({ projectDir, metadataPath: 'store.config.js' })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns config from "store.function.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('store.function.js') })
loadConfigAsync({ projectDir, metadataPath: 'store.function.js' })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns config from "store.async.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('store.async.js') })
loadConfigAsync({ projectDir, metadataPath: 'store.async.js' })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns invalid config from "invalid.config.json`, async () => {
await expect(
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.config.json'),
skipValidation: true,
})
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.config.js`, async () => {
await expect(
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.config.js'),
skipValidation: true,
})
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.js', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.function.js`, async () => {
await expect(
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.function.js'),
skipValidation: true,
})
loadConfigAsync({ projectDir, metadataPath: 'invalid.function.js', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.async.js`, async () => {
await expect(
loadConfigAsync({
projectDir,
profile: mockProfile('invalid.async.js'),
skipValidation: true,
})
loadConfigAsync({ projectDir, metadataPath: 'invalid.async.js', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`throws invalid config type from "invalid-type.config.json"`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.config.json') })
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.json' })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.config.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.config.js') })
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.js' })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.function.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.function.js') })
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.function.js' })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.async.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.async.js') })
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.async.js' })
).rejects.toThrow(MetadataValidationError);
});
});
@@ -1,4 +0,0 @@
import { infoKeywordLength } from './infoKeywordLength';
import { infoRestrictedWords } from './infoRestrictedWords';
export const appleRules = [infoKeywordLength, infoRestrictedWords];
Loaded 100 of 188 files, more files were not shown because too many files have changed in this diff. Show more