mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20ebf24a20 | ||
|
|
427ececb0b | ||
|
|
f65670e18f | ||
|
|
41a0a4554d | ||
|
|
c1da053f8c | ||
|
|
57da02af9d | ||
|
|
b9c505bbdc | ||
|
|
633e6a6721 | ||
|
|
f7ddb4ba8f | ||
|
|
91eb2c467b | ||
|
|
f237a7e074 | ||
|
|
242c3cffb6 | ||
|
|
eee54af6ee | ||
|
|
86f86dbe86 | ||
|
|
cc99bb3326 | ||
|
|
08dc52c631 | ||
|
|
51db45a861 | ||
|
|
790853b3b2 | ||
|
|
23479acabc | ||
|
|
de965e3693 | ||
|
|
5c3afc5141 | ||
|
|
50814723c5 | ||
|
|
c6919e9fd6 | ||
|
|
6b220f12ae | ||
|
|
2aade4c0c8 | ||
|
|
f16c40a496 | ||
|
|
aed6725d17 | ||
|
|
ac6c240217 | ||
|
|
34ae7f5e0b | ||
|
|
40aa1c7d35 | ||
|
|
b128af464c | ||
|
|
1236152120 |
No files matched your search
@@ -32,6 +32,9 @@ jobs:
|
||||
# Remove this whenever we solved those OOM errors.
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
- run: yarn lint --max-warnings=0
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
working-directory: ./scripts
|
||||
- run: yarn lint-changelog
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: ${{ matrix.coverage }}
|
||||
with:
|
||||
|
||||
+72
-30
@@ -8,6 +8,48 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 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))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 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))
|
||||
@@ -66,10 +108,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))
|
||||
@@ -245,7 +287,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00/))
|
||||
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00))
|
||||
- Allow users to skip metadata validation. ([#1175](https://github.com/expo/eas-cli/pull/1175) by [@byCedric](https://github.com/byCedric))
|
||||
- Add experimental `--resource-class` flag for Build commands. ([#1138](https://github.com/expo/eas-cli/pull/1138) by [@christopherwalter](https://github.com/christopherwalter))
|
||||
- Truncate long messages for `eas update` command, rather than failing. ([#1178](https://github.com/expo/eas-cli/pull/1178) by [@kgc00](https://github.com/kgc00))
|
||||
@@ -282,13 +324,13 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
|
||||
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00))
|
||||
- Added first beta of `eas metadata` to sync store information using store configuration files ([#1136](https://github.com/expo/eas-cli/pull/1136) by [@bycedric](https://github.com/bycedric))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00/))
|
||||
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
|
||||
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00))
|
||||
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -298,7 +340,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00/))
|
||||
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -469,7 +511,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
|
||||
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot), [@dsokal](https://github.com/dsokal))
|
||||
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
|
||||
@@ -521,7 +563,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
|
||||
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates. ([#854](https://github.com/expo/eas-cli/pull/854) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Adds `eas update`, which can bundle and publish updates on a branch.
|
||||
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
|
||||
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
|
||||
@@ -567,7 +609,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
|
||||
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales), [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -603,7 +645,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pull/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
@@ -701,7 +743,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701), [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
@@ -733,7 +775,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
|
||||
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
|
||||
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
|
||||
```json
|
||||
@@ -779,7 +821,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
|
||||
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95), [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -791,7 +833,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project. ([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
|
||||
@@ -925,7 +967,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix wrong warning for `metro.config.js` check on Windows. ([#588](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
@@ -1005,7 +1047,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
|
||||
- Include the file extension for update's assets in the manifest fragment. ([#535](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
|
||||
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
@@ -1025,7 +1067,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
|
||||
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/520) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -1039,7 +1081,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
|
||||
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504), [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
|
||||
@@ -1119,15 +1161,15 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
|
||||
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
|
||||
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
|
||||
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469), [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439), [#440](https://github.com/expo/eas-cli/pull/440), [#438](https://github.com/expo/eas-cli/pull/438), [#443](https://github.com/expo/eas-cli/pull/443), [#447](https://github.com/expo/eas-cli/pull/447), [#451](https://github.com/expo/eas-cli/pull/451), [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448), [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
|
||||
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
@@ -1141,7 +1183,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Android credentials setup now on Graphql API. ([#427](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
|
||||
|
||||
@@ -1173,7 +1215,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🎉 New features
|
||||
|
||||
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
|
||||
- Add eas init command. ([#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
|
||||
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
@@ -1184,7 +1226,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
|
||||
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
|
||||
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
|
||||
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
|
||||
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
|
||||
@@ -1211,7 +1253,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [#357](https://github.com/expo/eas-cli/pull/357), [#361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
@@ -1272,7 +1314,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
|
||||
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -1297,7 +1339,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🎉 New features
|
||||
|
||||
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/311) by [@barthap](https://github.com/barthap))
|
||||
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
|
||||
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
@@ -1381,7 +1423,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
@@ -1408,7 +1450,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `"buildType" is not allowed` error. ([595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
|
||||
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||
packages/eas-cli/src/submit @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @dsokal
|
||||
|
||||
packages/eas-cli/src/update @kgc00 @wschurman
|
||||
packages/eas-cli/src/commands/update @kgc00 @wschurman
|
||||
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
|
||||
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
|
||||
|
||||
packages/eas-cli/schema @bycedric
|
||||
packages/eas-cli/src/metadata @bycedric
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "2.4.1"
|
||||
"version": "2.6.0"
|
||||
}
|
||||
+10
-9
@@ -12,24 +12,25 @@
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "./scripts/bin/run release",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "28.1.6",
|
||||
"@types/node": "18.6.5",
|
||||
"eslint": "8.21.0",
|
||||
"@types/jest": "29.2.0",
|
||||
"@types/node": "18.11.5",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-universe": "11.1.0",
|
||||
"eslint-plugin-async-protect": "2.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "28.1.3",
|
||||
"jest-watch-typeahead": "2.0.0",
|
||||
"lerna": "4.0.0",
|
||||
"oclif": "3.1.2",
|
||||
"jest": "29.2.2",
|
||||
"jest-watch-typeahead": "2.2.0",
|
||||
"lerna": "6.0.1",
|
||||
"oclif": "3.2.22",
|
||||
"prettier": "2.7.1",
|
||||
"ts-jest": "28.0.7",
|
||||
"typescript": "4.7.4"
|
||||
"ts-jest": "29.0.3",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
|
||||
+89
-49
@@ -89,6 +89,7 @@ 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)
|
||||
@@ -97,6 +98,7 @@ eas --help COMMAND
|
||||
* [`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)
|
||||
@@ -125,7 +127,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -142,7 +144,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -159,7 +161,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -173,7 +175,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -202,7 +204,7 @@ EXAMPLES
|
||||
$ eas autocomplete --refresh-cache
|
||||
```
|
||||
|
||||
_See code: [@expo/plugin-autocomplete](https://github.com/expo/plugin-autocomplete/blob/v1.4.0/src/commands/autocomplete/index.ts)_
|
||||
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.4/src/commands/autocomplete/index.ts)_
|
||||
|
||||
## `eas branch:create [NAME]`
|
||||
|
||||
@@ -223,7 +225,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -244,7 +246,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -264,7 +266,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -284,7 +286,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -307,7 +309,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -337,7 +339,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -354,7 +356,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -371,7 +373,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -406,7 +408,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -443,7 +445,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -490,7 +492,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -509,7 +511,7 @@ DESCRIPTION
|
||||
Update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -526,7 +528,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -547,7 +549,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -569,7 +571,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -589,7 +591,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -612,7 +614,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -631,7 +633,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -648,7 +650,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -662,7 +664,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -682,7 +684,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -703,7 +705,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -717,7 +719,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -731,7 +733,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -751,7 +753,7 @@ DESCRIPTION
|
||||
display help for eas-cli
|
||||
```
|
||||
|
||||
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.3.0/src/commands/help.ts)_
|
||||
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.16/src/commands/help.ts)_
|
||||
|
||||
## `eas init`
|
||||
|
||||
@@ -803,6 +805,24 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
## `eas metadata:lint`
|
||||
|
||||
validate the local store configuration
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:lint [--json] [--profile <value>]
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
generate the local store configuration from the app stores
|
||||
@@ -818,7 +838,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -835,7 +855,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas open`
|
||||
|
||||
@@ -849,7 +869,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -863,7 +883,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -885,7 +905,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -908,7 +928,7 @@ DESCRIPTION
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -926,7 +946,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -940,7 +960,27 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/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.6.0/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -970,7 +1010,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1000,7 +1040,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1017,7 +1057,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1038,7 +1078,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1060,7 +1100,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -1080,7 +1120,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1101,7 +1141,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1121,7 +1161,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1138,7 +1178,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1160,7 +1200,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1177,7 +1217,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.4.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
Generated
+492
-52
@@ -2274,6 +2274,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "revokeEndpoint",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "tokenEndpoint",
|
||||
"description": "",
|
||||
@@ -2442,6 +2454,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "revokeEndpoint",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "tokenEndpoint",
|
||||
"description": "",
|
||||
@@ -2734,6 +2758,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "revokeEndpoint",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "tokenEndpoint",
|
||||
"description": "",
|
||||
@@ -11913,6 +11949,18 @@
|
||||
"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": "",
|
||||
@@ -12391,12 +12439,16 @@
|
||||
},
|
||||
{
|
||||
"name": "resourceClass",
|
||||
"description": "",
|
||||
"description": "The builder resource class requested by the developer",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "BuildResourceClass",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "BuildResourceClass",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
@@ -15406,6 +15458,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "archiveSource",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "SubmissionArchiveSourceInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "archiveUrl",
|
||||
"description": "",
|
||||
@@ -15641,6 +15705,22 @@
|
||||
"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,
|
||||
@@ -15712,6 +15792,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "archiveSource",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "SubmissionArchiveSourceInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "archiveUrl",
|
||||
"description": "",
|
||||
@@ -16080,6 +16172,60 @@
|
||||
"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",
|
||||
@@ -17284,7 +17430,7 @@
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": "1",
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
@@ -17296,7 +17442,7 @@
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": "50",
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
@@ -17305,17 +17451,9 @@
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "GitHubAppInstallationAccessibleRepository",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
"kind": "OBJECT",
|
||||
"name": "GitHubRepositoryPaginationResult",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
@@ -17468,6 +17606,22 @@
|
||||
"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": "",
|
||||
@@ -17716,6 +17870,39 @@
|
||||
"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": "searchRepositories",
|
||||
"description": "",
|
||||
@@ -17736,30 +17923,6 @@
|
||||
"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": "",
|
||||
@@ -17781,17 +17944,9 @@
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "GitHubAppInstallationAccessibleRepository",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
"kind": "OBJECT",
|
||||
"name": "GitHubRepositoryPaginationResult",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
@@ -17883,6 +18038,22 @@
|
||||
},
|
||||
"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,
|
||||
@@ -18161,6 +18332,57 @@
|
||||
"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",
|
||||
@@ -19474,6 +19696,39 @@
|
||||
"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",
|
||||
@@ -19887,6 +20142,39 @@
|
||||
"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",
|
||||
@@ -19991,6 +20279,50 @@
|
||||
"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",
|
||||
@@ -24394,6 +24726,22 @@
|
||||
"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",
|
||||
@@ -26444,6 +26792,86 @@
|
||||
],
|
||||
"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",
|
||||
@@ -28222,6 +28650,12 @@
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "EAS_BUILD_GCS_PROJECT_SOURCES",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "EAS_BUILD_PROJECT_SOURCES",
|
||||
"description": "",
|
||||
@@ -28233,6 +28667,12 @@
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "EAS_SUBMIT_GCS_APP_ARCHIVE",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "2.4.1",
|
||||
"version": "2.6.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -9,50 +9,53 @@
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.34",
|
||||
"@expo/code-signing-certificates": "0.0.2",
|
||||
"@expo/code-signing-certificates": "0.0.3",
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.94",
|
||||
"@expo/eas-json": "2.2.0",
|
||||
"@expo/eas-build-job": "0.2.96",
|
||||
"@expo/eas-json": "2.6.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
"@expo/package-manager": "0.0.56",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.18",
|
||||
"@expo/plugin-autocomplete": "1.4.0",
|
||||
"@expo/plugin-help": "5.3.0",
|
||||
"@expo/plugin-warn-if-update-available": "2.3.0",
|
||||
"@expo/plugin-help": "5.1.16",
|
||||
"@expo/plugin-warn-if-update-available": "2.4.0",
|
||||
"@expo/prebuild-config": "5.0.3",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.13.10",
|
||||
"@urql/core": "2.6.1",
|
||||
"@urql/exchange-retry": "0.3.3",
|
||||
"ajv": "^6.12.6",
|
||||
"@oclif/core": "1.19.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.4",
|
||||
"@segment/ajv-human-errors": "^2.1.2",
|
||||
"@urql/core": "3.0.5",
|
||||
"@urql/exchange-retry": "1.0.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"better-opn": "3.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.11.2",
|
||||
"cli-table3": "0.6.2",
|
||||
"dateformat": "4.6.3",
|
||||
"dotenv": "16.0.3",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.2.11",
|
||||
"fast-glob": "3.2.12",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"getenv": "1.0.0",
|
||||
"gradle-to-js": "2.0.1",
|
||||
"graphql": "16.5.0",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"ignore": "5.2.0",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.2",
|
||||
"joi": "17.6.0",
|
||||
"jks-js": "1.1.0",
|
||||
"joi": "17.6.4",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
@@ -68,33 +71,33 @@
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.7",
|
||||
"semver": "7.3.8",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"uuid": "9.0.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.11.5",
|
||||
"@graphql-codegen/cli": "2.13.7",
|
||||
"@graphql-codegen/introspection": "2.2.1",
|
||||
"@graphql-codegen/typescript": "2.7.3",
|
||||
"@graphql-codegen/typescript-operations": "2.5.3",
|
||||
"@graphql-codegen/typescript": "2.8.0",
|
||||
"@graphql-codegen/typescript-operations": "2.5.5",
|
||||
"@types/cli-progress": "3.11.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.0.4",
|
||||
"@types/node-forge": "1.3.0",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.0.14",
|
||||
"@types/semver": "7.3.10",
|
||||
"@types/tar": "6.1.2",
|
||||
"@types/prompts": "2.4.1",
|
||||
"@types/semver": "7.3.12",
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
@@ -104,10 +107,10 @@
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.9",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-deepmerge": "2.0.5",
|
||||
"ts-deepmerge": "4.0.0",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -129,7 +132,7 @@
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@expo/plugin-autocomplete",
|
||||
"@oclif/plugin-autocomplete",
|
||||
"@expo/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
],
|
||||
|
||||
@@ -778,7 +778,7 @@
|
||||
},
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"const": 0,
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
export type AnalyticsEvent = CommandEvent | BuildEvent | SubmissionEvent | MetadataEvent;
|
||||
|
||||
export enum CommandEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export enum MetadataEvent {
|
||||
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
||||
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
||||
}
|
||||
|
||||
export type AnalyticsEventProperties = Record<string, string | number | boolean>;
|
||||
|
||||
/**
|
||||
* The interface for commands to use to log events to analytics.
|
||||
*/
|
||||
export interface Analytics {
|
||||
logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for commands to use to orchestrate the analytics system. Should only be necessary to use
|
||||
* this within EASCommand.
|
||||
*/
|
||||
export interface AnalyticsWithOrchestration extends Analytics {
|
||||
setActor(actor: Actor): void;
|
||||
flushAsync(): Promise<void>;
|
||||
}
|
||||
|
||||
const USER_SETTINGS_KEY_AMPLITUDE_ENABLED = 'amplitudeEnabled';
|
||||
const USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID = 'amplitudeDeviceId';
|
||||
const USER_SETTINGS_KEY_ANALYTICS_ENABLED = 'analyticsEnabled';
|
||||
const USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID = 'analyticsDeviceId';
|
||||
|
||||
/**
|
||||
* Sets the user's analytics enabled preference. Note that this will only take effect
|
||||
* upon the next run of the CLI.
|
||||
*/
|
||||
export async function setAnalyticsEnabledAsync(enabled: boolean): Promise<void> {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's analytics enabled preference.
|
||||
*/
|
||||
export async function getAnalyticsEnabledAsync(): Promise<boolean> {
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, null);
|
||||
return !!analyticsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of Analytics based on the user's analytics enabled preferences.
|
||||
*/
|
||||
export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED, null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED);
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync(
|
||||
USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID,
|
||||
null
|
||||
);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID);
|
||||
}
|
||||
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
|
||||
if (!analyticsEnabled) {
|
||||
return new NoOpAnalytics();
|
||||
}
|
||||
|
||||
const persistedDeviceId = await UserSettings.getAsync(
|
||||
USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID,
|
||||
null
|
||||
);
|
||||
const deviceId = persistedDeviceId ?? uuidv4();
|
||||
if (!persistedDeviceId) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, deviceId);
|
||||
}
|
||||
return new RudderstackAnalytics(deviceId);
|
||||
}
|
||||
|
||||
class NoOpAnalytics implements AnalyticsWithOrchestration {
|
||||
logEvent(): void {}
|
||||
setActor(): void {}
|
||||
async flushAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
const RudderstackAnalyticsConfig =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
class RudderstackAnalytics implements AnalyticsWithOrchestration {
|
||||
private readonly rudderstackClient = new RudderAnalytics(
|
||||
RudderstackAnalyticsConfig.rudderstackWriteKey,
|
||||
new URL('/v1/batch', RudderstackAnalyticsConfig.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
|
||||
private identifiedActor: Actor | null = null;
|
||||
|
||||
constructor(private readonly persistentDeviceId: string) {
|
||||
// identify once with just anonymous ID. Once the actor is fetched, re-indentify with
|
||||
// both so that they can be associated
|
||||
this.rudderstackClient.identify({
|
||||
anonymousId: persistentDeviceId,
|
||||
});
|
||||
}
|
||||
|
||||
public setActor(actor: Actor): void {
|
||||
if (this.identifiedActor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rudderstackClient.identify({
|
||||
userId: actor.id,
|
||||
anonymousId: this.persistentDeviceId,
|
||||
traits: {
|
||||
username: getActorDisplayName(actor),
|
||||
user_id: actor.id,
|
||||
user_type: actor.__typename,
|
||||
},
|
||||
});
|
||||
this.identifiedActor = actor;
|
||||
}
|
||||
|
||||
public logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void {
|
||||
const userId = this.identifiedActor?.id;
|
||||
const deviceId = this.persistentDeviceId;
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
this.rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: this.getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
public async flushAsync(): Promise<void> {
|
||||
await this.rudderstackClient.flush();
|
||||
}
|
||||
|
||||
private getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
import { Analytics, Event } from './events';
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
|
||||
|
||||
export async function withAnalyticsAsync<Result>(
|
||||
analytics: Analytics,
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
attemptEvent: Event;
|
||||
successEvent: Event;
|
||||
failureEvent: Event;
|
||||
trackingCtx: TrackingContext;
|
||||
{
|
||||
attemptEvent,
|
||||
successEvent,
|
||||
failureEvent,
|
||||
properties,
|
||||
}: {
|
||||
attemptEvent: AnalyticsEvent;
|
||||
successEvent: AnalyticsEvent;
|
||||
failureEvent: AnalyticsEvent;
|
||||
properties: AnalyticsEventProperties;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
analytics.logEvent(attemptEvent, properties);
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
analytics.logEvent(successEvent, properties);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
analytics.logEvent(failureEvent, {
|
||||
...properties,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { logEvent } from './rudderstackClient';
|
||||
export type Event = BuildEvent | SubmissionEvent | MetadataEvent;
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export enum MetadataEvent {
|
||||
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
||||
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
||||
}
|
||||
|
||||
export class Analytics {
|
||||
static logEvent(name: Event, properties: Record<string, any>): void {
|
||||
logEvent(name, properties);
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
let rudderstackClient: RudderAnalytics | null = null;
|
||||
let userIdentified = false;
|
||||
let identifyData: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
traits: Record<string, any>;
|
||||
} | null = null;
|
||||
|
||||
export async function initAsync(): Promise<void> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync('amplitudeEnabled');
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
|
||||
}
|
||||
// TODO: cut here
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync('analyticsEnabled', false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
|
||||
if (analyticsEnabled) {
|
||||
const config =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
rudderstackClient = new RudderAnalytics(
|
||||
config.rudderstackWriteKey,
|
||||
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
|
||||
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
|
||||
const deviceId = savedDeviceId ?? uuidv4();
|
||||
if (!savedDeviceId) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', deviceId);
|
||||
}
|
||||
|
||||
identifyData = {
|
||||
userId,
|
||||
deviceId,
|
||||
traits,
|
||||
};
|
||||
|
||||
ensureUserIdentified();
|
||||
}
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (rudderstackClient) {
|
||||
await rudderstackClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent(name: string, properties: Record<string, any> = {}): void {
|
||||
if (!rudderstackClient) {
|
||||
return;
|
||||
}
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
function ensureUserIdentified(): void {
|
||||
if (!rudderstackClient || userIdentified || !identifyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
traits: identifyData.traits,
|
||||
});
|
||||
userIdentified = true;
|
||||
}
|
||||
|
||||
function getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { UpdateBranchFragment } from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import Log from '../log';
|
||||
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
|
||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
@@ -102,34 +101,13 @@ function renderPageOfBranches(
|
||||
if (json) {
|
||||
printJsonOnlyOutput(currentPage);
|
||||
} else {
|
||||
const table = new CliTable({
|
||||
head: ['Branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
table.push(
|
||||
...currentPage.map(branch => {
|
||||
if (branch.updates.length === 0) {
|
||||
return [branch.name, 'N/A', 'N/A', 'N/A', 'N/A'];
|
||||
}
|
||||
|
||||
const latestUpdateOnBranch = branch.updates[0];
|
||||
return [
|
||||
branch.name,
|
||||
formatUpdateMessage(latestUpdateOnBranch),
|
||||
latestUpdateOnBranch.runtimeVersion,
|
||||
latestUpdateOnBranch.group,
|
||||
getPlatformsForGroup({
|
||||
group: latestUpdateOnBranch.group,
|
||||
updates: branch.updates,
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branches:'));
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(table.toString());
|
||||
Log.log(
|
||||
currentPage
|
||||
.map(branch => formatBranch(getBranchDescription(branch)))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -78,21 +78,26 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
|
||||
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.syncProjectConfigurationAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -192,6 +197,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
let projectTarballPath;
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
@@ -232,7 +238,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
@@ -250,6 +256,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||
@@ -269,7 +276,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
@@ -46,9 +46,10 @@ export interface BuildContext<T extends Platform> {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
message?: string;
|
||||
trackingCtx: TrackingContext;
|
||||
analyticsEventProperties: AnalyticsEventProperties;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
workflow: Workflow;
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
|
||||
@@ -5,8 +5,7 @@ import getenv from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
@@ -33,6 +32,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
@@ -48,6 +48,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
message?: string;
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
|
||||
@@ -64,6 +65,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
});
|
||||
@@ -73,7 +75,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
const analyticsEventProperties = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
@@ -83,7 +85,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
no_wait: noWait,
|
||||
run_from_ci: runFromCI,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName: account.name,
|
||||
@@ -101,9 +103,10 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
trackingCtx,
|
||||
analyticsEventProperties,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
workflow,
|
||||
message,
|
||||
runFromCI,
|
||||
@@ -131,7 +134,7 @@ function getDevClientEventProperties({
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
}): Partial<AnalyticsEventProperties> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import semver from 'semver';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.114';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.115';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
? 'simulator'
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata: Metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
trackingContext: ctx.analyticsEventProperties,
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
|
||||
@@ -5,6 +5,8 @@ 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';
|
||||
@@ -53,6 +55,69 @@ 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,6 +10,7 @@ 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 {
|
||||
@@ -116,6 +117,7 @@ function resolveResourceClass(
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
@@ -169,6 +171,7 @@ export async function runBuildAndSubmitAsync(
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
@@ -271,6 +274,7 @@ async function prepareAndStartBuildAsync({
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
projectDir: string;
|
||||
@@ -281,6 +285,7 @@ async function prepareAndStartBuildAsync({
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
@@ -297,6 +302,7 @@ async function prepareAndStartBuildAsync({
|
||||
message: flags.message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
|
||||
@@ -378,6 +384,7 @@ async function prepareAndStartSubmissionAsync({
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
actor: buildCtx.user,
|
||||
graphqlClient: buildCtx.graphqlClient,
|
||||
analytics: buildCtx.analytics,
|
||||
projectId: buildCtx.projectId,
|
||||
exp: buildCtx.exp,
|
||||
});
|
||||
|
||||
@@ -153,14 +153,13 @@ function renderPageOfChannels(
|
||||
printJsonOnlyOutput({ currentPage });
|
||||
} else {
|
||||
for (const channel of currentPage) {
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
|
||||
if (currentPage.indexOf(channel) < currentPage.length - 1) {
|
||||
Log.log(`\n${chalk.dim('———')}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,13 +173,8 @@ function renderPageOfBranchesOnChannel(
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
|
||||
} else {
|
||||
// The channel details contain both the branch and latest update group
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channel.name },
|
||||
{ label: 'ID', value: channel.id },
|
||||
])
|
||||
);
|
||||
logChannelDetails(channelWithNewBranches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import CliTable from 'cli-table3';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import Log from '../log';
|
||||
import { UPDATE_COLUMNS, getUpdateGroupDescriptionsWithBranch } from '../update/utils';
|
||||
import {
|
||||
FormattedBranchDescription,
|
||||
formatBranch,
|
||||
getUpdateGroupDescriptionsWithBranch,
|
||||
} from '../update/utils';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -75,38 +79,39 @@ export function getBranchMapping(branchMappingString?: string): {
|
||||
|
||||
export function logChannelDetails(channel: UpdateChannelObject): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const updateChannelsTable = new CliTable({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
if (branchMapping.data.length > 2) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
|
||||
|
||||
for (const currentBranch of channel.updateBranches) {
|
||||
const updateGroupDescriptions = getUpdateGroupDescriptionsWithBranch(
|
||||
currentBranch.updateGroups
|
||||
const branchDescription = channel.updateBranches.flatMap(branch => {
|
||||
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
|
||||
branch.updateGroups
|
||||
);
|
||||
|
||||
const isRolloutBranch = isRollout && rolloutBranchIds.includes(currentBranch.id);
|
||||
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === currentBranch.id;
|
||||
const isRolloutBranch = isRollout && rolloutBranchIds.includes(branch.id);
|
||||
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === branch.id;
|
||||
let rolloutPercentNumber: number | undefined = undefined;
|
||||
if (isRolloutBranch) {
|
||||
rolloutPercentNumber = isBaseBranch ? rolloutPercent! * 100 : (1 - rolloutPercent!) * 100;
|
||||
}
|
||||
|
||||
updateGroupDescriptions.forEach(({ branch, message, runtimeVersion, group, platforms }) => {
|
||||
updateChannelsTable.push([
|
||||
return updateGroupWithBranchDescriptions.map(
|
||||
({ branch, ...updateGroup }): FormattedBranchDescription => ({
|
||||
branch,
|
||||
...(isRolloutBranch
|
||||
? [isBaseBranch ? `${rolloutPercent! * 100}%` : `${(1 - rolloutPercent!) * 100}%`]
|
||||
: []),
|
||||
message,
|
||||
runtimeVersion,
|
||||
group,
|
||||
platforms,
|
||||
]);
|
||||
});
|
||||
branchRolloutPercentage: rolloutPercentNumber,
|
||||
update: updateGroup,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (branchDescription.length === 0) {
|
||||
Log.log(chalk.dim('No branches are pointed to this channel.'));
|
||||
} else {
|
||||
Log.log(
|
||||
branchDescription
|
||||
.map(description => formatBranch(description))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
Log.log(updateChannelsTable.toString());
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Command } from '@oclif/core';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
AnalyticsWithOrchestration,
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import AnalyticsContextField from './context/AnalyticsContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField';
|
||||
import LoggedInContextField from './context/LoggedInContextField';
|
||||
@@ -76,10 +77,19 @@ export default abstract class EasCommand extends Command {
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server. Returns the project config in the context.
|
||||
* This also requires the user to be logged in (getProjectIdAsync requires logged in), so also expose that context.
|
||||
* Exposing the loggedIn context here helps us guarantee user identification for logging purposes.
|
||||
*/
|
||||
ProjectConfig: {
|
||||
loggedIn: new LoggedInContextField(),
|
||||
projectConfig: new ProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Analytics manager. Returns the analytics manager in the context for use by the command.
|
||||
*/
|
||||
Analytics: {
|
||||
analytics: new AnalyticsContextField(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -106,37 +116,52 @@ export default abstract class EasCommand extends Command {
|
||||
): Promise<ContextOutput<C>> {
|
||||
const contextDefinition = commandClass.contextDefinition;
|
||||
|
||||
const contextValuePairs = await Promise.all(
|
||||
Object.keys(contextDefinition).map(async contextKey => {
|
||||
return [
|
||||
contextKey,
|
||||
await contextDefinition[contextKey].getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager: this.sessionManager,
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
// do these serially so that they don't do things like ask for login twice in parallel
|
||||
const contextValuePairs = [];
|
||||
for (const [contextKey, contextField] of Object.entries(contextDefinition)) {
|
||||
contextValuePairs.push([
|
||||
contextKey,
|
||||
await contextField.getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager: this.sessionManager,
|
||||
analytics: this.analytics,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return Object.fromEntries(contextValuePairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user session manager. Responsible for coordinating all user session related state.
|
||||
* If needed in a subclass, SessionManager ContextOption.
|
||||
* If needed in a subclass, use the SessionManager ContextOption.
|
||||
*/
|
||||
private readonly sessionManager = new SessionManager();
|
||||
private sessionManagerInternal?: SessionManager;
|
||||
private get sessionManager(): SessionManager {
|
||||
return nullthrows(this.sessionManagerInternal);
|
||||
}
|
||||
|
||||
/**
|
||||
* The analytics manager. Used for logging analytics.
|
||||
* It is set up here to ensure a consistent set up.
|
||||
*/
|
||||
private analyticsInternal?: AnalyticsWithOrchestration;
|
||||
private get analytics(): AnalyticsWithOrchestration {
|
||||
return nullthrows(this.analyticsInternal);
|
||||
}
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
await initAnalyticsAsync();
|
||||
this.analyticsInternal = await createAnalyticsAsync();
|
||||
this.sessionManagerInternal = new SessionManager(this.analytics);
|
||||
|
||||
// this is needed for logEvent call below as it identifies the user in the analytics system
|
||||
// if possible
|
||||
await this.sessionManager.getUserAsync();
|
||||
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
this.analytics.logEvent(CommandEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
@@ -147,7 +172,7 @@ export default abstract class EasCommand extends Command {
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
override async finally(err: Error): Promise<any> {
|
||||
await flushAnalyticsAsync();
|
||||
await this.analytics.flushAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
|
||||
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/SessionManager');
|
||||
jest.mock('../../analytics/rudderstackClient', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
|
||||
jest.mock('../../analytics/AnalyticsManager', () => {
|
||||
const { CommandEvent } = jest.requireActual('../../analytics/AnalyticsManager');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
CommandEvent,
|
||||
createAnalyticsAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -25,10 +23,16 @@ afterAll(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
});
|
||||
|
||||
const analytics: AnalyticsWithOrchestration = {
|
||||
logEvent: jest.fn((): void => {}),
|
||||
setActor: jest.fn((): void => {}),
|
||||
flushAsync: jest.fn(async (): Promise<void> => {}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(initAsync).mockReset();
|
||||
jest.mocked(flushAsync).mockReset();
|
||||
jest.mocked(logEvent).mockReset();
|
||||
jest.resetAllMocks();
|
||||
|
||||
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
|
||||
});
|
||||
|
||||
const createTestEasCommand = (): typeof EasCommand => {
|
||||
@@ -61,21 +65,21 @@ describe(EasCommand.name, () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
expect(createAnalyticsAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
expect(analytics.logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
@@ -90,7 +94,7 @@ describe(EasCommand.name, () => {
|
||||
});
|
||||
} catch {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
|
||||
export default class AnalyticsContextField extends ContextField<Analytics> {
|
||||
async getValueAsync({ analytics }: ContextOptions): Promise<Analytics> {
|
||||
return analytics;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
|
||||
export interface ContextOptions {
|
||||
sessionManager: SessionManager;
|
||||
analytics: Analytics;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
|
||||
export default class LoggedInContextField extends ContextField<{
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
return { actor, graphqlClient };
|
||||
|
||||
return {
|
||||
actor,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
|
||||
export default class MaybeLoggedInContextField extends ContextField<{
|
||||
actor: Actor | null;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
|
||||
actor: Actor | null;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
const authenticationInfo = {
|
||||
@@ -17,7 +21,12 @@ export default class MaybeLoggedInContextField extends ContextField<{
|
||||
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const actor = await sessionManager.getUserAsync();
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
return { actor: actor ?? null, graphqlClient };
|
||||
return {
|
||||
actor: actor ?? null,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
};
|
||||
}
|
||||
}
|
||||
+1
@@ -51,6 +51,7 @@ describe(getProjectIdAsync, () => {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
},
|
||||
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AnyVariables,
|
||||
Client,
|
||||
OperationContext,
|
||||
OperationResult,
|
||||
@@ -17,9 +18,9 @@ import { getExpoApiBaseUrl } from '../../../api';
|
||||
import { httpsProxyAgent } from '../../../fetch';
|
||||
|
||||
export interface ExpoGraphqlClient extends Client {
|
||||
query<Data = any, Variables extends object = object>(
|
||||
query<Data = any, Variables extends AnyVariables = AnyVariables>(
|
||||
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
|
||||
variables: Variables | undefined,
|
||||
variables: Variables,
|
||||
context: Partial<OperationContext> & { additionalTypenames: string[] }
|
||||
): PromisifiedSource<OperationResult<Data, Variables>>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import env from '../../env';
|
||||
import { FeatureGateKey } from './FeatureGateKey';
|
||||
|
||||
function parseFeatureGateEnvVariableValue(value: string | undefined): string[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',').map(v => v.trim());
|
||||
}
|
||||
|
||||
export default class FeatureGateEnvOverrides {
|
||||
private readonly map = new Map<string, boolean>();
|
||||
|
||||
constructor() {
|
||||
const overrideEnableGateKeys = new Set(parseFeatureGateEnvVariableValue(env.featureGateEnable));
|
||||
const overrideDisableGateKeys = new Set(
|
||||
parseFeatureGateEnvVariableValue(env.featureGateDisable)
|
||||
);
|
||||
|
||||
for (const overrideEnableKey of overrideEnableGateKeys) {
|
||||
if (overrideDisableGateKeys.has(overrideEnableKey)) {
|
||||
continue;
|
||||
}
|
||||
this.map.set(overrideEnableKey, true);
|
||||
}
|
||||
for (const overrideDisableGateKey of overrideDisableGateKeys) {
|
||||
this.map.set(overrideDisableGateKey, false);
|
||||
}
|
||||
}
|
||||
|
||||
public isOverridden(key: FeatureGateKey): boolean {
|
||||
return this.map.has(key) ?? false;
|
||||
}
|
||||
|
||||
public getOverride(key: FeatureGateKey): boolean {
|
||||
return nullthrows(this.map.get(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum FeatureGateKey {
|
||||
// for tests
|
||||
TEST = 'test',
|
||||
}
|
||||
|
||||
export const featureGateDefaultValueWhenNoServerValue: Record<FeatureGateKey, boolean> = {
|
||||
[FeatureGateKey.TEST]: true,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FeatureGateKey } from './FeatureGateKey';
|
||||
|
||||
export const setOverride = (_key: FeatureGateKey, _enabled: boolean): void => {
|
||||
throw new Error('Must use mocked FeatureGateTestOverrides');
|
||||
};
|
||||
|
||||
export const removeOverride = (_key: FeatureGateKey): void => {
|
||||
throw new Error('Must use mocked FeatureGateTestOverrides');
|
||||
};
|
||||
|
||||
export const isOverridden = (_key: FeatureGateKey): boolean => false;
|
||||
|
||||
export const getOverride = (_key: FeatureGateKey): boolean => {
|
||||
throw new Error('Must use mocked FeatureGateTestOverrides');
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import FeatureGateEnvOverrides from './FeatureGateEnvOverrides';
|
||||
import { FeatureGateKey, featureGateDefaultValueWhenNoServerValue } from './FeatureGateKey';
|
||||
import * as FeatureGateTestOverrides from './FeatureGateTestOverrides';
|
||||
|
||||
export default class FeatureGating {
|
||||
constructor(
|
||||
private readonly serverValues: { [key: string]: boolean },
|
||||
private readonly envOverrides: FeatureGateEnvOverrides
|
||||
) {}
|
||||
|
||||
public isEnabled(experimentKey: FeatureGateKey): boolean {
|
||||
if (process.env.NODE_ENV === 'test' && FeatureGateTestOverrides.isOverridden(experimentKey)) {
|
||||
return FeatureGateTestOverrides.getOverride(experimentKey);
|
||||
}
|
||||
|
||||
if (this.envOverrides.isOverridden(experimentKey)) {
|
||||
return this.envOverrides.getOverride(experimentKey);
|
||||
}
|
||||
|
||||
if (experimentKey in this.serverValues) {
|
||||
return this.serverValues[experimentKey];
|
||||
}
|
||||
|
||||
return featureGateDefaultValueWhenNoServerValue[experimentKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test gate override methods
|
||||
*/
|
||||
|
||||
public static overrideKeyForScope(
|
||||
key: FeatureGateKey,
|
||||
enabled: boolean,
|
||||
scope: () => void
|
||||
): void {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error(`Cannot overrideKeyForScope outside of test environment`);
|
||||
}
|
||||
|
||||
FeatureGateTestOverrides.setOverride(key, enabled);
|
||||
try {
|
||||
scope();
|
||||
} finally {
|
||||
FeatureGateTestOverrides.removeOverride(key);
|
||||
}
|
||||
}
|
||||
|
||||
public static async overrideKeyForScopeAsync(
|
||||
key: FeatureGateKey,
|
||||
enabled: boolean,
|
||||
scope: () => Promise<void>
|
||||
): Promise<void> {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error(`Cannot overrideKeyForScopeAsync outside of test environment`);
|
||||
}
|
||||
|
||||
FeatureGateTestOverrides.setOverride(key, enabled);
|
||||
try {
|
||||
await scope();
|
||||
} finally {
|
||||
FeatureGateTestOverrides.removeOverride(key);
|
||||
}
|
||||
}
|
||||
|
||||
public static overrideKeyForEachInTest(key: FeatureGateKey, enabled: boolean): void {
|
||||
beforeEach(() => FeatureGateTestOverrides.setOverride(key, enabled));
|
||||
afterEach(() => FeatureGateTestOverrides.removeOverride(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FeatureGateKey } from '../FeatureGateKey';
|
||||
|
||||
const map = new Map();
|
||||
|
||||
export const setOverride = (key: FeatureGateKey, enabled: boolean): void => {
|
||||
map.set(key, enabled);
|
||||
};
|
||||
|
||||
export const removeOverride = (key: FeatureGateKey): void => {
|
||||
map.delete(key);
|
||||
};
|
||||
|
||||
export const isOverridden = (key: FeatureGateKey): boolean => map.has(key);
|
||||
|
||||
export const getOverride = (key: FeatureGateKey): boolean => {
|
||||
if (map.has(key)) {
|
||||
return map.get(key);
|
||||
}
|
||||
throw new Error(`Key ${key} not overridden`);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import env from '../../../env';
|
||||
import FeatureGateEnvOverrides from '../FeatureGateEnvOverrides';
|
||||
import { FeatureGateKey } from '../FeatureGateKey';
|
||||
import FeatureGating from '../FeatureGating';
|
||||
|
||||
jest.mock('../FeatureGateTestOverrides');
|
||||
|
||||
describe(FeatureGating, () => {
|
||||
it('supports scoped overriding', () => {
|
||||
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
|
||||
FeatureGating.overrideKeyForScope(FeatureGateKey.TEST, false, () => {
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
it('supports async scoped overriding', async () => {
|
||||
const featureGating = new FeatureGating({}, new FeatureGateEnvOverrides());
|
||||
await FeatureGating.overrideKeyForScopeAsync(FeatureGateKey.TEST, false, async () => {
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
it('uses value from the server, falls back to defined value', () => {
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
|
||||
const featureGating2 = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: false },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating2.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
|
||||
const featureGating3 = new FeatureGating({}, new FeatureGateEnvOverrides());
|
||||
expect(featureGating3.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
describe('environment variable overriding', () => {
|
||||
afterAll(() => {
|
||||
env.featureGateEnable = undefined;
|
||||
env.featureGateDisable = undefined;
|
||||
});
|
||||
|
||||
it('supports env overrides affirmative', () => {
|
||||
env.featureGateEnable = [FeatureGateKey.TEST].join(',');
|
||||
env.featureGateDisable = [].join(',');
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: false },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
|
||||
it('supports env overrides negative', () => {
|
||||
env.featureGateEnable = [].join(',');
|
||||
env.featureGateDisable = [FeatureGateKey.TEST].join(',');
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('jest scoped overriding', () => {
|
||||
FeatureGating.overrideKeyForEachInTest(FeatureGateKey.TEST, false);
|
||||
|
||||
it('supports jest scoped overriding', () => {
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removal of jest scoped overriding', () => {
|
||||
it('jest scoped overriding is only for describe scope above', () => {
|
||||
const featureGating = new FeatureGating(
|
||||
{ [FeatureGateKey.TEST]: true },
|
||||
new FeatureGateEnvOverrides()
|
||||
);
|
||||
expect(featureGating.isEnabled(FeatureGateKey.TEST)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { OptionFlag } from '@oclif/core/lib/interfaces';
|
||||
|
||||
import { EasNonInteractiveAndJsonFlags } from './flags';
|
||||
|
||||
@@ -37,7 +38,7 @@ export const getLimitFlagWithCustomValues = ({
|
||||
}: {
|
||||
defaultTo: number;
|
||||
limit: number;
|
||||
}): any =>
|
||||
}): OptionFlag<number | undefined> =>
|
||||
Flags.integer({
|
||||
description: `The number of items to fetch each query. Defaults to ${defaultTo} and is capped at ${limit}.`,
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getAnalyticsEnabledAsync, setAnalyticsEnabledAsync } from '../analytics/AnalyticsManager';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
|
||||
export default class AnalyticsView extends EasCommand {
|
||||
static override description = 'display or change analytics settings';
|
||||
@@ -10,10 +10,10 @@ export default class AnalyticsView extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
|
||||
if (status) {
|
||||
await UserSettings.setAsync('analyticsEnabled', status === 'on');
|
||||
setAnalyticsEnabledAsync(status === 'on');
|
||||
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
|
||||
} else {
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', null);
|
||||
const analyticsEnabled = await getAnalyticsEnabledAsync();
|
||||
Log.log(
|
||||
`Analytics are ${
|
||||
analyticsEnabled === false ? 'disabled' : 'enabled'
|
||||
|
||||
@@ -100,6 +100,7 @@ export default class Build extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -114,6 +115,7 @@ export default class Build extends EasCommand {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
} = await this.getContextAsync(Build, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
@@ -133,6 +135,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectDir,
|
||||
flagsWithPlatform,
|
||||
actor,
|
||||
|
||||
@@ -65,6 +65,7 @@ export default class BuildInspect extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -73,6 +74,7 @@ export default class BuildInspect extends EasCommand {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
} = await this.getContextAsync(BuildInspect, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -97,6 +99,7 @@ export default class BuildInspect extends EasCommand {
|
||||
try {
|
||||
await runBuildAndSubmitAsync(
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectDir,
|
||||
{
|
||||
nonInteractive: false,
|
||||
|
||||
@@ -9,13 +9,10 @@ import {
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import {
|
||||
AppPlatform,
|
||||
DistributionType,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildList extends EasCommand {
|
||||
@@ -82,7 +79,8 @@ export default class BuildList extends EasCommand {
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
|
||||
const graphqlBuildDistribution =
|
||||
buildDistributionTypeToGraphQLDistributionType(buildDistribution);
|
||||
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
await listAndRenderBuildsOnAppAsync(graphqlClient, {
|
||||
@@ -133,17 +131,3 @@ const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | u
|
||||
return GraphQLBuildStatus.Canceled;
|
||||
}
|
||||
};
|
||||
|
||||
const toGraphQLBuildDistribution = (
|
||||
buildDistribution?: BuildDistributionType
|
||||
): DistributionType | undefined => {
|
||||
if (buildDistribution === BuildDistributionType.STORE) {
|
||||
return DistributionType.Store;
|
||||
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
|
||||
return DistributionType.Internal;
|
||||
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,213 @@
|
||||
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;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export default class Credentials extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.OptionalProjectConfig,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -20,6 +21,7 @@ export default class Credentials extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig,
|
||||
analytics,
|
||||
} = await this.getContextAsync(Credentials, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -27,6 +29,7 @@ export default class Credentials extends EasCommand {
|
||||
await new SelectPlatform(
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectConfig ?? null,
|
||||
flags.platform
|
||||
).runAsync();
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { loadConfigAsync } from '../../metadata/config/resolve';
|
||||
import { MetadataValidationError, logMetadataValidationError } from '../../metadata/errors';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class MetadataLint extends EasCommand {
|
||||
static override description = 'validate the local store configuration';
|
||||
|
||||
static override flags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
default: false,
|
||||
}),
|
||||
profile: Flags.string({
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
// The metadata lint command is created to integrate in other dev tooling, like vscode-expo.
|
||||
// These integrations might spam this command, so we avoid communicating with our services here.
|
||||
// Note that this is an exception and you should normally use `ProjectConfig` instead.
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(MetadataLint);
|
||||
const { projectDir } = await this.getContextAsync(MetadataLint, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
} else {
|
||||
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
|
||||
}
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
if (submitProfiles.length !== 1) {
|
||||
throw new Error('Metadata only supports iOS and a single submit profile.');
|
||||
}
|
||||
|
||||
const submitProfile = submitProfiles[0].profile;
|
||||
|
||||
try {
|
||||
await loadConfigAsync({ projectDir, profile: submitProfile });
|
||||
|
||||
if (flags.json) {
|
||||
return printJsonOnlyOutput([]);
|
||||
}
|
||||
|
||||
Log.log('✅ Store configuration is valid.');
|
||||
} catch (error) {
|
||||
if (!(error instanceof MetadataValidationError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
return printJsonOnlyOutput(error.errors);
|
||||
}
|
||||
|
||||
logMetadataValidationError(error);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
@@ -6,9 +8,9 @@ import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { downloadMetadataAsync } from '../../metadata/download';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class MetadataPull extends EasCommand {
|
||||
static override description = 'generate the local store configuration from the app stores';
|
||||
@@ -24,6 +26,7 @@ export default class MetadataPull extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -33,6 +36,7 @@ export default class MetadataPull extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
analytics,
|
||||
} = await this.getContextAsync(MetadataPull, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -40,23 +44,35 @@ export default class MetadataPull extends EasCommand {
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
if (submitProfiles.length !== 1) {
|
||||
throw new Error('Metadata only supports iOS and a single submit profile.');
|
||||
}
|
||||
|
||||
const submitProfile = submitProfiles[0].profile;
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const metadataCtx = await createMetadataContextAsync({
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
exp,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const filePath = await downloadMetadataAsync(metadataCtx);
|
||||
const filePath = await downloadMetadataAsync({
|
||||
analytics,
|
||||
exp,
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
profile: submitProfile,
|
||||
});
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { uploadMetadataAsync } from '../../metadata/upload';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class MetadataPush extends EasCommand {
|
||||
static override description = 'sync the local store configuration to the app stores';
|
||||
@@ -22,6 +24,7 @@ export default class MetadataPush extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -31,6 +34,7 @@ export default class MetadataPush extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
analytics,
|
||||
} = await this.getContextAsync(MetadataPush, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -38,23 +42,36 @@ export default class MetadataPush extends EasCommand {
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
if (submitProfiles.length !== 1) {
|
||||
throw new Error('Metadata only supports iOS and a single submit profile.');
|
||||
}
|
||||
|
||||
const submitProfile = submitProfiles[0].profile;
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const metadataCtx = await createMetadataContextAsync({
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
exp,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const { appleLink } = await uploadMetadataAsync(metadataCtx);
|
||||
const { appleLink } = await uploadMetadataAsync({
|
||||
analytics,
|
||||
exp,
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
profile: submitProfile,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Store configuration is synced with the app stores.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
|
||||
import { AppJSONConfig, PackageJSONConfig, getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import { vol } from 'memfs';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
@@ -7,8 +7,11 @@ import LoggedInContextField from '../../../commandUtils/context/LoggedInContextF
|
||||
import ProjectDirContextField from '../../../commandUtils/context/ProjectDirContextField';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { saveProjectIdToAppConfigAsync } from '../../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester, jester2 } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import ProjectInit from '../init';
|
||||
@@ -18,6 +21,7 @@ jest.mock('@expo/config');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../user/actions');
|
||||
jest.mock('../../../graphql/mutations/AppMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
@@ -77,9 +81,11 @@ function mockTestProject(options: {
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest.spyOn(ProjectDirContextField.prototype, 'getValueAsync').mockResolvedValue('/test-project');
|
||||
jest
|
||||
.spyOn(LoggedInContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue({ actor: jester, graphqlClient });
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
});
|
||||
}
|
||||
|
||||
const commandOptions = { root: '/test-project' } as any;
|
||||
@@ -97,11 +103,23 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
describe('interactive', () => {
|
||||
it('is no-op if already configured for id', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to overwrite when different', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '12345');
|
||||
@@ -109,8 +127,16 @@ describe(ProjectInit.name, () => {
|
||||
});
|
||||
|
||||
it('aborts when prompt to overwrite is declined', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
await new ProjectInit(['--id', '12345'], commandOptions).run();
|
||||
await expect(
|
||||
new ProjectInit(['--id', '12345'], commandOptions).run()
|
||||
).rejects.toThrowError('Aborting');
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
});
|
||||
@@ -118,6 +144,12 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
describe('force', () => {
|
||||
it('does not prompt to overwrite when different', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
await new ProjectInit(['--id', '12345', '--force'], {
|
||||
root: '/test-project',
|
||||
} as any).run();
|
||||
@@ -140,17 +172,171 @@ describe(ProjectInit.name, () => {
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checks owner and slug consistency', () => {
|
||||
it('is a no-op if already consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to configure if not consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('overrides if force flag is present and it is not consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234', '--force'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('throws if non-interactive is present and it is not consistent', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await expect(
|
||||
new ProjectInit(['--id', '1234', '--non-interactive'], commandOptions).run()
|
||||
).rejects.toThrow(
|
||||
'Project config error: Project owner (jester2) does not match the value configured in the "owner" field (jester). Use --force flag to overwrite.'
|
||||
);
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is not yet configured', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({});
|
||||
});
|
||||
|
||||
it('configures', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '1234');
|
||||
});
|
||||
|
||||
describe('checks owner and slug consistency', () => {
|
||||
it('is a no-op if already consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts to configure if not consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('overrides if force flag is present and it is not consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit(['--id', '1234', '--force'], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
|
||||
it('throws if non-interactive is present and it is not consistent', async () => {
|
||||
mockTestProject({ configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await expect(
|
||||
new ProjectInit(['--id', '1234', '--non-interactive'], commandOptions).run()
|
||||
).rejects.toThrow(
|
||||
'Project config error: Project owner (jester2) does not match the value configured in the "owner" field (jester). Use --force flag to overwrite.'
|
||||
);
|
||||
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -9,6 +11,7 @@ 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';
|
||||
@@ -54,7 +57,99 @@ export default class ProjectInit extends EasCommand {
|
||||
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
|
||||
}
|
||||
|
||||
private static async initializeWithExplicitIDAsync(
|
||||
private static async modifyExpoConfigAsync(
|
||||
projectDir: string,
|
||||
modifications: Partial<ExpoConfig>
|
||||
): Promise<void> {
|
||||
const result = await modifyConfigAsync(projectDir, modifications);
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`Warning: Your project uses dynamic app configuration, and cannot be automatically modified.`
|
||||
);
|
||||
Log.warn(
|
||||
chalk.dim(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
|
||||
)
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`To complete the setup process, add the following in your ${chalk.bold(
|
||||
getProjectConfigDescription(projectDir)
|
||||
)}:`
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(chalk.bold(JSON.stringify(modifications, null, 2)));
|
||||
Log.warn();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
}
|
||||
|
||||
private static async ensureOwnerSlugConsistencyAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const correctOwner = appForProjectId.ownerAccount.name;
|
||||
const correctSlug = appForProjectId.slug;
|
||||
|
||||
if (exp.owner && exp.owner !== correctOwner) {
|
||||
if (force) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
|
||||
} else {
|
||||
const message = `Project owner (${correctOwner}) does not match the value configured in the "owner" field (${exp.owner}).`;
|
||||
if (nonInteractive) {
|
||||
throw new Error(`Project config error: ${message} Use --force flag to overwrite.`);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${message}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting');
|
||||
}
|
||||
|
||||
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
|
||||
}
|
||||
} else if (!exp.owner) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
|
||||
}
|
||||
|
||||
if (exp.slug && exp.slug !== correctSlug) {
|
||||
if (force) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
|
||||
} else {
|
||||
const message = `Project slug (${correctSlug}) does not match the value configured in the "slug" field (${exp.slug}).`;
|
||||
if (nonInteractive) {
|
||||
throw new Error(`Project config error: ${message} Use --force flag to overwrite.`);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${message}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting');
|
||||
}
|
||||
|
||||
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
|
||||
}
|
||||
} else if (!exp.slug) {
|
||||
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
|
||||
}
|
||||
}
|
||||
|
||||
private static async setExplicitIDAsync(
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
@@ -92,14 +187,29 @@ export default class ProjectInit extends EasCommand {
|
||||
)}. Do you wish to overwrite it?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
Log.log('Aborting');
|
||||
return;
|
||||
throw new Error('Aborting');
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async initializeWithExplicitIDAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
): Promise<void> {
|
||||
await this.setExplicitIDAsync(projectId, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
await this.ensureOwnerSlugConsistencyAsync(graphqlClient, projectId, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
private static async initializeWithInteractiveSelectionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
@@ -227,7 +337,10 @@ export default class ProjectInit extends EasCommand {
|
||||
} = await this.getContextAsync(ProjectInit, { nonInteractive });
|
||||
|
||||
if (id) {
|
||||
await ProjectInit.initializeWithExplicitIDAsync(id, projectDir, { force, nonInteractive });
|
||||
await ProjectInit.initializeWithExplicitIDAsync(graphqlClient, id, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
} else {
|
||||
await ProjectInit.initializeWithInteractiveSelectionAsync(graphqlClient, actor, projectDir);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async secretValue => {
|
||||
if (!secretValue) {
|
||||
return 'Secret value may not be empty.';
|
||||
return validationMessage;
|
||||
}
|
||||
if (secretType === SecretType.FILE) {
|
||||
const secretFilePath = path.resolve(secretValue);
|
||||
@@ -208,13 +208,13 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
|
||||
if (secretType === SecretType.STRING) {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
secretValue
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
secretFilePath
|
||||
)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
@@ -252,13 +252,13 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
|
||||
if (secretType === SecretType.STRING) {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} with value ${chalk.bold(
|
||||
secretValue
|
||||
)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
`Created a new secret ${chalk.bold(name)} from file ${chalk.bold(
|
||||
secretFilePath
|
||||
)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
|
||||
import {
|
||||
EnvironmentSecretWithScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
export default class EnvironmentSecretList extends EasCommand {
|
||||
static override description = 'list environment secrets available for your current app';
|
||||
@@ -25,23 +27,18 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(graphqlClient, projectId);
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Type', 'Scope', 'ID', 'Updated at'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const secret of secrets) {
|
||||
const { name, createdAt: updatedAt, scope, id, type } = secret;
|
||||
table.push([
|
||||
name,
|
||||
EnvironmentSecretTypeToSecretType[type],
|
||||
scope,
|
||||
id,
|
||||
dateFormat(updatedAt, 'mmm dd HH:MM:ss'),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Secrets for this account and project:}`);
|
||||
Log.log(table.toString());
|
||||
Log.log(secrets.map(secret => formatSecret(secret)).join(`\n\n${chalk.dim('———')}\n\n`));
|
||||
}
|
||||
}
|
||||
|
||||
function formatSecret(secret: EnvironmentSecretWithScope): string {
|
||||
return formatFields([
|
||||
{ label: 'ID', value: secret.id },
|
||||
{ label: 'Name', value: secret.name },
|
||||
{ label: 'Scope', value: secret.scope },
|
||||
{ label: 'Type', value: secret.type },
|
||||
// TODO: Figure out why do we name it updated, while it's created at?
|
||||
{ label: 'Updated at', value: dateFormat(secret.createdAt, 'mmm dd HH:MM:ss') },
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentSecretFragment, EnvironmentSecretType } from '../../graphql/generated';
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import {
|
||||
EnvironmentSecretScope,
|
||||
EnvironmentSecretsQuery,
|
||||
} from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import {
|
||||
getDisplayNameForProjectIdAsync,
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import intersection from '../../utils/expodash/intersection';
|
||||
|
||||
export default class EnvironmentSecretPush extends EasCommand {
|
||||
static override description = 'read environment secrets from env file and store on the server';
|
||||
|
||||
static override flags = {
|
||||
scope: Flags.enum({
|
||||
description: 'Scope for the secrets',
|
||||
options: [EnvironmentSecretScope.ACCOUNT, EnvironmentSecretScope.PROJECT],
|
||||
default: EnvironmentSecretScope.PROJECT,
|
||||
}),
|
||||
'env-file': Flags.string({
|
||||
description: 'Env file with secrets',
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Delete and recreate existing secrets',
|
||||
default: false,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { scope, force, 'env-file': maybeEnvFilePath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentSecretPush);
|
||||
const {
|
||||
projectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentSecretPush, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
const ownerAccount = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
const envFilePath = await resolveEnvFilePathAsync(maybeEnvFilePath, nonInteractive);
|
||||
if (!(await fs.pathExists(envFilePath))) {
|
||||
throw new Error(`File "${envFilePath}" does not exist`);
|
||||
}
|
||||
|
||||
const newSecrets: Record<string, string> = dotenv.parse(await fs.readFile(envFilePath));
|
||||
|
||||
await ensureSecretsDontExistOnServerAsync(
|
||||
graphqlClient,
|
||||
{ projectId, scope, force },
|
||||
newSecrets
|
||||
);
|
||||
|
||||
await createSecretsAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
scope,
|
||||
accountId: ownerAccount.id,
|
||||
accountName: ownerAccount.name,
|
||||
projectId,
|
||||
projectDisplayName,
|
||||
},
|
||||
newSecrets
|
||||
);
|
||||
|
||||
Log.withTick(
|
||||
`Created the following secrets on ${scope} ${chalk.bold(
|
||||
scope === EnvironmentSecretScope.ACCOUNT ? ownerAccount.name : projectDisplayName
|
||||
)}:`
|
||||
);
|
||||
for (const secretName of Object.keys(newSecrets)) {
|
||||
Log.log(`- ${secretName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveEnvFilePathAsync(
|
||||
maybeEnvFilePath: string | undefined,
|
||||
nonInteractive: boolean
|
||||
): Promise<string> {
|
||||
if (maybeEnvFilePath) {
|
||||
return maybeEnvFilePath;
|
||||
}
|
||||
|
||||
const validationMessage = 'Env file must be passed.';
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const { envFilePath } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'envFilePath',
|
||||
message: 'Path to the env file with secrets:',
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
validate: async secretValueRaw => {
|
||||
if (!secretValueRaw) {
|
||||
return validationMessage;
|
||||
}
|
||||
const envFilePath = path.resolve(secretValueRaw);
|
||||
if (!(await fs.pathExists(envFilePath))) {
|
||||
return `File "${envFilePath}" does not exist.`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return envFilePath;
|
||||
}
|
||||
|
||||
async function ensureSecretsDontExistOnServerAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ projectId, scope, force }: { projectId: string; scope: EnvironmentSecretScope; force: boolean },
|
||||
secrets: Record<string, string>
|
||||
): Promise<void> {
|
||||
const serverSecrets = await readAllSecretsFromServerAsync(graphqlClient, projectId, scope);
|
||||
const commonSecretNames = findCommonSecretNames(serverSecrets, secrets);
|
||||
|
||||
if (commonSecretNames.length > 0) {
|
||||
if (!force) {
|
||||
Log.log(`This ${scope} already has environment secrets with the following names:`);
|
||||
for (const name of commonSecretNames) {
|
||||
Log.log(`- ${name}`);
|
||||
}
|
||||
Log.error('Run with --force flag to proceed');
|
||||
Errors.exit(1);
|
||||
} else {
|
||||
const spinner = ora('Deleting secrets already present on server...').start();
|
||||
const commonSecretNameSet = new Set(commonSecretNames);
|
||||
const commonServerSecrets = serverSecrets.filter(({ name }) => commonSecretNameSet.has(name));
|
||||
await deleteSecretsAsync(graphqlClient, commonServerSecrets);
|
||||
spinner.succeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readAllSecretsFromServerAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
scope: EnvironmentSecretScope
|
||||
): Promise<EnvironmentSecretFragment[]> {
|
||||
const { appSecrets, accountSecrets } = await EnvironmentSecretsQuery.byAppIdAsync(
|
||||
graphqlClient,
|
||||
projectId
|
||||
);
|
||||
|
||||
return scope === EnvironmentSecretScope.ACCOUNT ? accountSecrets : appSecrets;
|
||||
}
|
||||
|
||||
function findCommonSecretNames(
|
||||
serverSecrets: EnvironmentSecretFragment[],
|
||||
newSecrets: Record<string, string>
|
||||
): string[] {
|
||||
const serverSecretKeys = serverSecrets.map(({ name }) => name);
|
||||
const newSecretKeys = Object.keys(newSecrets);
|
||||
const commonKeys = intersection(serverSecretKeys, newSecretKeys);
|
||||
return commonKeys;
|
||||
}
|
||||
|
||||
async function deleteSecretsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
secrets: EnvironmentSecretFragment[]
|
||||
): Promise<void> {
|
||||
const promises = secrets.map(secret =>
|
||||
EnvironmentSecretMutation.deleteAsync(graphqlClient, secret.id)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async function createSecretsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
scope,
|
||||
accountId,
|
||||
accountName,
|
||||
projectId,
|
||||
projectDisplayName,
|
||||
}: {
|
||||
scope: EnvironmentSecretScope;
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
},
|
||||
secrets: Record<string, string>
|
||||
): Promise<void> {
|
||||
const promises = [];
|
||||
|
||||
const spinner = ora(
|
||||
`Creating secrets on ${scope} ${chalk.bold(
|
||||
scope === EnvironmentSecretScope.ACCOUNT ? accountName : projectDisplayName
|
||||
)}...`
|
||||
).start();
|
||||
for (const [secretName, secretValue] of Object.entries(secrets)) {
|
||||
if (scope === EnvironmentSecretScope.ACCOUNT) {
|
||||
promises.push(
|
||||
EnvironmentSecretMutation.createForAccountAsync(
|
||||
graphqlClient,
|
||||
{ name: secretName, value: secretValue, type: EnvironmentSecretType.String },
|
||||
accountId
|
||||
)
|
||||
);
|
||||
} else {
|
||||
promises.push(
|
||||
EnvironmentSecretMutation.createForAppAsync(
|
||||
graphqlClient,
|
||||
{ name: secretName, value: secretValue, type: EnvironmentSecretType.String },
|
||||
projectId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
spinner.succeed();
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ export default class Submit extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -100,6 +101,7 @@ export default class Submit extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig: { exp, projectId, projectDir },
|
||||
analytics,
|
||||
} = await this.getContextAsync(Submit, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
@@ -129,6 +131,7 @@ export default class Submit extends EasCommand {
|
||||
nonInteractive: flagsWithPlatform.nonInteractive,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
exp,
|
||||
projectId,
|
||||
});
|
||||
|
||||
@@ -87,6 +87,11 @@ export default class UpdateConfigure extends EasCommand {
|
||||
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`All builds of your app going forward will be eligible to receive updates published with EAS Update.`
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Your app is configured to run EAS Update!`);
|
||||
}
|
||||
|
||||
@@ -233,15 +233,13 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
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?`,
|
||||
message: chalk`The module {cyan expo-updates} must be installed to load EAS updates in-app. Install?`,
|
||||
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.`, {
|
||||
Errors.error(`Install ${chalk.bold('expo-updates')} and try again.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, getUpdateGroupDescriptions } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { formatUpdateGroup, getUpdateGroupDescriptions } from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class UpdateView extends EasCommand {
|
||||
@@ -48,20 +46,9 @@ export default class UpdateView extends EasCommand {
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(updateGroupDescription);
|
||||
} else {
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
Log.log(chalk.bold('Update group:'));
|
||||
Log.log(formatFields([{ label: 'ID', value: updateGroupDescription.group }]));
|
||||
groupTable.push([
|
||||
updateGroupDescription.message,
|
||||
updateGroupDescription.runtimeVersion,
|
||||
updateGroupDescription.group,
|
||||
updateGroupDescription.platforms,
|
||||
]);
|
||||
Log.log(groupTable.toString());
|
||||
|
||||
Log.log(formatUpdateGroup(updateGroupDescription));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppFragment, Role } from '../../graphql/generated';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
export const jester = {
|
||||
export const jester: Actor = {
|
||||
__typename: 'User' as const,
|
||||
id: 'jester-id',
|
||||
username: 'jester',
|
||||
@@ -22,6 +23,7 @@ export const jester = {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
export const jester2 = {
|
||||
@@ -36,6 +38,7 @@ export const jester2 = {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
export const testUsername = jester.username;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Analytics } from '../../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AccountFragment, AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
@@ -16,7 +17,7 @@ export class CreateKeystore {
|
||||
}
|
||||
|
||||
const projectId = ctx.projectId;
|
||||
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, projectId);
|
||||
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, ctx.analytics, projectId);
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(
|
||||
ctx.graphqlClient,
|
||||
this.account,
|
||||
@@ -28,6 +29,7 @@ export class CreateKeystore {
|
||||
|
||||
private async provideOrGenerateAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
|
||||
@@ -36,6 +38,6 @@ export class CreateKeystore {
|
||||
validateKeystore(providedKeystoreWithType);
|
||||
return providedKeystoreWithType;
|
||||
}
|
||||
return await generateRandomKeystoreAsync(graphqlClient, projectId);
|
||||
return await generateRandomKeystoreAsync(graphqlClient, analytics, projectId);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Analytics, BuildEvent } from '../../../analytics/events';
|
||||
import { Analytics, BuildEvent } from '../../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import fetch from '../../../fetch';
|
||||
import { AndroidKeystoreType } from '../../../graphql/generated';
|
||||
@@ -38,6 +38,7 @@ enum KeystoreCreateStep {
|
||||
|
||||
export async function generateRandomKeystoreAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
const keystoreData: KeystoreParams = {
|
||||
@@ -45,15 +46,16 @@ export async function generateRandomKeystoreAsync(
|
||||
keyPassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyAlias: crypto.randomBytes(16).toString('hex'),
|
||||
};
|
||||
return await createKeystoreAsync(graphqlClient, keystoreData, projectId);
|
||||
return await createKeystoreAsync(graphqlClient, analytics, keystoreData, projectId);
|
||||
}
|
||||
|
||||
async function createKeystoreAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
keystoreParams: KeystoreParams,
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Attempt,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
@@ -74,14 +76,14 @@ async function createKeystoreAsync(
|
||||
showKeytoolDetectionMsg: !localAttempt,
|
||||
});
|
||||
}
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Success,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
return keystore;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
analytics.logEvent(BuildEvent.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Fail,
|
||||
reason: error.message,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Env } from '@expo/eas-build-job';
|
||||
import { EasJson } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { Analytics } from '../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../log';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
@@ -26,6 +27,7 @@ export class CredentialsContext {
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
public readonly graphqlClient: ExpoGraphqlClient;
|
||||
public readonly analytics: Analytics;
|
||||
public readonly easJsonCliConfig?: EasJson['cli'];
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
@@ -41,6 +43,7 @@ export class CredentialsContext {
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
@@ -48,6 +51,7 @@ export class CredentialsContext {
|
||||
this.projectDir = options.projectDir;
|
||||
this.user = options.user;
|
||||
this.graphqlClient = options.graphqlClient;
|
||||
this.analytics = options.analytics;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
this.projectInfo = options.projectInfo;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ async function generateAscApiKeyAsync(
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, {
|
||||
nickname: getAscApiKeyName(purpose),
|
||||
});
|
||||
return await getMinimalAscApiKeyAsync(ascApiKey);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ProfileType } from '@expo/app-store';
|
||||
|
||||
import { Analytics } from '../../../analytics/AnalyticsManager';
|
||||
import Log from '../../../log';
|
||||
import {
|
||||
AscApiKey,
|
||||
@@ -192,9 +193,12 @@ export default class AppStoreApi {
|
||||
return await getAscApiKeyAsync(userCtx, keyId);
|
||||
}
|
||||
|
||||
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
|
||||
public async createAscApiKeyAsync(
|
||||
analytics: Analytics,
|
||||
{ nickname }: { nickname: string }
|
||||
): Promise<AscApiKey> {
|
||||
const userCtx = await this.ensureUserAuthenticatedAsync();
|
||||
return await createAscApiKeyAsync(userCtx, { nickname });
|
||||
return await createAscApiKeyAsync(analytics, userCtx, { nickname });
|
||||
}
|
||||
|
||||
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApiKey, ApiKeyType, UnexpectedResponse, UserRole } from '@expo/apple-utils';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { Analytics } from '../../../../analytics/AnalyticsManager';
|
||||
import {
|
||||
createAscApiKeyAsync,
|
||||
downloadWithRetryAsync,
|
||||
@@ -72,10 +74,13 @@ test(`getAscApiKeyAsync`, async () => {
|
||||
});
|
||||
|
||||
test(`createAscApiKeyAsync`, async () => {
|
||||
const analytics = instance(mock<Analytics>());
|
||||
const mockRequestContext = {};
|
||||
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
|
||||
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
|
||||
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
|
||||
const result = await createAscApiKeyAsync(analytics, mockAuthCtx, {
|
||||
nickname: 'test-name',
|
||||
});
|
||||
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
|
||||
nickname: 'test-name',
|
||||
allAppsVisible: true,
|
||||
@@ -99,7 +104,7 @@ test(`revokeAscApiKeyAsync`, async () => {
|
||||
|
||||
test(`downloadWithRetryAsync`, async () => {
|
||||
const cacheFailureMessage = `The specified resource does not exist - There is no resource of type 'apiKeys' with id 'TEST-ID'`;
|
||||
|
||||
const analytics = instance(mock<Analytics>());
|
||||
// complete failure
|
||||
const mockApiKeyWithDownloadError = {
|
||||
...mockApiKey,
|
||||
@@ -108,7 +113,7 @@ test(`downloadWithRetryAsync`, async () => {
|
||||
}),
|
||||
} as unknown as ApiKey;
|
||||
await expect(
|
||||
downloadWithRetryAsync(mockApiKeyWithDownloadError, { minTimeout: 1 }) // stay within jest timeout window
|
||||
downloadWithRetryAsync(analytics, mockApiKeyWithDownloadError, { minTimeout: 1 }) // stay within jest timeout window
|
||||
).rejects.toThrowError(cacheFailureMessage);
|
||||
// expect to try once and retry 3 times = 4 total
|
||||
expect(mockApiKeyWithDownloadError.downloadAsync as jest.Mock).toBeCalledTimes(7);
|
||||
@@ -119,15 +124,23 @@ test(`downloadWithRetryAsync`, async () => {
|
||||
(mockApiKeyWithOneTimeDownloadError.downloadAsync as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new UnexpectedResponse(cacheFailureMessage);
|
||||
});
|
||||
const keyP8AfterOneFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
|
||||
minTimeout: 1,
|
||||
});
|
||||
const keyP8AfterOneFailure = await downloadWithRetryAsync(
|
||||
analytics,
|
||||
mockApiKeyWithOneTimeDownloadError,
|
||||
{
|
||||
minTimeout: 1,
|
||||
}
|
||||
);
|
||||
expect(keyP8AfterOneFailure).toBe('super secret');
|
||||
|
||||
// successful case
|
||||
(mockApiKey.downloadAsync as jest.Mock).mockClear();
|
||||
const keyP8NoFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
|
||||
minTimeout: 1,
|
||||
});
|
||||
const keyP8NoFailure = await downloadWithRetryAsync(
|
||||
analytics,
|
||||
mockApiKeyWithOneTimeDownloadError,
|
||||
{
|
||||
minTimeout: 1,
|
||||
}
|
||||
);
|
||||
expect(keyP8NoFailure).toBe('super secret');
|
||||
});
|
||||
+1
-1
@@ -254,7 +254,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
]);
|
||||
|
||||
expect(results.enabled).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
"HealthKit",
|
||||
"Apple Pay Payment Processing",
|
||||
"iCloud",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
|
||||
import promiseRetry from 'promise-retry';
|
||||
|
||||
import { Analytics, SubmissionEvent } from '../../../analytics/events';
|
||||
import { Analytics, SubmissionEvent } from '../../../analytics/AnalyticsManager';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
|
||||
@@ -58,6 +58,7 @@ export async function getAscApiKeyAsync(
|
||||
* is available for download.
|
||||
* */
|
||||
export async function downloadWithRetryAsync(
|
||||
analytics: Analytics,
|
||||
key: ApiKey,
|
||||
{
|
||||
minTimeout = 1000,
|
||||
@@ -81,7 +82,7 @@ export async function downloadWithRetryAsync(
|
||||
Log.log(
|
||||
`Received an unexpected response from Apple, retrying in ${secondsToRetry} seconds...`
|
||||
);
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_RETRY, {
|
||||
analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_RETRY, {
|
||||
errorName: e.name,
|
||||
reason: e.message,
|
||||
retry: number,
|
||||
@@ -97,7 +98,7 @@ export async function downloadWithRetryAsync(
|
||||
minTimeout,
|
||||
}
|
||||
);
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_SUCCESS, {});
|
||||
analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_SUCCESS, {});
|
||||
return keyP8;
|
||||
} catch (e: any) {
|
||||
if (
|
||||
@@ -110,7 +111,7 @@ export async function downloadWithRetryAsync(
|
||||
)}`
|
||||
);
|
||||
}
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_FAIL, {
|
||||
analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_FAIL, {
|
||||
errorName: e.name,
|
||||
reason: e.message,
|
||||
});
|
||||
@@ -123,6 +124,7 @@ export async function downloadWithRetryAsync(
|
||||
* **Does not support App Store Connect API (CI).**
|
||||
*/
|
||||
export async function createAscApiKeyAsync(
|
||||
analytics: Analytics,
|
||||
userAuthCtx: UserAuthCtx,
|
||||
{
|
||||
nickname,
|
||||
@@ -140,7 +142,7 @@ export async function createAscApiKeyAsync(
|
||||
roles: roles ?? [UserRole.ADMIN],
|
||||
keyType: keyType ?? ApiKeyType.PUBLIC_API,
|
||||
});
|
||||
const keyP8 = await downloadWithRetryAsync(key);
|
||||
const keyP8 = await downloadWithRetryAsync(analytics, key);
|
||||
if (!keyP8) {
|
||||
const { nickname, roles } = key.attributes;
|
||||
const humanReadableKey = `App Store Connect Key '${nickname}' (${
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../../log';
|
||||
import { pressAnyKeyToContinueAsync } from '../../prompts';
|
||||
@@ -7,6 +8,7 @@ import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
export interface Action<T = void> {
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
projectInfo: CredentialsContextProjectInfo | null;
|
||||
runAsync(ctx: CredentialsContext): Promise<T>;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export class ManageAndroid {
|
||||
projectInfo: this.callingAction.projectInfo,
|
||||
user: this.callingAction.actor,
|
||||
graphqlClient: this.callingAction.graphqlClient,
|
||||
analytics: this.callingAction.analytics,
|
||||
env: buildProfile?.env,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -66,6 +66,7 @@ export class ManageIos {
|
||||
projectInfo: this.callingAction.projectInfo,
|
||||
user: this.callingAction.actor,
|
||||
graphqlClient: this.callingAction.graphqlClient,
|
||||
analytics: this.callingAction.analytics,
|
||||
env: buildProfile?.env,
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { selectPlatformAsync } from '../../platform';
|
||||
import { Actor } from '../../user/User';
|
||||
@@ -9,6 +10,7 @@ export class SelectPlatform {
|
||||
constructor(
|
||||
public readonly actor: Actor,
|
||||
public readonly graphqlClient: ExpoGraphqlClient,
|
||||
public readonly analytics: Analytics,
|
||||
public readonly projectInfo: CredentialsContextProjectInfo | null,
|
||||
private readonly flagPlatform?: string
|
||||
) {}
|
||||
|
||||
@@ -43,6 +43,7 @@ describe(AccountResolver, () => {
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
describe('when inside project dir', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -8,6 +7,7 @@ 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,11 +15,6 @@ 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,
|
||||
@@ -89,7 +84,7 @@ async function collectDeviceDataAsync(
|
||||
This will ${chalk.bold('not')} register the device on the Apple Developer Portal yet.`
|
||||
);
|
||||
Log.newLine();
|
||||
printDeviceDataSummary(deviceData, appleTeam);
|
||||
Log.log(formatNewDevice({ ...deviceData, identifier: deviceData.udid }, appleTeam));
|
||||
Log.newLine();
|
||||
|
||||
const registrationConfirmed = await confirmAsync({
|
||||
@@ -156,21 +151,3 @@ 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,29 +1,61 @@
|
||||
import { AppleDevice, AppleTeam } from '../../graphql/generated';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { AppleDevice, AppleDeviceClass, AppleTeam } from '../../graphql/generated';
|
||||
import formatFields, { FormatFieldsItem } 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 = [
|
||||
const fields: FormatFieldsItem[] = [
|
||||
{ label: 'ID', value: device.id },
|
||||
{ label: 'Name', value: device.name ?? 'Unknown' },
|
||||
{
|
||||
label: 'Class',
|
||||
value: device.deviceClass
|
||||
? `${device.deviceClass}${device.model ? ` ${device.model}` : ''}`
|
||||
: 'Unknown',
|
||||
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' }
|
||||
);
|
||||
}
|
||||
|
||||
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' }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,4 +8,12 @@ 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,
|
||||
};
|
||||
+87
-14
@@ -401,6 +401,7 @@ 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']>;
|
||||
@@ -415,6 +416,7 @@ 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']>;
|
||||
};
|
||||
@@ -457,6 +459,7 @@ 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']>;
|
||||
};
|
||||
@@ -1744,6 +1747,8 @@ 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>;
|
||||
@@ -1784,7 +1789,8 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
queuePosition?: Maybe<Scalars['Int']>;
|
||||
reactNativeVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
resourceClass?: Maybe<BuildResourceClass>;
|
||||
/** The builder resource class requested by the developer */
|
||||
resourceClass: BuildResourceClass;
|
||||
runFromCI?: Maybe<Scalars['Boolean']>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
@@ -2219,6 +2225,7 @@ export type CreateAccessTokenResponse = {
|
||||
|
||||
export type CreateAndroidSubmissionInput = {
|
||||
appId: Scalars['ID'];
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
archiveUrl?: InputMaybe<Scalars['String']>;
|
||||
config: AndroidSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']>;
|
||||
@@ -2245,6 +2252,7 @@ export type CreateGitHubRepositoryInput = {
|
||||
appId: Scalars['ID'];
|
||||
githubAppInstallationId: Scalars['ID'];
|
||||
githubRepositoryIdentifier: Scalars['Int'];
|
||||
nodeIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateGitHubRepositorySettingsInput = {
|
||||
@@ -2255,6 +2263,7 @@ export type CreateGitHubRepositorySettingsInput = {
|
||||
|
||||
export type CreateIosSubmissionInput = {
|
||||
appId: Scalars['ID'];
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
archiveUrl?: InputMaybe<Scalars['String']>;
|
||||
config: IosSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']>;
|
||||
@@ -2317,6 +2326,16 @@ 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'];
|
||||
@@ -2506,7 +2525,7 @@ export type GetSignedAssetUploadSpecificationsResult = {
|
||||
|
||||
export type GitHubAppInstallation = {
|
||||
__typename?: 'GitHubAppInstallation';
|
||||
accessibleRepositories: Array<GitHubAppInstallationAccessibleRepository>;
|
||||
accessibleRepositories: GitHubRepositoryPaginationResult;
|
||||
account: Account;
|
||||
actor?: Maybe<Actor>;
|
||||
id: Scalars['ID'];
|
||||
@@ -2526,6 +2545,7 @@ export type GitHubAppInstallationAccessibleRepository = {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['Int'];
|
||||
name: Scalars['String'];
|
||||
nodeId: Scalars['String'];
|
||||
owner: GitHubRepositoryOwner;
|
||||
private: Scalars['Boolean'];
|
||||
url: Scalars['String'];
|
||||
@@ -2566,14 +2586,18 @@ export type GitHubAppQuery = {
|
||||
__typename?: 'GitHubAppQuery';
|
||||
appIdentifier: Scalars['String'];
|
||||
clientIdentifier: Scalars['String'];
|
||||
searchRepositories: Array<GitHubAppInstallationAccessibleRepository>;
|
||||
installation: GitHubAppInstallation;
|
||||
searchRepositories: GitHubRepositoryPaginationResult;
|
||||
};
|
||||
|
||||
|
||||
export type GitHubAppQueryInstallationArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type GitHubAppQuerySearchRepositoriesArgs = {
|
||||
githubAppInstallationId: Scalars['ID'];
|
||||
page?: InputMaybe<Scalars['Int']>;
|
||||
perPage?: InputMaybe<Scalars['Int']>;
|
||||
query: Scalars['String'];
|
||||
};
|
||||
|
||||
@@ -2584,6 +2608,7 @@ export type GitHubRepository = {
|
||||
githubRepositoryIdentifier: Scalars['Int'];
|
||||
id: Scalars['ID'];
|
||||
metadata?: Maybe<GitHubRepositoryMetadata>;
|
||||
nodeIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GitHubRepositoryMetadata = {
|
||||
@@ -2623,6 +2648,12 @@ export type GitHubRepositoryOwner = {
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GitHubRepositoryPaginationResult = {
|
||||
__typename?: 'GitHubRepositoryPaginationResult';
|
||||
repositories: Array<GitHubAppInstallationAccessibleRepository>;
|
||||
totalCount: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type GitHubRepositorySettings = {
|
||||
__typename?: 'GitHubRepositorySettings';
|
||||
app: App;
|
||||
@@ -2788,6 +2819,8 @@ 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 */
|
||||
@@ -2801,6 +2834,11 @@ export type IosAppBuildCredentialsMutationCreateIosAppBuildCredentialsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type IosAppBuildCredentialsMutationDeleteIosAppBuildCredentialsArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type IosAppBuildCredentialsMutationSetDistributionCertificateArgs = {
|
||||
distributionCertificateId: Scalars['ID'];
|
||||
id: Scalars['ID'];
|
||||
@@ -2849,6 +2887,8 @@ 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 */
|
||||
@@ -2863,6 +2903,11 @@ export type IosAppCredentialsMutationCreateIosAppCredentialsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type IosAppCredentialsMutationDeleteIosAppCredentialsArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type IosAppCredentialsMutationSetAppStoreConnectApiKeyForSubmissionsArgs = {
|
||||
ascApiKeyId: Scalars['ID'];
|
||||
id: Scalars['ID'];
|
||||
@@ -2874,6 +2919,16 @@ 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',
|
||||
@@ -3499,6 +3554,8 @@ 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
|
||||
@@ -3829,6 +3886,20 @@ 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']>;
|
||||
@@ -4093,8 +4164,10 @@ export type UploadSessionCreateUploadSessionArgs = {
|
||||
};
|
||||
|
||||
export enum UploadSessionType {
|
||||
EasBuildGcsProjectSources = 'EAS_BUILD_GCS_PROJECT_SOURCES',
|
||||
EasBuildProjectSources = 'EAS_BUILD_PROJECT_SOURCES',
|
||||
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
|
||||
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE',
|
||||
EasSubmitGcsAppArchive = 'EAS_SUBMIT_GCS_APP_ARCHIVE'
|
||||
}
|
||||
|
||||
export type UsageMetricTotal = {
|
||||
@@ -4931,7 +5004,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, 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 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, 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 CreateIosBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
@@ -4941,7 +5014,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, 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 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, 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 CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
@@ -5096,14 +5169,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, 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 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, 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 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, 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 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, 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 ViewBuildsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5113,7 +5186,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, 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 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, 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 ViewUpdateChannelOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5209,7 +5282,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, 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 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 WebhooksByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5230,9 +5303,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, 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 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, 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 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, 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 EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ export const KeystoreGenerationUrlMutation = {
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`,
|
||||
{}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
@@ -30,12 +30,13 @@ export const UserQuery = {
|
||||
id
|
||||
...AccountFragment
|
||||
}
|
||||
featureGates
|
||||
isExpoAdmin
|
||||
}
|
||||
}
|
||||
${print(AccountFragmentNode)}
|
||||
`,
|
||||
/* variables */ undefined,
|
||||
{},
|
||||
{
|
||||
additionalTypenames: ['User'],
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const BuildFragmentNode = gql`
|
||||
artifacts {
|
||||
buildUrl
|
||||
xcodeBuildLogsUrl
|
||||
applicationArchiveUrl
|
||||
}
|
||||
initiatingActor {
|
||||
__typename
|
||||
|
||||
@@ -1,128 +1,149 @@
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
import { getStaticConfigFilePath, loadConfigAsync } from '../config';
|
||||
import { getStaticConfigFilePath, loadConfigAsync } from '../config/resolve';
|
||||
import { MetadataValidationError } from '../errors';
|
||||
|
||||
describe(getStaticConfigFilePath, () => {
|
||||
const projectDir = '/app';
|
||||
|
||||
it(`returns same file for store.config.json`, () => {
|
||||
const metadataPath = 'store.config.json';
|
||||
const expectedFile = path.join(projectDir, metadataPath);
|
||||
function mockProfile(metadataPath: string): SubmitProfile {
|
||||
return { metadataPath } as any;
|
||||
}
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
it(`returns same file for store.config.json`, () => {
|
||||
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.config.json') })).toBe(
|
||||
path.join(projectDir, 'store.config.json')
|
||||
);
|
||||
});
|
||||
|
||||
it(`returns store.config.json for store.config.js`, () => {
|
||||
const metadataPath = 'store.config.js';
|
||||
const expectedFile = path.join(projectDir, 'store.config.json');
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.config.js') })).toBe(
|
||||
path.join(projectDir, 'store.config.json')
|
||||
);
|
||||
});
|
||||
|
||||
it(`returns store.staging.json file for store.staging.js`, () => {
|
||||
const metadataPath = 'store.staging.js';
|
||||
const expectedFile = path.join(projectDir, 'store.staging.json');
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('store.staging.js') })).toBe(
|
||||
path.join(projectDir, 'store.staging.json')
|
||||
);
|
||||
});
|
||||
|
||||
// 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`, () => {
|
||||
const metadataPath = 'custom-name.js';
|
||||
const expectedFile = path.join(projectDir, 'custom-name.json');
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
expect(getStaticConfigFilePath({ projectDir, profile: mockProfile('custom-name.js') })).toBe(
|
||||
path.join(projectDir, 'custom-name.json')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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, metadataPath: 'doesnt-exist.json' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('doesnt-exist.json') })
|
||||
).rejects.toThrow('file not found');
|
||||
});
|
||||
|
||||
it(`throws when validation errors are found without skipping validation`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('invalid.config.json') })
|
||||
).rejects.toThrow('errors found');
|
||||
});
|
||||
|
||||
it(`returns config from "store.config.json"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'store.config.json' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('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, metadataPath: 'store.config.js' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('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, metadataPath: 'store.function.js' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('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, metadataPath: 'store.async.js' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('store.async.js') })
|
||||
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.config.json`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json', skipValidation: true })
|
||||
loadConfigAsync({
|
||||
projectDir,
|
||||
profile: mockProfile('invalid.config.json'),
|
||||
skipValidation: true,
|
||||
})
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.config.js`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.js', skipValidation: true })
|
||||
loadConfigAsync({
|
||||
projectDir,
|
||||
profile: mockProfile('invalid.config.js'),
|
||||
skipValidation: true,
|
||||
})
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.function.js`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.function.js', skipValidation: true })
|
||||
loadConfigAsync({
|
||||
projectDir,
|
||||
profile: mockProfile('invalid.function.js'),
|
||||
skipValidation: true,
|
||||
})
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.async.js`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.async.js', skipValidation: true })
|
||||
loadConfigAsync({
|
||||
projectDir,
|
||||
profile: mockProfile('invalid.async.js'),
|
||||
skipValidation: true,
|
||||
})
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.config.json"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.json' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.config.json') })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.config.js"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.js' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.config.js') })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.function.js"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.function.js' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.function.js') })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.async.js"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.async.js' })
|
||||
loadConfigAsync({ projectDir, profile: mockProfile('invalid-type.async.js') })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { infoKeywordLength } from './infoKeywordLength';
|
||||
import { infoRestrictedWords } from './infoRestrictedWords';
|
||||
|
||||
export const appleRules = [infoKeywordLength, infoRestrictedWords];
|
||||
@@ -0,0 +1,36 @@
|
||||
import { truthy } from '../../../utils/expodash/filter';
|
||||
import { IssueRule } from '../../config/issue';
|
||||
|
||||
const KEYWORD_CHARACTER_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Keywords are limited to 100 characters when converted to a comma separated string.
|
||||
* @see https://developer.apple.com/app-store/search/
|
||||
*/
|
||||
export const infoKeywordLength: IssueRule = {
|
||||
id: 'apple.info.keyword.length',
|
||||
severity: 2,
|
||||
validate(config) {
|
||||
if (!config.apple?.info || Object.keys(config.apple.info).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.keys(config.apple.info)
|
||||
.map(locale => {
|
||||
const keywords = config.apple?.info?.[locale].keywords ?? [];
|
||||
const length = keywords.join(',').length;
|
||||
|
||||
if (length > KEYWORD_CHARACTER_LIMIT) {
|
||||
return {
|
||||
id: this.id,
|
||||
severity: this.severity,
|
||||
path: ['apple', 'info', locale, 'keywords'],
|
||||
message: `Keywords are limited to ${KEYWORD_CHARACTER_LIMIT} characters, but found ${length}.`,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(truthy);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { truthy } from '../../../utils/expodash/filter';
|
||||
import { IssueRule } from '../../config/issue';
|
||||
import { AppleInfo } from '../types';
|
||||
|
||||
const RESTRICTED_PROPERTIES: (keyof AppleInfo)[] = ['title', 'subtitle', 'description', 'keywords'];
|
||||
const RESTRICTED_WORDS = {
|
||||
beta: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.',
|
||||
};
|
||||
|
||||
/**
|
||||
* Apple restricts certain words from being used in name, description, or keywords.
|
||||
* Using these words likely result in a rejection.
|
||||
*/
|
||||
export const infoRestrictedWords: IssueRule = {
|
||||
id: 'apple.info.restrictedWords',
|
||||
severity: 1,
|
||||
validate(config) {
|
||||
if (!config.apple?.info || Object.keys(config.apple.info).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.keys(config.apple.info)
|
||||
.map(locale =>
|
||||
RESTRICTED_PROPERTIES.map(property => {
|
||||
const value = getStringValue(config.apple?.info?.[locale][property]);
|
||||
const issueDescription = getDescriptionForFirstMatch(value);
|
||||
|
||||
if (issueDescription) {
|
||||
return {
|
||||
id: this.id,
|
||||
severity: this.severity,
|
||||
path: ['apple', 'info', locale, property],
|
||||
message: issueDescription,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(truthy)
|
||||
)
|
||||
.filter(truthy)
|
||||
.flat();
|
||||
},
|
||||
};
|
||||
|
||||
function getDescriptionForFirstMatch(value: string): string | null {
|
||||
const sanitized = value.toLowerCase();
|
||||
|
||||
for (const [word, description] of Object.entries(RESTRICTED_WORDS)) {
|
||||
if (sanitized.includes(word)) {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getStringValue(value?: null | string | string[]): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(' ');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MetadataContext } from '../../context';
|
||||
import { AppleTask } from '../task';
|
||||
import { AgeRatingTask } from './age-rating';
|
||||
import { AppInfoTask } from './app-info';
|
||||
@@ -12,10 +11,7 @@ type AppleTaskOptions = {
|
||||
/**
|
||||
* List of all eligible tasks to sync local store configuration to the App store.
|
||||
*/
|
||||
export function createAppleTasks(
|
||||
_ctx: MetadataContext,
|
||||
{ version }: AppleTaskOptions = {}
|
||||
): AppleTask[] {
|
||||
export function createAppleTasks({ version }: AppleTaskOptions = {}): AppleTask[] {
|
||||
return [
|
||||
new AppVersionTask({ version }),
|
||||
new AppInfoTask(),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { App, Session } from '@expo/apple-utils';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
|
||||
export type MetadataAppStoreAuthentication = {
|
||||
/** The root entity of the App store */
|
||||
app: App;
|
||||
/** The authentication state we used to fetch the root entity */
|
||||
auth: Partial<Session.AuthState>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the bundle identifier from the selected submit profile.
|
||||
* This bundle identifier is used as target for the metadata submission.
|
||||
*/
|
||||
async function resolveAppStoreBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
profile: SubmitProfile,
|
||||
exp: ExpoConfig
|
||||
): Promise<string> {
|
||||
if ('bundleIdentifier' in profile) {
|
||||
return profile.bundleIdentifier ?? (await getBundleIdentifierAsync(projectDir, exp));
|
||||
}
|
||||
|
||||
return await getBundleIdentifierAsync(projectDir, exp);
|
||||
}
|
||||
|
||||
/**
|
||||
* To start syncing ASC entities, we need access to the apple utils App instance.
|
||||
* This resolves both the authentication and that App instance.
|
||||
*/
|
||||
export async function getAppStoreAuthAsync({
|
||||
projectDir,
|
||||
profile,
|
||||
exp,
|
||||
credentialsCtx,
|
||||
}: {
|
||||
projectDir: string;
|
||||
profile: SubmitProfile;
|
||||
exp: ExpoConfig;
|
||||
credentialsCtx: CredentialsContext;
|
||||
}): Promise<MetadataAppStoreAuthentication> {
|
||||
const bundleId = await resolveAppStoreBundleIdentifierAsync(projectDir, profile, exp);
|
||||
|
||||
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
|
||||
assert(authCtx.authState, 'Failed to authenticate with App Store Connect');
|
||||
|
||||
// TODO: improve error handling by mentioning possible configuration errors
|
||||
const app = await App.findAsync(getRequestContext(authCtx), { bundleId });
|
||||
assert(app, `Failed to load app "${bundleId}" from App Store Connect`);
|
||||
|
||||
return { app, auth: authCtx.authState };
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MetadataConfig } from './schema';
|
||||
|
||||
export enum IssueSeverity {
|
||||
info = 0,
|
||||
warn = 1,
|
||||
error = 2,
|
||||
}
|
||||
|
||||
export interface Issue {
|
||||
/** The type of issue, auto-generated for AJV errors based on the validation type */
|
||||
id: string;
|
||||
/** The severity of the lint message */
|
||||
severity: IssueSeverity;
|
||||
/** The data path, using segments, to the source of this message */
|
||||
path: string[];
|
||||
/** A human readable description of the issue, presented to users */
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IssueRule<Data = MetadataConfig> {
|
||||
id: string;
|
||||
severity: IssueSeverity;
|
||||
validate(config: Data): null | Issue | Issue[];
|
||||
}
|
||||
+28
-39
@@ -1,19 +1,13 @@
|
||||
import Ajv from 'ajv';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { AppleConfigReader } from './apple/config/reader';
|
||||
import { AppleConfigWriter } from './apple/config/writer';
|
||||
import { AppleMetadata } from './apple/types';
|
||||
import { MetadataValidationError } from './errors';
|
||||
|
||||
export interface MetadataConfig {
|
||||
/** The store configuration version */
|
||||
configVersion: number;
|
||||
/** All App Store related configuration */
|
||||
apple?: AppleMetadata;
|
||||
}
|
||||
import { AppleConfigReader } from '../apple/config/reader';
|
||||
import { AppleConfigWriter } from '../apple/config/writer';
|
||||
import { MetadataValidationError } from '../errors';
|
||||
import { MetadataConfig } from './schema';
|
||||
import { validateConfig } from './validate';
|
||||
|
||||
/**
|
||||
* Resolve the dynamic config from the user.
|
||||
@@ -27,18 +21,30 @@ async function resolveDynamicConfigAsync(configFile: string): Promise<unknown> {
|
||||
: userConfigOrFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the prefered store config file name from the submit profile.
|
||||
* This is relative to the project directory, and uses `store.config.json` by default.
|
||||
*/
|
||||
function resolveConfigFilePath(profile: SubmitProfile): string {
|
||||
if ('metadataPath' in profile) {
|
||||
return profile.metadataPath ?? 'store.config.json';
|
||||
}
|
||||
|
||||
return 'store.config.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the static configuration file path, based on the metadata context.
|
||||
* This uses any custom name provided, but swaps out the extension for `.json`.
|
||||
*/
|
||||
export function getStaticConfigFilePath({
|
||||
projectDir,
|
||||
metadataPath,
|
||||
profile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
metadataPath: string;
|
||||
profile: SubmitProfile;
|
||||
}): string {
|
||||
const configFile = path.join(projectDir, metadataPath);
|
||||
const configFile = path.join(projectDir, resolveConfigFilePath(profile));
|
||||
const configExtension = path.extname(configFile);
|
||||
|
||||
return path.join(projectDir, `${path.basename(configFile, configExtension)}.json`);
|
||||
@@ -52,14 +58,14 @@ export function getStaticConfigFilePath({
|
||||
*/
|
||||
export async function loadConfigAsync({
|
||||
projectDir,
|
||||
metadataPath,
|
||||
profile,
|
||||
skipValidation = false,
|
||||
}: {
|
||||
projectDir: string;
|
||||
metadataPath: string;
|
||||
profile: SubmitProfile;
|
||||
skipValidation?: boolean;
|
||||
}): Promise<MetadataConfig> {
|
||||
const configFile = path.join(projectDir, metadataPath);
|
||||
const configFile = path.join(projectDir, resolveConfigFilePath(profile));
|
||||
if (!(await fs.pathExists(configFile))) {
|
||||
throw new MetadataValidationError(`Metadata store config file not found: "${configFile}"`);
|
||||
}
|
||||
@@ -67,37 +73,20 @@ export async function loadConfigAsync({
|
||||
const configData = await resolveDynamicConfigAsync(configFile);
|
||||
|
||||
if (!skipValidation) {
|
||||
const { valid, errors: validationErrors } = validateConfig(configData);
|
||||
const issues = validateConfig(configData);
|
||||
|
||||
if (!valid) {
|
||||
throw new MetadataValidationError(`Metadata store config errors found`, validationErrors);
|
||||
if (issues.length > 0) {
|
||||
throw new MetadataValidationError(`Metadata store config errors found`, issues);
|
||||
}
|
||||
}
|
||||
|
||||
return configData as MetadataConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the JSON Schema validation to normalize defaults and flag early config errors.
|
||||
* This includes validating the known store limitations for every configurable property.
|
||||
*/
|
||||
export function validateConfig(config: unknown): {
|
||||
valid: boolean;
|
||||
errors: Ajv.ErrorObject[];
|
||||
} {
|
||||
const validator = new Ajv({ allErrors: true, useDefaults: true })
|
||||
.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
|
||||
.compile(require('../../schema/metadata-0.json'));
|
||||
|
||||
const valid = validator(config) as boolean;
|
||||
|
||||
return { valid, errors: validator.errors || [] };
|
||||
}
|
||||
|
||||
/** Create a versioned deserializer to fetch App Store data from the store configuration. */
|
||||
export function createAppleReader(config: MetadataConfig): AppleConfigReader {
|
||||
assert(config.configVersion === 0, 'Unsupported store configuration version');
|
||||
assert(config.apple, 'No apple configuration found');
|
||||
assert(config.apple !== undefined, 'No apple configuration found');
|
||||
return new AppleConfigReader(config.apple);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AppleMetadata } from '../apple/types';
|
||||
|
||||
export interface MetadataConfig {
|
||||
/** The store configuration version */
|
||||
configVersion: number;
|
||||
/** All App Store related configuration */
|
||||
apple?: AppleMetadata;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { appleRules } from '../apple/rules';
|
||||
import { createValidator, getReadableErrors } from '../utils/ajv';
|
||||
import { Issue, IssueRule, IssueSeverity } from './issue';
|
||||
import { MetadataConfig } from './schema';
|
||||
|
||||
const metadataSchema = require('../../../schema/metadata-0.json');
|
||||
|
||||
/** Validate the user-provided validation for issues */
|
||||
export function validateConfig(config: unknown): Issue[] {
|
||||
const validator = createValidator().compile(metadataSchema);
|
||||
|
||||
validator(config);
|
||||
|
||||
const issues: Issue[] = getReadableErrors(validator.errors ?? []).map(error => {
|
||||
const path = error.path === '$' ? [] : error.path.substring(2).split('.');
|
||||
const id = error.original?.keyword ?? 'unknown';
|
||||
|
||||
return {
|
||||
id: `json-schema.${id}`,
|
||||
path,
|
||||
severity: IssueSeverity.error,
|
||||
message: error.message,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
issues.push(...validateRules(appleRules, config as MetadataConfig));
|
||||
} catch {
|
||||
// When the rules are failing, the json schema validation errors explain the issue
|
||||
// TODO(cedric): optionally add debugging logging for these types of errors
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/** Validate the set of rules against the parsed metadata config */
|
||||
function validateRules(rules: IssueRule[], config: MetadataConfig): Issue[] {
|
||||
const issues: Issue[] = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
const result = rule.validate(config);
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
issues.push(...result);
|
||||
} else if (result) {
|
||||
issues.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { App, Session } from '@expo/apple-utils';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor, EasJsonUtils, SubmitProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import { Actor } from '../user/User';
|
||||
|
||||
export type MetadataContext = {
|
||||
/** Submission profile platform to use */
|
||||
platform: Platform.IOS;
|
||||
/** Resolved submission profile configuration */
|
||||
profile: SubmitProfile<Platform.IOS>;
|
||||
/** Configured store config path, relative from projectDir (defaults to store.config.json) */
|
||||
metadataPath: string;
|
||||
/** Authenticated Expo account */
|
||||
user: Actor;
|
||||
/** The store credentials manager */
|
||||
credentialsCtx: CredentialsContext;
|
||||
/** The app bundle identifier to use for the store */
|
||||
bundleIdentifier: string;
|
||||
/** Root of the Expo project directory */
|
||||
projectDir: string;
|
||||
/** Resolved Expo app configuration */
|
||||
exp: ExpoConfig;
|
||||
};
|
||||
|
||||
export type MetadataAppStoreAuthentication = {
|
||||
/** The root entity of the App store */
|
||||
app: App;
|
||||
/** The authentication state we used to fetch the root entity */
|
||||
auth: Partial<Session.AuthState>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata uses the submission profile to find the configured metadata filename.
|
||||
* Note, only iOS is supported for metadata right now.
|
||||
*/
|
||||
export async function createMetadataContextAsync(params: {
|
||||
projectDir: string;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
profileName?: string;
|
||||
}): Promise<MetadataContext> {
|
||||
const easJsonAccessor = new EasJsonAccessor(params.projectDir);
|
||||
const submitProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
easJsonAccessor,
|
||||
Platform.IOS,
|
||||
params.profileName
|
||||
);
|
||||
|
||||
const exp = params.exp;
|
||||
const user = params.credentialsCtx.user;
|
||||
const bundleIdentifier =
|
||||
submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp));
|
||||
|
||||
return {
|
||||
platform: Platform.IOS,
|
||||
profile: submitProfile,
|
||||
metadataPath: submitProfile.metadataPath ?? 'store.config.json',
|
||||
user,
|
||||
credentialsCtx: params.credentialsCtx,
|
||||
bundleIdentifier,
|
||||
projectDir: params.projectDir,
|
||||
exp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To start syncing ASC entities, we need access to the apple utils App instance.
|
||||
* This ensures we are authenticated and have the app instance ready to use.
|
||||
*/
|
||||
export async function ensureMetadataAppStoreAuthenticatedAsync({
|
||||
credentialsCtx,
|
||||
bundleIdentifier,
|
||||
}: MetadataContext): Promise<MetadataAppStoreAuthentication> {
|
||||
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
|
||||
assert(authCtx.authState, 'Failed to authenticate with App Store Connect');
|
||||
|
||||
// TODO: improve error handling by mentioning possible configuration errors
|
||||
const app = await App.findAsync(getRequestContext(authCtx), { bundleId: bundleIdentifier });
|
||||
assert(app, `Failed to load app "${bundleIdentifier}" from App Store Connect`);
|
||||
|
||||
return { app, auth: authCtx.authState };
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { MetadataEvent } from '../analytics/events';
|
||||
import { Analytics, MetadataEvent } from '../analytics/AnalyticsManager';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import Log from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { AppleData } from './apple/data';
|
||||
import { createAppleTasks } from './apple/tasks';
|
||||
import { createAppleWriter, getStaticConfigFilePath } from './config';
|
||||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
|
||||
import { getAppStoreAuthAsync } from './auth';
|
||||
import { createAppleWriter, getStaticConfigFilePath } from './config/resolve';
|
||||
import { MetadataDownloadError, MetadataValidationError } from './errors';
|
||||
import { subscribeTelemetry } from './utils/telemetry';
|
||||
|
||||
@@ -15,12 +18,24 @@ import { subscribeTelemetry } from './utils/telemetry';
|
||||
* Generate a local store configuration from the stores.
|
||||
* Note, only App Store is supported at this time.
|
||||
*/
|
||||
export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promise<string> {
|
||||
const filePath = getStaticConfigFilePath(metadataCtx);
|
||||
const fileExists = await fs.pathExists(filePath);
|
||||
export async function downloadMetadataAsync({
|
||||
projectDir,
|
||||
profile,
|
||||
exp,
|
||||
analytics,
|
||||
credentialsCtx,
|
||||
}: {
|
||||
projectDir: string;
|
||||
profile: SubmitProfile;
|
||||
exp: ExpoConfig;
|
||||
analytics: Analytics;
|
||||
credentialsCtx: CredentialsContext;
|
||||
}): Promise<string> {
|
||||
const filePath = getStaticConfigFilePath({ projectDir, profile });
|
||||
|
||||
const fileExists = await fs.pathExists(filePath);
|
||||
if (fileExists) {
|
||||
const filePathRelative = path.relative(metadataCtx.projectDir, filePath);
|
||||
const filePathRelative = path.relative(projectDir, filePath);
|
||||
const overwrite = await confirmAsync({
|
||||
message: `Do you want to overwrite the existing "${filePathRelative}"?`,
|
||||
});
|
||||
@@ -29,8 +44,15 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
|
||||
}
|
||||
}
|
||||
|
||||
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
|
||||
const { app, auth } = await getAppStoreAuthAsync({
|
||||
exp,
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
profile,
|
||||
});
|
||||
|
||||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
|
||||
analytics,
|
||||
MetadataEvent.APPLE_METADATA_DOWNLOAD,
|
||||
{ app, auth }
|
||||
);
|
||||
@@ -40,7 +62,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
|
||||
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleWriter();
|
||||
const tasks = createAppleTasks(metadataCtx);
|
||||
const tasks = createAppleTasks();
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ErrorObject } from 'ajv';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log, { link } from '../log';
|
||||
import { Issue } from './config/issue';
|
||||
|
||||
/**
|
||||
* Before syncing data to the ASC API, we need to validate the metadata config.
|
||||
@@ -9,7 +9,7 @@ import Log, { link } from '../log';
|
||||
* and should contain useful information for the user to solve before trying again.
|
||||
*/
|
||||
export class MetadataValidationError extends Error {
|
||||
public constructor(message?: string, public readonly errors: ErrorObject[] = []) {
|
||||
public constructor(message?: string, public readonly errors: Issue[] = []) {
|
||||
super(message ?? 'Store configuration validation failed');
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,9 @@ export function logMetadataValidationError(error: MetadataValidationError): void
|
||||
Log.error(chalk.bold(error.message));
|
||||
if (error.errors?.length > 0) {
|
||||
// TODO(cedric): group errors by property to make multiple errors for same property more readable
|
||||
Log.log(error.errors.map(err => ` - ${chalk.bold(err.dataPath)} ${err.message}`).join('\n'));
|
||||
for (const err of error.errors) {
|
||||
Log.log(` - ${chalk.bold(`$.${err.path.join('.')}`)} ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { MetadataEvent } from '../analytics/events';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
|
||||
import { Analytics, MetadataEvent } from '../analytics/AnalyticsManager';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import Log from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { AppleData } from './apple/data';
|
||||
import { createAppleTasks } from './apple/tasks';
|
||||
import { MetadataConfig, createAppleReader, loadConfigAsync } from './config';
|
||||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
|
||||
import { getAppStoreAuthAsync } from './auth';
|
||||
import { createAppleReader, loadConfigAsync } from './config/resolve';
|
||||
import { MetadataConfig } from './config/schema';
|
||||
import { MetadataUploadError, MetadataValidationError, logMetadataValidationError } from './errors';
|
||||
import { subscribeTelemetry } from './utils/telemetry';
|
||||
|
||||
@@ -12,12 +17,29 @@ import { subscribeTelemetry } from './utils/telemetry';
|
||||
* Sync a local store configuration with the stores.
|
||||
* Note, only App Store is supported at this time.
|
||||
*/
|
||||
export async function uploadMetadataAsync(
|
||||
metadataCtx: MetadataContext
|
||||
): Promise<{ appleLink: string }> {
|
||||
const storeConfig = await loadConfigWithValidationPromptAsync(metadataCtx);
|
||||
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
|
||||
export async function uploadMetadataAsync({
|
||||
projectDir,
|
||||
profile,
|
||||
exp,
|
||||
analytics,
|
||||
credentialsCtx,
|
||||
}: {
|
||||
projectDir: string;
|
||||
profile: SubmitProfile;
|
||||
exp: ExpoConfig;
|
||||
analytics: Analytics;
|
||||
credentialsCtx: CredentialsContext;
|
||||
}): Promise<{ appleLink: string }> {
|
||||
const storeConfig = await loadConfigWithValidationPromptAsync(projectDir, profile);
|
||||
const { app, auth } = await getAppStoreAuthAsync({
|
||||
exp,
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
profile,
|
||||
});
|
||||
|
||||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
|
||||
analytics,
|
||||
MetadataEvent.APPLE_METADATA_UPLOAD,
|
||||
{ app, auth }
|
||||
);
|
||||
@@ -27,7 +49,7 @@ export async function uploadMetadataAsync(
|
||||
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleReader(storeConfig);
|
||||
const tasks = createAppleTasks(metadataCtx, {
|
||||
const tasks = createAppleTasks({
|
||||
// We need to resolve a different version as soon as possible.
|
||||
// This version is the parent model of all changes we are going to push.
|
||||
version: config.getVersion()?.versionString,
|
||||
@@ -61,10 +83,11 @@ export async function uploadMetadataAsync(
|
||||
}
|
||||
|
||||
async function loadConfigWithValidationPromptAsync(
|
||||
metadataCtx: MetadataContext
|
||||
projectDir: string,
|
||||
profile: SubmitProfile
|
||||
): Promise<MetadataConfig> {
|
||||
try {
|
||||
return await loadConfigAsync(metadataCtx);
|
||||
return await loadConfigAsync({ projectDir, profile });
|
||||
} catch (error) {
|
||||
if (error instanceof MetadataValidationError) {
|
||||
logMetadataValidationError(error);
|
||||
@@ -74,7 +97,7 @@ async function loadConfigWithValidationPromptAsync(
|
||||
);
|
||||
|
||||
if (await confirmAsync({ message: 'Do you still want to push the store configuration?' })) {
|
||||
return await loadConfigAsync({ ...metadataCtx, skipValidation: true });
|
||||
return await loadConfigAsync({ projectDir, profile, skipValidation: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AggregateAjvError, HumanError } from '@segment/ajv-human-errors';
|
||||
import Ajv, { ErrorObject } from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
|
||||
const jsonSchema = require('ajv/lib/refs/json-schema-draft-06.json');
|
||||
|
||||
/**
|
||||
* Create a new AJV validator using the JSON Schema 06 draft.
|
||||
* It also adds the additional formats from the `ajv-formats` package.
|
||||
*
|
||||
* @see https://github.com/ajv-validator/ajv-formats
|
||||
*/
|
||||
export function createValidator(): Ajv {
|
||||
const validator = new Ajv({
|
||||
strict: false, // The metadata schema is shared with vscode, including vscode-only properties
|
||||
verbose: true, // Required for `ajv-human-errors`
|
||||
allErrors: true, // Required for `ajv-human-errors`
|
||||
});
|
||||
|
||||
return addFormats(validator).addMetaSchema(jsonSchema);
|
||||
}
|
||||
|
||||
export function getReadableErrors(errors: ErrorObject[] = []): HumanError[] {
|
||||
if (errors.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return new AggregateAjvError(errors).toJSON();
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { App, Session, getRequestClient } from '@expo/apple-utils';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Analytics, MetadataEvent } from '../../analytics/events';
|
||||
import { Analytics, MetadataEvent } from '../../analytics/AnalyticsManager';
|
||||
|
||||
export type TelemetryContext = {
|
||||
app: App;
|
||||
@@ -15,6 +15,7 @@ export type TelemetryContext = {
|
||||
* Returns an execution ID to group all events of a single run together, and a unsubscribe function.
|
||||
*/
|
||||
export function subscribeTelemetry(
|
||||
analytics: Analytics,
|
||||
event: MetadataEvent,
|
||||
options: TelemetryContext
|
||||
): {
|
||||
@@ -29,7 +30,7 @@ export function subscribeTelemetry(
|
||||
|
||||
const responseInterceptorId = interceptors.response.use(
|
||||
response => {
|
||||
Analytics.logEvent(event, {
|
||||
analytics.logEvent(event, {
|
||||
executionId,
|
||||
type: 'response',
|
||||
phase: 'resolved',
|
||||
@@ -42,16 +43,16 @@ export function subscribeTelemetry(
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
Analytics.logEvent(event, {
|
||||
analytics.logEvent(event, {
|
||||
executionId,
|
||||
type: 'response',
|
||||
phase: 'rejected',
|
||||
method: error.request.method.toUpperCase(),
|
||||
url: scrubber(error.config.url),
|
||||
url: scrubber(error.config?.url),
|
||||
error: scrubber(error.message),
|
||||
status: String(error.response?.status),
|
||||
statusText: scrubber(error.response?.statusText),
|
||||
input: scrubber(error.config.data),
|
||||
input: scrubber(error.config?.data),
|
||||
output: scrubber(error.response?.data),
|
||||
});
|
||||
|
||||
|
||||
@@ -98,9 +98,7 @@ export async function validateAppVersionRuntimePolicySupportAsync(
|
||||
}
|
||||
|
||||
export async function installExpoUpdatesAsync(projectDir: string): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log(`Running ${chalk.bold('expo install expo-updates')}`);
|
||||
Log.newLine();
|
||||
Log.log(chalk.gray`> npx expo install expo-updates`);
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-updates']);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import Log from '../../log';
|
||||
|
||||
export async function runAppOnAndroidEmulatorAsync(_appPath: string): Promise<void> {
|
||||
Log.error('Not implemented!');
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import path from 'path';
|
||||
|
||||
import * as simulator from './simulator';
|
||||
import { validateSystemRequirementsAsync } from './systemRequirements';
|
||||
|
||||
export async function runAppOnIosSimulatorAsync(appPath: string): Promise<void> {
|
||||
await validateSystemRequirementsAsync();
|
||||
|
||||
const selectedSimulator = await simulator.selectSimulatorAsync();
|
||||
await simulator.ensureSimulatorBootedAsync(selectedSimulator);
|
||||
|
||||
await simulator.ensureSimulatorAppOpenedAsync(selectedSimulator.udid);
|
||||
|
||||
const bundleIdentifier = await getAppBundleIdentifierAsync(appPath);
|
||||
await simulator.installAppAsync(selectedSimulator.udid, appPath);
|
||||
|
||||
await simulator.launchAppAsync(selectedSimulator.udid, bundleIdentifier);
|
||||
}
|
||||
|
||||
async function getAppBundleIdentifierAsync(appPath: string): Promise<string> {
|
||||
const { stdout, stderr } = await spawnAsync('xcrun', [
|
||||
'plutil',
|
||||
'-extract',
|
||||
'CFBundleIdentifier',
|
||||
'raw',
|
||||
path.join(appPath, 'Info.plist'),
|
||||
]);
|
||||
|
||||
if (!stdout) {
|
||||
throw new Error(
|
||||
`Could not read app bundle identifier from ${path.join(appPath, 'Info.plist')}: ${stderr}`
|
||||
);
|
||||
}
|
||||
|
||||
return stdout.trim();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SpawnOptions, SpawnResult } from '@expo/spawn-async';
|
||||
|
||||
import { xcrunAsync } from './xcrun';
|
||||
|
||||
export async function simctlAsync(args: string[], options?: SpawnOptions): Promise<SpawnResult> {
|
||||
return xcrunAsync(['simctl', ...args], options);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as osascript from '@expo/osascript';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { sleepAsync } from '../../utils/promise';
|
||||
import { simctlAsync } from './simctl';
|
||||
|
||||
interface IosSimulator {
|
||||
runtime: string;
|
||||
osVersion: string;
|
||||
windowName: string;
|
||||
osType: 'iOS';
|
||||
state: 'Booted' | 'Shutdown';
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
udid: string;
|
||||
lastBootedAt?: Date;
|
||||
}
|
||||
|
||||
export async function selectSimulatorAsync(): Promise<IosSimulator> {
|
||||
const bootedSimulator = await getFirstBootedIosSimulatorAsync();
|
||||
|
||||
if (bootedSimulator) {
|
||||
return bootedSimulator;
|
||||
}
|
||||
|
||||
const simulators = await getAvaliableIosSimulatorsListAsync();
|
||||
|
||||
Log.newLine();
|
||||
const { selectedSimulator } = await promptAsync({
|
||||
type: 'select',
|
||||
message: `Select a simulator to run your app on`,
|
||||
name: 'selectedSimulator',
|
||||
choices: simulators.map(simulator => ({
|
||||
title: `ios ${simulator.osVersion} ${simulator.name}`,
|
||||
value: simulator,
|
||||
})),
|
||||
});
|
||||
|
||||
return selectedSimulator;
|
||||
}
|
||||
|
||||
export async function getFirstBootedIosSimulatorAsync(): Promise<IosSimulator | undefined> {
|
||||
const bootedSimulators = await getAvaliableIosSimulatorsListAsync('booted');
|
||||
|
||||
if (bootedSimulators.length > 0) {
|
||||
return bootedSimulators[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getAvaliableIosSimulatorsListAsync(query?: string): Promise<IosSimulator[]> {
|
||||
const { stdout } = query
|
||||
? await simctlAsync(['list', 'devices', '--json', query])
|
||||
: await simctlAsync(['list', 'devices', '--json']);
|
||||
const info = parseSimControlJsonResults(stdout);
|
||||
|
||||
const iosSimulators = [];
|
||||
|
||||
for (const runtime of Object.keys(info.devices)) {
|
||||
// Given a string like 'com.apple.CoreSimulator.SimRuntime.tvOS-13-4'
|
||||
const runtimeSuffix = runtime.split('com.apple.CoreSimulator.SimRuntime.').pop();
|
||||
|
||||
if (!runtimeSuffix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create an array [tvOS, 13, 4]
|
||||
const [osType, ...osVersionComponents] = runtimeSuffix.split('-');
|
||||
|
||||
if (osType === 'iOS') {
|
||||
// Join the end components [13, 4] -> '13.4'
|
||||
const osVersion = osVersionComponents.join('.');
|
||||
const sims = info.devices[runtime];
|
||||
for (const device of sims) {
|
||||
if (device.isAvailable) {
|
||||
iosSimulators.push({
|
||||
...device,
|
||||
runtime,
|
||||
osVersion,
|
||||
windowName: `${device.name} (${osVersion})`,
|
||||
osType: 'iOS' as const,
|
||||
state: device.state as 'Booted' | 'Shutdown',
|
||||
lastBootedAt: device.lastBootedAt ? new Date(device.lastBootedAt) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return iosSimulators;
|
||||
}
|
||||
|
||||
function parseSimControlJsonResults(input: string): any {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch (error: any) {
|
||||
// Nov 15, 2020: Observed this can happen when opening the simulator and the simulator prompts the user to update the xcode command line tools.
|
||||
// Unexpected token I in JSON at position 0
|
||||
if (error.message.includes('Unexpected token')) {
|
||||
Log.error(`Apple's simctl returned malformed JSON:\n${input}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureSimulatorBootedAsync(simulator: IosSimulator): Promise<void> {
|
||||
if (simulator.state === 'Booted') {
|
||||
return;
|
||||
}
|
||||
|
||||
await simctlAsync(['boot', simulator.udid]);
|
||||
}
|
||||
|
||||
export async function openSimulatorAppAsync(simulatorUdid: string): Promise<void> {
|
||||
const args = ['-a', 'Simulator'];
|
||||
if (simulatorUdid) {
|
||||
// This has no effect if the app is already running.
|
||||
args.push('--args', '-CurrentDeviceUDID', simulatorUdid);
|
||||
}
|
||||
await spawnAsync('open', args);
|
||||
}
|
||||
|
||||
export async function launchAppAsync(
|
||||
simulatorUdid: string,
|
||||
bundleIdentifier: string
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log('Launching your app...');
|
||||
|
||||
await simctlAsync(['launch', simulatorUdid, bundleIdentifier]);
|
||||
|
||||
Log.succeed('Successfully launched your app!');
|
||||
}
|
||||
|
||||
// I think the app can be open while no simulators are booted.
|
||||
async function waitForSimulatorAppToStartAsync(
|
||||
maxWaitTimeMs: number,
|
||||
intervalMs: number
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log('Waiting for Simulator app to start...');
|
||||
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < maxWaitTimeMs) {
|
||||
if (await isSimulatorAppRunningAsync()) {
|
||||
return;
|
||||
}
|
||||
await sleepAsync(Math.min(intervalMs, Math.max(maxWaitTimeMs - (Date.now() - startTime), 0)));
|
||||
}
|
||||
throw new Error('Timed out waiting for the iOS simulator to start.');
|
||||
}
|
||||
|
||||
async function isSimulatorAppRunningAsync(): Promise<boolean> {
|
||||
try {
|
||||
const result = await osascript.execAsync(
|
||||
'tell app "System Events" to count processes whose name is "Simulator"'
|
||||
);
|
||||
|
||||
if (result.trim() === '0') {
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('Application isn’t running')) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function ensureSimulatorAppOpenedAsync(simulatorUuid: string): Promise<void> {
|
||||
if (await isSimulatorAppRunningAsync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await openSimulatorAppAsync(simulatorUuid);
|
||||
await waitForSimulatorAppToStartAsync(60 * 1000, 1000);
|
||||
}
|
||||
|
||||
export async function installAppAsync(deviceId: string, filePath: string): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log('Installing your app on the simulator...');
|
||||
|
||||
await simctlAsync(['install', deviceId, filePath]);
|
||||
|
||||
Log.succeed('Successfully installed your app on the simulator!');
|
||||
}
|
||||
|
||||
export async function getSimulatorAppIdAsync(): Promise<string | undefined> {
|
||||
try {
|
||||
return (await osascript.execAsync('id of app "Simulator"')).trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getSimulatorAppIdAsync } from './simulator';
|
||||
import * as xcode from './xcode';
|
||||
import { installXcrunAsync, isXcrunInstalledAsync } from './xcrun';
|
||||
|
||||
function assertPlatform(): void {
|
||||
if (process.platform !== 'darwin') {
|
||||
Log.error('iOS simulator apps can only be run on macOS devices.');
|
||||
throw Error('iOS simulator apps can only be run on macOS devices.');
|
||||
}
|
||||
}
|
||||
|
||||
async function assertCorrectXcodeVersionInstalledAsync(): Promise<void> {
|
||||
const xcodeVersion = await xcode.getXcodeVersionAsync();
|
||||
|
||||
if (!xcodeVersion) {
|
||||
const { goToAppStore } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Xcode needs to be installed, would you like to continue to the App Store?',
|
||||
name: 'goToAppStore',
|
||||
choices: [
|
||||
{ title: 'Yes', value: true },
|
||||
{ title: 'No', value: false },
|
||||
],
|
||||
});
|
||||
|
||||
if (goToAppStore) {
|
||||
await xcode.openAppStoreAsync(xcode.APP_STORE_ID);
|
||||
}
|
||||
|
||||
throw Error('Please try again once Xcode is installed');
|
||||
}
|
||||
|
||||
if (semver.lt(xcodeVersion, xcode.MIN_XCODE_VERSION)) {
|
||||
throw Error(
|
||||
`Xcode version ${chalk.bold(xcodeVersion)} is too old. Please upgrade to version ${chalk.bold(
|
||||
xcode.MIN_XCODE_VERSION
|
||||
)} or higher.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureXcrunInstalledAsync(): Promise<void> {
|
||||
if (!isXcrunInstalledAsync()) {
|
||||
const { installXcrun } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Xcode Command Line Tools need to be installed, continue?',
|
||||
name: 'installXcrun',
|
||||
choices: [
|
||||
{ title: 'Yes', value: true },
|
||||
{ title: 'No', value: false },
|
||||
],
|
||||
});
|
||||
|
||||
if (installXcrun) {
|
||||
await installXcrunAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
throw Error('Please try again once Xcode Command Line Tools are installed');
|
||||
}
|
||||
}
|
||||
|
||||
async function assertSimulatorAppInstalledAsync(): Promise<void> {
|
||||
const simulatorAppId = await getSimulatorAppIdAsync();
|
||||
if (!simulatorAppId) {
|
||||
throw new Error(
|
||||
`Can't determine id of Simulator app; the Simulator is most likely not installed on this machine. Run 'sudo xcode-select -s /Applications/Xcode.app'`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
simulatorAppId !== 'com.apple.iphonesimulator' &&
|
||||
simulatorAppId !== 'com.apple.CoreSimulator.SimulatorTrampoline'
|
||||
) {
|
||||
throw new Error(
|
||||
`Simulator is installed but is identified as '${simulatorAppId}', can't recognize what that is`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// make sure we can run simctl
|
||||
await spawnAsync('xcrun', ['simctl', 'help']);
|
||||
} catch (error: any) {
|
||||
Log.warn(`Unable to run simctl:\n${error.toString()}`);
|
||||
throw new Error(
|
||||
'xcrun is not configured correctly. Ensure `sudo xcode-select --reset` works before running this command again.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateSystemRequirementsAsync(): Promise<void> {
|
||||
assertPlatform();
|
||||
await assertCorrectXcodeVersionInstalledAsync();
|
||||
await ensureXcrunInstalledAsync();
|
||||
await assertSimulatorAppInstalledAsync();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../../log';
|
||||
|
||||
// Based on the RN docs (Aug 2020).
|
||||
export const MIN_XCODE_VERSION = '9.4.0';
|
||||
export const APP_STORE_ID = '497799835';
|
||||
|
||||
export async function getXcodeVersionAsync(): Promise<string | undefined> {
|
||||
try {
|
||||
const { stdout } = await spawnAsync('xcodebuild', ['-version']);
|
||||
|
||||
const version = stdout.match(/Xcode (\d+\.\d+)/)?.[1];
|
||||
|
||||
const semverFormattedVersion = `${version}.0`;
|
||||
|
||||
if (!semver.valid(semverFormattedVersion)) {
|
||||
Log.warn(
|
||||
`Xcode version ${chalk.bold(version)} is in unknown format. Expected format is ${chalk.bold(
|
||||
'12.0'
|
||||
)}.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return semverFormattedVersion;
|
||||
} catch {
|
||||
// not installed
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openAppStoreAsync(appId: string): Promise<void> {
|
||||
const link = getAppStoreLink(appId);
|
||||
await spawnAsync(`open`, [link]);
|
||||
}
|
||||
|
||||
function getAppStoreLink(appId: string): string {
|
||||
if (process.platform === 'darwin') {
|
||||
// TODO: Is there ever a case where the macappstore isn't available on mac?
|
||||
return `macappstore://itunes.apple.com/app/id${appId}`;
|
||||
}
|
||||
return `https://apps.apple.com/us/app/id${appId}`;
|
||||
}
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user