Compare commits

...
18 Commits
Author SHA1 Message Date
Dominik Sokal de965e3693 v2.5.0 2022-10-24 09:41:08 +02:00
Dominik Sokal 5c3afc5141 upgrade eas-cli-local-build-plugin to 0.0.115 2022-10-24 09:40:50 +02:00
Dominik Sokal 50814723c5 [ENG-6183] add secret:push command (#1457)
* add secret:sync command

* address PR feedback

* address PR feedback
2022-10-21 09:52:09 +02:00
Will Schurman c6919e9fd6 [eas-cli] Add changelog entry for #1452 (#1468) 2022-10-20 10:29:08 -07:00
Evan Bacon 6b220f12ae chore: remove table logging in update:view (#1463)
* chore: remove table logging in update:view

* update CHANGELOG.md
2022-10-20 09:18:34 -07:00
Evan Bacon 2aade4c0c8 feat: use versioned cli for export (#1460)
* feat: use versioned cli

* update CHANGELOG.md

* allow EXPO_USE_LOCAL_CLI to disable local CLI
2022-10-19 13:52:37 -07:00
Evan Bacon f16c40a496 chore(updates): fix up install prompt language (#1462)
* chore: fixup install prompt

* update CHANGELOG.md
2022-10-19 13:44:39 -07:00
Brent Vatne aed6725d17 [eas-cli] Handle case where git is installed but not working properly (#1454) 2022-10-19 10:54:16 -07:00
Will Schurman ac6c240217 [eas-cli] Update init command to handle slug and owner (#1452) 2022-10-19 09:34:22 -07:00
Will Schurman 34ae7f5e0b [eas-cli] Clean up withAnalyticsAsync (#1449) 2022-10-18 09:25:14 -07:00
Dominik Sokal 40aa1c7d35 [ENG-5269] add changelog linter (#1451)
* [scripts] add simple CHANGELOG.md linter

* fix CHANGELOG.md

* run lint-changelog in CI
2022-10-18 12:56:03 +02:00
Will Schurman b128af464c [eas-cli] Initialize analytics consistently and move analytics into context (#1444) 2022-10-17 09:43:45 -07:00
Expo CI 1236152120 update CHANGELOG.md 2022-10-17 09:11:35 +00:00
Dominik Sokal 9201af566d v2.4.1 2022-10-17 10:57:02 +02:00
Dominik Sokal cf4eb7a4e4 [ENG-6721][submit] move error messages from CLI (#1443)
* remove submit errors from CLI

* update CHANGELOG.md
2022-10-17 09:45:53 +02:00
Dominik Sokal 3ca02ab740 update CHANGELOG.md 2022-10-14 14:36:43 +02:00
Dominik Sokal c05ef9ccca upgrade eas-cli-local-build-plugin to 0.0.114 2022-10-14 14:33:52 +02:00
Expo CI dccd249cb0 update CHANGELOG.md 2022-10-14 08:03:48 +00:00
76 changed files with 1460 additions and 604 deletions

No files matched your search

+3
View File
@@ -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:
+56 -31
View File
@@ -8,10 +8,35 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Handle more common errors for EAS Submit. ([#1440](https://github.com/expo/eas-cli/pull/1440) by [@dsokal](https://github.com/dsokal))
- Update init command to handle slug and owner. ([#1452](https://github.com/expo/eas-cli/pull/1452) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Initialize analytics consistently and move analytics into context. ([#1444](https://github.com/expo/eas-cli/pull/1444) by [@wschurman](https://github.com/wschurman))
- Skip using `--non-interactive` and `--experimental-bundle` flags when local Expo CLI is installed. ([#1460](https://github.com/expo/eas-cli/pull/1460) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Handle case where git is installed but not working properly. ([#1454](https://github.com/expo/eas-cli/pull/1454) by [@brentvatne](https://github.com/brentvatne))
- Remove table logging in `update:view`. ([#1463](https://github.com/expo/eas-cli/pull/1463) by [@EvanBacon](https://github.com/EvanBacon))
- Fix install prompt language. ([#1462](https://github.com/expo/eas-cli/pull/1462) by [@EvanBacon](https://github.com/EvanBacon))
## [2.4.1](https://github.com/expo/eas-cli/releases/tag/v2.4.1) - 2022-10-17
### 🐛 Bug fixes
- Bump `eas-cli-local-build-plugin` to 0.0.144 to not require `yarn` for local builds. ([c05ef](https://github.com/expo/eas-cli/commit/c05ef9cccaa819d8e5fb84858cb09cadaae90134) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Remove EAS Submit errors from codebase. The errors have been moved to the server side. ([#1443](https://github.com/expo/eas-cli/pull/1443) by [@dsokal](https://github.com/dsokal))
## [2.4.0](https://github.com/expo/eas-cli/releases/tag/v2.4.0) - 2022-10-14
### 🎉 New features
- Handle more common errors for EAS Submit. ([#1440](https://github.com/expo/eas-cli/pull/1440) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Move graphql and session into command context. ([#1435](https://github.com/expo/eas-cli/pull/1435), [#1436](https://github.com/expo/eas-cli/pull/1436) by [@wschurman](https://github.com/wschurman))
@@ -56,10 +81,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))
@@ -235,7 +260,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))
@@ -272,13 +297,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
@@ -288,7 +313,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
@@ -459,7 +484,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
@@ -511,7 +536,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.
@@ -557,7 +582,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
@@ -593,7 +618,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
@@ -691,7 +716,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))
@@ -723,7 +748,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
@@ -769,7 +794,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
@@ -781,7 +806,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
@@ -915,7 +940,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
@@ -995,7 +1020,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
@@ -1015,7 +1040,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
@@ -1029,7 +1054,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
@@ -1109,15 +1134,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))
@@ -1131,7 +1156,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
@@ -1163,7 +1188,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))
@@ -1174,7 +1199,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
@@ -1201,7 +1226,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))
@@ -1262,7 +1287,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
@@ -1287,7 +1312,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))
@@ -1371,7 +1396,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
@@ -1398,7 +1423,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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.4.0"
"version": "2.5.0"
}
+1
View File
@@ -12,6 +12,7 @@
"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"
+47 -47
View File
@@ -125,7 +125,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -142,7 +142,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -159,7 +159,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -173,7 +173,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -223,7 +223,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -244,7 +244,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -264,7 +264,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -284,7 +284,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -307,7 +307,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -337,7 +337,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -354,7 +354,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -371,7 +371,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -406,7 +406,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -443,7 +443,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -490,7 +490,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.4.0/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.5.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -509,7 +509,7 @@ DESCRIPTION
Update a version in native code with a value stored on EAS servers
```
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.4.0/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.5.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -526,7 +526,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -547,7 +547,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -569,7 +569,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -589,7 +589,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -612,7 +612,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -631,7 +631,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -648,7 +648,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -662,7 +662,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -682,7 +682,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -703,7 +703,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -717,7 +717,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -731,7 +731,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -818,7 +818,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -835,7 +835,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -849,7 +849,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -863,7 +863,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -885,7 +885,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -908,7 +908,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -926,7 +926,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -940,7 +940,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -970,7 +970,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1000,7 +1000,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1017,7 +1017,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.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1038,7 +1038,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1060,7 +1060,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -1080,7 +1080,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1101,7 +1101,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1121,7 +1121,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1138,7 +1138,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1160,7 +1160,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1177,7 +1177,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.4.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.5.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.4.0",
"version": "2.5.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -37,6 +37,7 @@
"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",
@@ -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 },
};
}
}
+16 -11
View File
@@ -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;
-60
View File
@@ -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
}
+17 -10
View File
@@ -5,8 +5,8 @@ import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -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,
}
);
}
+3 -2
View File
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json';
import { 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;
+9 -6
View File
@@ -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) {
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.110';
const PLUGIN_PACKAGE_VERSION = '0.0.115';
export interface LocalBuildOptions {
enable: boolean;
+1 -1
View File
@@ -30,7 +30,7 @@ export async function collectMetadataAsync<T extends Platform>(
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata: Metadata = {
trackingContext: ctx.trackingCtx,
trackingContext: ctx.analyticsEventProperties,
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
workflow: ctx.workflow,
@@ -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,
});
+46 -21
View File
@@ -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;
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { getAnalyticsEnabledAsync, setAnalyticsEnabledAsync } from '../analytics/AnalyticsManager';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends EasCommand {
static override description = 'display or change analytics settings';
@@ -10,10 +10,10 @@ export default class AnalyticsView extends EasCommand {
async runAsync(): Promise<void> {
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
if (status) {
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,
@@ -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();
@@ -24,6 +24,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 +34,7 @@ export default class MetadataPull extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig: { exp, projectId, projectDir },
analytics,
} = await this.getContextAsync(MetadataPull, {
nonInteractive: false,
});
@@ -45,11 +47,13 @@ export default class MetadataPull extends EasCommand {
projectDir,
user: actor,
graphqlClient,
analytics,
nonInteractive: false,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
analytics,
projectDir,
exp,
profileName: flags.profile,
@@ -22,6 +22,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 +32,7 @@ export default class MetadataPush extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig: { exp, projectId, projectDir },
analytics,
} = await this.getContextAsync(MetadataPush, {
nonInteractive: false,
});
@@ -43,11 +45,13 @@ export default class MetadataPush extends EasCommand {
projectDir,
user: actor,
graphqlClient,
analytics,
nonInteractive: false,
});
const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
analytics,
projectDir,
exp,
profileName: flags.profile,
@@ -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,9 @@ 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 { 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 +19,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: () => {} }),
@@ -97,11 +99,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 +123,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 +140,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 +168,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();
});
});
});
});
+117 -4
View File
@@ -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)}.`
);
@@ -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;
}
}
+3
View File
@@ -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,
});
@@ -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,
});
}
+3 -16
View File
@@ -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,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,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
) {}
+5
View File
@@ -4,6 +4,7 @@ import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils, SubmitProfile } from '@expo/eas-json';
import assert from 'assert';
import { Analytics } from '../analytics/AnalyticsManager';
import { CredentialsContext } from '../credentials/context';
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
@@ -20,6 +21,8 @@ export type MetadataContext = {
user: Actor;
/** The store credentials manager */
credentialsCtx: CredentialsContext;
/** The analytics manager for EAS cli */
analytics: Analytics;
/** The app bundle identifier to use for the store */
bundleIdentifier: string;
/** Root of the Expo project directory */
@@ -42,6 +45,7 @@ export type MetadataAppStoreAuthentication = {
export async function createMetadataContextAsync(params: {
projectDir: string;
credentialsCtx: CredentialsContext;
analytics: Analytics;
exp: ExpoConfig;
profileName?: string;
}): Promise<MetadataContext> {
@@ -62,6 +66,7 @@ export async function createMetadataContextAsync(params: {
profile: submitProfile,
metadataPath: submitProfile.metadataPath ?? 'store.config.json',
user,
analytics: params.analytics,
credentialsCtx: params.credentialsCtx,
bundleIdentifier,
projectDir: params.projectDir,
+2 -1
View File
@@ -1,7 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import { MetadataEvent } from '../analytics/events';
import { MetadataEvent } from '../analytics/AnalyticsManager';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { AppleData } from './apple/data';
@@ -31,6 +31,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
metadataCtx.analytics,
MetadataEvent.APPLE_METADATA_DOWNLOAD,
{ app, auth }
);
+2 -1
View File
@@ -1,4 +1,4 @@
import { MetadataEvent } from '../analytics/events';
import { MetadataEvent } from '../analytics/AnalyticsManager';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { AppleData } from './apple/data';
@@ -18,6 +18,7 @@ export async function uploadMetadataAsync(
const storeConfig = await loadConfigWithValidationPromptAsync(metadataCtx);
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
metadataCtx.analytics,
MetadataEvent.APPLE_METADATA_UPLOAD,
{ app, auth }
);
@@ -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,7 +43,7 @@ export function subscribeTelemetry(
return response;
},
(error: AxiosError) => {
Analytics.logEvent(event, {
analytics.logEvent(event, {
executionId,
type: 'response',
phase: 'rejected',
+1 -3
View File
@@ -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();
}
+14 -9
View File
@@ -13,7 +13,7 @@ import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync } from '../utils/expoCli';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import uniqBy from '../utils/expodash/uniqBy';
@@ -157,14 +157,19 @@ export async function buildBundlesAsync({
throw new Error('Could not locate package.json');
}
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
]);
if (shouldUseVersionedExpoCLI(projectDir)) {
await expoCommandAsync(projectDir, ['export', '--output-dir', inputDir, '--dump-sourcemap']);
} else {
// Legacy global Expo CLI
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
]);
}
}
export async function resolveInputDirectoryAsync(customInputDirectory: string): Promise<string> {
+8 -7
View File
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { AnalyticsEvent, SubmissionEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { Event, SubmissionEvent } from '../analytics/events';
import {
AndroidSubmissionConfigInput,
IosSubmissionConfigInput,
@@ -22,9 +22,9 @@ export interface SubmissionInput<P extends Platform> {
}
interface AnalyticEvents {
attemptEvent: Event;
successEvent: Event;
failureEvent: Event;
attemptEvent: AnalyticsEvent;
successEvent: AnalyticsEvent;
failureEvent: AnalyticsEvent;
}
export default abstract class BaseSubmitter<
@@ -50,11 +50,11 @@ export default abstract class BaseSubmitter<
const sourceOption = await withAnalyticsAsync<
ResolvedSourceOptions[keyof ResolvedSourceOptions]
>(async () => await this.sourceOptionResolver[sourceOptionKey](), {
>(this.ctx.analytics, async () => await this.sourceOptionResolver[sourceOptionKey](), {
attemptEvent: sourceOptionAnalytics.attemptEvent,
successEvent: sourceOptionAnalytics.successEvent,
failureEvent: sourceOptionAnalytics.failureEvent,
trackingCtx: this.ctx.trackingCtx,
properties: this.ctx.analyticsEventProperties,
});
resolvedSourceOptions[sourceOptionKey] = sourceOption;
}
@@ -91,12 +91,13 @@ export default abstract class BaseSubmitter<
submissionInput: SubmissionInput<P>
): Promise<SubmissionFragment> {
return await withAnalyticsAsync<SubmissionFragment>(
this.ctx.analytics,
async () => this.createSubmissionAsync(submissionInput),
{
attemptEvent: SubmissionEvent.SUBMIT_REQUEST_ATTEMPT,
successEvent: SubmissionEvent.SUBMIT_REQUEST_SUCCESS,
failureEvent: SubmissionEvent.SUBMIT_REQUEST_FAIL,
trackingCtx: this.ctx.trackingCtx,
properties: this.ctx.analyticsEventProperties,
}
);
}
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { SubmissionEvent } from '../../analytics/events';
import { SubmissionEvent } from '../../analytics/AnalyticsManager';
import {
AndroidSubmissionConfigInput,
SubmissionAndroidReleaseStatus,
@@ -1,8 +1,10 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
jester as mockJester,
@@ -74,6 +76,7 @@ describe(AndroidSubmitCommand, () => {
it("throws error if didn't provide serviceAccountKeyPath in the submit profile", async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
@@ -89,6 +92,7 @@ describe(AndroidSubmitCommand, () => {
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -103,6 +107,7 @@ describe(AndroidSubmitCommand, () => {
it('sends a request to Submission Service', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
@@ -119,6 +124,7 @@ describe(AndroidSubmitCommand, () => {
nonInteractive: false,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -141,6 +147,7 @@ describe(AndroidSubmitCommand, () => {
it('assigns the build ID to submission', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([fakeBuildFragment as BuildFragment]);
@@ -160,6 +167,7 @@ describe(AndroidSubmitCommand, () => {
nonInteractive: false,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -4,6 +4,7 @@ import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testAndroidAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-android';
import {
@@ -139,6 +140,7 @@ describe(getServiceAccountKeyPathAsync, () => {
describe(getServiceAccountKeyResultAsync, () => {
it('returns a local Service Account Key file with a ServiceAccountSourceType.path source', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
projectDir: testProject.projectRoot,
@@ -153,6 +155,7 @@ describe(getServiceAccountKeyResultAsync, () => {
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -178,6 +181,7 @@ describe(getServiceAccountKeyResultAsync, () => {
filePath: '/project_dir/subdir/service-account.json',
}));
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
projectDir: testProject.projectRoot,
@@ -192,6 +196,7 @@ describe(getServiceAccountKeyResultAsync, () => {
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -213,6 +218,7 @@ describe(getServiceAccountKeyResultAsync, () => {
it('returns a remote Service Account Key file with a ServiceAccountSourceType.credentialService source', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
projectDir: testProject.projectRoot,
@@ -227,6 +233,7 @@ describe(getServiceAccountKeyResultAsync, () => {
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
+13 -6
View File
@@ -3,8 +3,11 @@ import { Platform } from '@expo/eas-build-job';
import { SubmitProfile } from '@expo/eas-json';
import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, SubmissionEvent } from '../analytics/events';
import {
Analytics,
AnalyticsEventProperties,
SubmissionEvent,
} from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
@@ -14,7 +17,7 @@ export interface SubmissionContext<T extends Platform> {
accountName: string;
archiveFlags: SubmitArchiveFlags;
credentialsCtx: CredentialsContext;
trackingCtx: TrackingContext;
analyticsEventProperties: AnalyticsEventProperties;
exp: ExpoConfig;
nonInteractive: boolean;
platform: T;
@@ -24,6 +27,7 @@ export interface SubmissionContext<T extends Platform> {
projectName: string;
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
applicationIdentifierOverride?: string;
}
@@ -45,6 +49,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
applicationIdentifier?: string;
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
exp: ExpoConfig;
projectId: string;
}): Promise<SubmissionContext<T>> {
@@ -56,6 +61,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
exp,
projectId,
graphqlClient,
analytics,
} = params;
const { env, ...rest } = params;
const projectName = exp.slug;
@@ -67,19 +73,20 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
projectDir,
user: actor,
graphqlClient,
analytics,
projectInfo: { exp, projectId },
nonInteractive,
});
}
const trackingCtx = {
const analyticsEventProperties = {
tracking_id: uuidv4(),
platform: params.platform,
...(accountId && { account_id: accountId }),
project_id: projectId,
};
Analytics.logEvent(SubmissionEvent.SUBMIT_COMMAND, trackingCtx);
rest.analytics.logEvent(SubmissionEvent.SUBMIT_COMMAND, analyticsEventProperties);
return {
...rest,
@@ -87,7 +94,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
credentialsCtx,
projectName,
user: actor,
trackingCtx,
analyticsEventProperties,
applicationIdentifierOverride: applicationIdentifier,
};
}
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { SubmissionEvent } from '../../analytics/events';
import { SubmissionEvent } from '../../analytics/AnalyticsManager';
import { MinimalAscApiKey } from '../../credentials/ios/credentials';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
@@ -3,6 +3,7 @@ import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
jester as mockJester,
@@ -38,6 +39,7 @@ const projectId = uuidv4();
async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platform.IOS>> {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const analytics = instance(mock<Analytics>());
return await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
@@ -50,6 +52,7 @@ async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platfor
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -2,6 +2,7 @@ import { Platform } from '@expo/eas-build-job';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
jester as mockJester,
@@ -47,6 +48,7 @@ describe(getFromCredentialsServiceAsync, () => {
it('returns an App Specific Password from the credentialService source', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
@@ -59,6 +61,7 @@ describe(getFromCredentialsServiceAsync, () => {
nonInteractive: false,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -78,6 +81,7 @@ describe(getFromCredentialsServiceAsync, () => {
});
it('returns an ASC API Key from the credentialService source', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
@@ -90,6 +94,7 @@ describe(getFromCredentialsServiceAsync, () => {
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -1,7 +1,9 @@
import { Platform } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
jester as mockJester,
@@ -54,6 +56,7 @@ describe(IosSubmitCommand, () => {
it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
@@ -67,6 +70,7 @@ describe(IosSubmitCommand, () => {
nonInteractive: true,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
@@ -79,6 +83,7 @@ describe(IosSubmitCommand, () => {
it('sends a request to Submission Service', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
@@ -96,6 +101,7 @@ describe(IosSubmitCommand, () => {
nonInteractive: false,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
});
+3 -2
View File
@@ -1,8 +1,8 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { SubmissionEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { SubmissionEvent } from '../analytics/events';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
import Log, { link } from '../log';
@@ -17,6 +17,7 @@ export async function submitAsync<T extends Platform>(
ctx: SubmissionContext<T>
): Promise<SubmissionFragment> {
return await withAnalyticsAsync(
ctx.analytics,
async () => {
const command =
ctx.platform === Platform.ANDROID
@@ -28,7 +29,7 @@ export async function submitAsync<T extends Platform>(
attemptEvent: SubmissionEvent.SUBMIT_COMMAND_ATTEMPT,
successEvent: SubmissionEvent.SUBMIT_COMMAND_SUCCESS,
failureEvent: SubmissionEvent.SUBMIT_COMMAND_FAIL,
trackingCtx: ctx.trackingCtx,
properties: ctx.analyticsEventProperties,
}
);
}
+14 -112
View File
@@ -1,119 +1,21 @@
import { SubmissionError } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import Log from '../../log';
enum SubmissionErrorCode {
ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR',
ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR',
ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR',
ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR',
UPLOAD_TAKING_TOO_LONG_ERROR = 'SUBMISSION_SERVICE_COMMON_UPLOAD_TAKING_TOO_LONG_ERROR',
ANDROID_UNKNOWN_ERROR = 'SUBMISSION_SERVICE_ANDROID_UNKNOWN_ERROR',
ANDROID_FIRST_UPLOAD_ERROR = 'SUBMISSION_SERVICE_ANDROID_FIRST_UPLOAD_ERROR',
ANDROID_OLD_VERSION_CODE_ERROR = 'SUBMISSION_SERVICE_ANDROID_OLD_VERSION_CODE_ERROR',
ANDROID_MISSING_PRIVACY_POLICY = 'SUBMISSION_SERVICE_ANDROID_MISSING_PRIVACY_POLICY',
IOS_OLD_APP_VERSION = 'SUBMISSION_SERVICE_IOS_OLD_APP_VERSION',
IOS_OLD_BUILD_NUMBER = 'SUBMISSION_SERVICE_IOS_OLD_BUILD_NUMBER',
IOS_INVALID_APP_VERSION_NUMBER = 'SUBMISSION_SERVICE_IOS_INVALID_APP_VERSION_NUMBER',
IOS_INVALID_BUILD_NUMBER = 'SUBMISSION_SERVICE_IOS_INVALID_BUILD_NUMBER',
IOS_UNKNOWN_ERROR = 'SUBMISSION_SERVICE_IOS_UNKNOWN_ERROR',
IOS_MISSING_APP_ICON = 'SUBMISSION_SERVICE_IOS_MISSING_APP_ICON',
IOS_TRANSPARENT_APP_ICON = 'SUBMISSION_SERVICE_IOS_TRANSPARENT_APP_ICON',
IOS_INVALID_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_SIGNATURE',
IOS_INCORRECT_CREDENTIALS = 'SUBMISSION_SERVICE_IOS_INVALID_CREDENTIALS',
IOS_IPAD_INVALID_ORIENTATION = 'SUBMISSION_SERVICE_IOS_IPAD_INVALID_ORIENTATION',
IOS_APPLE_MAINTENANCE = 'SUBMISSION_SERVICE_IOS_APPLE_MAINTENANCE',
IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE',
IOS_SIGNED_WITH_ADHOC_PROFILE = 'SUBMISSION_SERVICE_IOS_SIGNED_WITH_ADHOC_PROFILE',
}
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR]:
"Failed to download the archive file (Response code: 404 Not Found). Make sure the URL you've provided is correct.",
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR]:
'Failed to download the archive file (Response code: 403 Forbidden). This is most probably caused by trying to upload an expired build artifact. All EAS build artifacts expire after 30 days.',
[SubmissionErrorCode.ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR]:
'The compressed archive is corrupt, in an unsupported format, or contains an invalid application format. Supported files include .apk, .aab, and .ipa files and one of these files compressed into a .tar.gz archive.',
[SubmissionErrorCode.ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR]:
"EAS Submit couldn't find a valid build artifact within provided compressed archive.\n" +
'If you provide a tar.gz archive, it should contain at least one .apk/.aab/.ipa file, depending on the submission platform.',
[SubmissionErrorCode.ANDROID_UNKNOWN_ERROR]:
"We couldn't figure out what went wrong. See logs to learn more.",
[SubmissionErrorCode.ANDROID_FIRST_UPLOAD_ERROR]:
"You haven't submitted this app to Google Play Store yet. The first submission of the app needs to be performed manually.\n" +
`${learnMore('https://expo.fyi/first-android-submission')}.`,
[SubmissionErrorCode.ANDROID_OLD_VERSION_CODE_ERROR]:
"You've already submitted this version of the app.\n" +
'Versions are identified by Android version code (expo.android.versionCode in app.json).\n' +
"If you're submitting a managed Expo project, increment the version code in app.json and build the project with eas build.\n" +
`${learnMore('https://expo.fyi/bumping-android-version-code')}.`,
[SubmissionErrorCode.ANDROID_MISSING_PRIVACY_POLICY]:
'The app has permissions that require a privacy policy set for the app.\n' +
`${learnMore('https://expo.fyi/missing-privacy-policy')}.`,
[SubmissionErrorCode.IOS_OLD_APP_VERSION]:
"You've already submitted this version of the app.\n" +
'Versions are identified by CFBundleShortVersionString from Info.plist (expo.version in app.json).\n' +
"If you're submitting an Expo project built with EAS Build, increment the version (expo.version) in app.json and build the project again.",
[SubmissionErrorCode.IOS_OLD_BUILD_NUMBER]:
"You've already submitted this build of the app.\n" +
'Builds are identified by CFBundleVersion from Info.plist (expo.ios.buildNumber in app.json).\n' +
"If you're submitting an Expo project built with EAS Build, increment the build number (expo.ios.buildNumber) in app.json and build the project again.\n" +
`${learnMore('https://expo.fyi/bumping-ios-build-number')}.`,
[SubmissionErrorCode.IOS_INVALID_APP_VERSION_NUMBER]:
'Invalid app version number.\n' +
'Versions are identified by CFBundleShortVersionString from Info.plist (expo.version in app.json).\n' +
"If you're submitting an Expo project built with EAS Build, fix the version (expo.version) in app.json and build the project again.\n" +
`${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}.`,
[SubmissionErrorCode.IOS_INVALID_BUILD_NUMBER]:
'Invalid build number.\n' +
'Builds are identified by CFBundleVersion from Info.plist (expo.ios.buildNumber in app.json).\n' +
"If you're submitting an Expo project built with EAS Build, fix the build number (expo.ios.buildNumber) in app.json and build the project again.\n" +
`${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion'
)}.`,
[SubmissionErrorCode.IOS_UNKNOWN_ERROR]:
"We couldn't figure out what went wrong. See logs to learn more.",
[SubmissionErrorCode.IOS_MISSING_APP_ICON]:
'Your iOS app icon is missing or is an invalid format. The icon must be a 1024x1024 PNG image with no transparency.\n' +
'Check your icon image and icon configuration in app.json.\n' +
`${learnMore('https://docs.expo.dev/guides/app-icons/')}`,
[SubmissionErrorCode.IOS_TRANSPARENT_APP_ICON]: `Invalid App Store Icon. The App Store Icon can't be transparent nor contain an alpha channel.`,
[SubmissionErrorCode.IOS_INVALID_SIGNATURE]:
'Your app signature seems to be invalid.\n' +
"Check your iOS Distribution Certificate and your app's Provisioning Profile.\n" +
`${learnMore('https://docs.expo.dev/distribution/app-signing')}`,
[SubmissionErrorCode.IOS_INCORRECT_CREDENTIALS]:
'Your Apple ID or app-specific password is incorrect. Verify that you entered them correctly and try again.',
[SubmissionErrorCode.IOS_IPAD_INVALID_ORIENTATION]:
"Your app doesn't support iPad multitasking and has to require full screen.\n" +
"If you're submitting a managed Expo project, set the `expo.ios.requireFullScreen` to true in app.json and build the project again.\n" +
`${learnMore('https://expo.fyi/ipad-requires-fullscreen')}`,
[SubmissionErrorCode.IOS_APPLE_MAINTENANCE]:
'It looks like Apple servers are undergoing an unscheduled maintenance. Try again later.',
[SubmissionErrorCode.IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE]:
'Invalid Provisioning Profile Signature (ITMS-90165)\n' +
"Some of Apple's certificates have expired.\n" +
'Delete your Provisioning Profile from your account. Then rebuild the app interactively to generate a new one, and try submitting it to the App Store again.',
[SubmissionErrorCode.UPLOAD_TAKING_TOO_LONG_ERROR]:
'Submission has reached the timeout limit. Try again.',
[SubmissionErrorCode.IOS_SIGNED_WITH_ADHOC_PROFILE]:
'Invalid Provisioning Profile for Apple App Store distribution. The application was signed with an Ad Hoc/Enterprise Provisioning Profile, which is meant for "Internal Distribution". In order to distribute an app on the store, it must be signed with a Distribution Provisioning Profile. Ensure that you are submitting the correct build, or rebuild the application with a Distribution Provisioning Profile and submit that new build.',
};
const UNKNOWN_ERROR_CODES = [
'SUBMISSION_SERVICE_ANDROID_UNKNOWN_ERROR',
'SUBMISSION_SERVICE_IOS_UNKNOWN_ERROR',
];
/**
* Returns a boolean indicating whether the submission logs should be printed.
*/
export function printSubmissionError(error: SubmissionError): boolean {
if (
error.errorCode &&
(Object.values(SubmissionErrorCode) as string[]).includes(error.errorCode)
) {
const errorCode = error.errorCode as SubmissionErrorCode;
Log.addNewLineIfNone();
Log.error(SubmissionErrorMessages[errorCode]);
return [SubmissionErrorCode.ANDROID_UNKNOWN_ERROR, SubmissionErrorCode.IOS_UNKNOWN_ERROR].some(
code => code === errorCode
);
} else {
Log.log(error.message);
Log.addNewLineIfNone();
if (!error.message || (error.errorCode && UNKNOWN_ERROR_CODES.includes(error.errorCode))) {
Log.error(error.message ?? `We couldn't figure out what went wrong. See logs to learn more.`);
return true;
} else {
Log.error(error.message);
return false;
}
}
+10
View File
@@ -10,6 +10,7 @@ import { RequestedPlatform } from '../platform';
import { confirmAsync } from '../prompts';
import { getActorDisplayName } from '../user/User';
import groupBy from '../utils/expodash/groupBy';
import formatFields from '../utils/formatFields';
import { ProfileData } from '../utils/profiles';
export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'> & {
@@ -43,6 +44,15 @@ export const UPDATE_COLUMNS = [
export const UPDATE_COLUMNS_WITH_BRANCH = ['Branch', ...UPDATE_COLUMNS];
export function formatUpdateGroup(update: FormattedUpdateGroupDescription): string {
return formatFields([
{ label: 'Platforms', value: update.platforms },
{ label: 'Runtime Version', value: update.runtimeVersion },
{ label: 'Message', value: update.message },
{ label: 'Group ID', value: update.group },
]);
}
export function getPlatformsForGroup({
group,
updates = [],
+11 -14
View File
@@ -5,7 +5,7 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ApiV2Error } from '../ApiV2Error';
import * as Analytics from '../analytics/rudderstackClient';
import { AnalyticsWithOrchestration } from '../analytics/AnalyticsManager';
import { ApiV2Client } from '../api';
import { createGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CurrentUserQuery } from '../graphql/generated';
@@ -13,7 +13,6 @@ import { UserQuery } from '../graphql/queries/UserQuery';
import Log, { learnMore } from '../log';
import { promptAsync, selectAsync } from '../prompts';
import { getStateJsonPath } from '../utils/paths';
import { getActorDisplayName } from './User';
import { fetchSessionSecretAndUserAsync } from './fetchSessionSecretAndUser';
type UserSettingsData = {
@@ -54,7 +53,9 @@ type LoggedInAuthenticationInfo =
type Actor = NonNullable<CurrentUserQuery['meActor']>;
export default class SessionManager {
private currentUser: Actor | undefined;
private currentActor: Actor | undefined;
constructor(private readonly analytics: AnalyticsWithOrchestration) {}
public getAccessToken(): string | null {
return process.env.EXPO_TOKEN ?? null;
@@ -83,27 +84,23 @@ export default class SessionManager {
}
public async logoutAsync(): Promise<void> {
this.currentUser = undefined;
this.currentActor = undefined;
await this.setSessionAsync(undefined);
}
public async getUserAsync(): Promise<Actor | undefined> {
if (!this.currentUser && (this.getAccessToken() || this.getSessionSecret())) {
if (!this.currentActor && (this.getAccessToken() || this.getSessionSecret())) {
const authenticationInfo = {
accessToken: this.getAccessToken(),
sessionSecret: this.getSessionSecret(),
};
const user = await UserQuery.currentUserAsync(createGraphqlClient(authenticationInfo));
this.currentUser = user ?? undefined;
if (user) {
await Analytics.setUserDataAsync(user.id, {
username: getActorDisplayName(user),
user_id: user.id,
user_type: user.__typename,
});
const actor = await UserQuery.currentUserAsync(createGraphqlClient(authenticationInfo));
this.currentActor = actor ?? undefined;
if (actor) {
this.analytics.setActor(actor);
}
}
return this.currentUser;
return this.currentActor;
}
/**
@@ -3,6 +3,7 @@ import { vol } from 'memfs';
import path from 'path';
import { ApiV2Error } from '../../ApiV2Error';
import { AnalyticsWithOrchestration } from '../../analytics/AnalyticsManager';
import { ApiV2Client } from '../../api';
import Log from '../../log';
import { promptAsync, selectAsync } from '../../prompts';
@@ -34,6 +35,12 @@ const authStub: any = {
const OLD_ENV = process.env;
const analytics: AnalyticsWithOrchestration = {
logEvent: jest.fn((): void => {}),
setActor: jest.fn((): void => {}),
flushAsync: jest.fn(async (): Promise<void> => {}),
};
beforeEach(() => {
vol.reset();
jest.resetModules();
@@ -48,27 +55,27 @@ afterEach(() => {
describe(SessionManager, () => {
describe('getSession', () => {
it('returns null when session is not stored', () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(sessionManager['getSession']()).toBeNull();
});
it('returns stored session data', async () => {
await fs.mkdirp(path.dirname(getStateJsonPath()));
await fs.writeJSON(getStateJsonPath(), { auth: authStub });
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(sessionManager['getSession']()).toMatchObject(authStub);
});
});
describe('setSessionAsync', () => {
it('stores empty session data', async () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['setSessionAsync']();
expect(await fs.pathExists(getStateJsonPath())).toBeTruthy();
});
it('stores actual session data', async () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['setSessionAsync'](authStub);
expect(await fs.readJSON(getStateJsonPath())).toMatchObject({ auth: authStub });
});
@@ -76,25 +83,25 @@ describe(SessionManager, () => {
describe('getAccessToken', () => {
it('returns null when envvar is undefined', () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(sessionManager['getAccessToken']()).toBeNull();
});
it('returns token when envar is defined', () => {
process.env.EXPO_TOKEN = 'mytesttoken';
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(sessionManager['getAccessToken']()).toBe('mytesttoken');
});
});
describe('getSessionSecret', () => {
it('returns null when session is not stored', () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(sessionManager['getSessionSecret']()).toBeNull();
});
it('returns secret when session is stored', async () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['setSessionAsync'](authStub);
expect(sessionManager['getSessionSecret']()).toBe(authStub.sessionSecret);
});
@@ -102,18 +109,18 @@ describe(SessionManager, () => {
describe('getUserAsync', () => {
it('skips fetching user without access token or session secret', async () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(await sessionManager.getUserAsync()).toBeUndefined();
});
it('fetches user when access token is defined', async () => {
process.env.EXPO_TOKEN = 'accesstoken';
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
expect(await sessionManager.getUserAsync()).toMatchObject({ __typename: 'User' });
});
it('fetches user when session secret is defined', async () => {
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['setSessionAsync']({
sessionSecret: 'blah',
userId: '1234',
@@ -131,7 +138,7 @@ describe(SessionManager, () => {
id: 'USER_ID',
username: 'USERNAME',
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['loginAsync']({ username: 'USERNAME', password: 'PASSWORD' });
expect(await fs.readFile(getStateJsonPath(), 'utf8')).toMatchInlineSnapshot(`
"{
@@ -154,7 +161,7 @@ describe(SessionManager, () => {
id: 'USER_ID',
username: 'USERNAME',
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['loginAsync']({ username: 'USERNAME', password: 'PASSWORD' });
expect(sessionManager['getSessionSecret']()).toBe('SESSION_SECRET');
@@ -174,7 +181,7 @@ describe(SessionManager, () => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
const sessionManagerRetryUsernamePasswordAuthWithOTPAsyncSpy = jest.spyOn(
sessionManager as any,
'retryUsernamePasswordAuthWithOTPAsync'
@@ -233,7 +240,7 @@ describe(SessionManager, () => {
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
const sessionManagerLoginAsyncSpy = jest.spyOn(sessionManager as any, 'loginAsync');
await sessionManager['retryUsernamePasswordAuthWithOTPAsync']('blah', 'blah', {
@@ -262,7 +269,7 @@ describe(SessionManager, () => {
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
const sessionManagerLoginAsyncSpy = jest.spyOn(sessionManager as any, 'loginAsync');
await sessionManager['retryUsernamePasswordAuthWithOTPAsync']('blah', 'blah', {
@@ -296,7 +303,7 @@ describe(SessionManager, () => {
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
const sessionManagerLoginAsyncSpy = jest.spyOn(sessionManager as any, 'loginAsync');
@@ -329,7 +336,7 @@ describe(SessionManager, () => {
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await expect(
sessionManager['retryUsernamePasswordAuthWithOTPAsync']('blah', 'blah', {
@@ -363,7 +370,7 @@ describe(SessionManager, () => {
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['retryUsernamePasswordAuthWithOTPAsync']('blah', 'blah', {
secondFactorDevices: [
@@ -409,7 +416,7 @@ describe(SessionManager, () => {
});
const apiV2PostSpy = jest.spyOn(ApiV2Client.prototype, 'postAsync');
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await sessionManager['retryUsernamePasswordAuthWithOTPAsync']('blah', 'blah', {
secondFactorDevices: [
{
@@ -456,7 +463,7 @@ describe(SessionManager, () => {
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
const sessionManager = new SessionManager();
const sessionManager = new SessionManager(analytics);
await expect(
sessionManager['retryUsernamePasswordAuthWithOTPAsync']('blah', 'blah', {
+7
View File
@@ -1,9 +1,16 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { boolish } from 'getenv';
import resolveFrom from 'resolve-from';
import Log from '../log';
export function shouldUseVersionedExpoCLI(projectDir: string): boolean {
// Users can disable local CLI settings EXPO_USE_LOCAL_CLI=false
// https://github.com/expo/expo/blob/69eddda7bb1dbfab44258f468cf7f22984c1e44e/packages/expo/bin/cli.js#L10
return !!resolveFrom.silent(projectDir, '@expo/cli') && boolish('EXPO_USE_LOCAL_CLI', true);
}
export async function expoCommandAsync(
projectDir: string,
args: string[],
@@ -0,0 +1,13 @@
import intersection from '../intersection';
describe(intersection, () => {
it('returns an empty list if the are no common items', () => {
expect(intersection([1, 2, 3], [4, 5, 6])).toEqual([]);
});
it('returns a list with common items if the are some', () => {
expect(intersection([1, 2, 3, 4], [3, 4, 5, 6])).toEqual([3, 4]);
});
it('works for empty lists', () => {
expect(intersection([], [])).toEqual([]);
});
});
@@ -0,0 +1,5 @@
export default function intersection<T = any>(items1: T[], items2: T[]): T[] {
const set1 = new Set(items1);
const set2 = new Set(items2);
return Array.from(new Set([...set1].filter(i => set2.has(i))));
}
+22 -2
View File
@@ -18,9 +18,29 @@ import { Client } from '../vcs';
export default class GitClient extends Client {
public override async ensureRepoExistsAsync(): Promise<void> {
if (!(await isGitInstalledAsync())) {
try {
if (!(await isGitInstalledAsync())) {
Log.error(
`${chalk.bold('git')} command not found. Install it before proceeding or set ${chalk.bold(
'EAS_NO_VCS=1'
)} to use EAS CLI without Git (or any other version control system).`
);
Log.error(learnMore('https://expo.fyi/eas-vcs-workflow'));
Errors.exit(1);
}
} catch (error: any) {
Log.error(
`${chalk.bold('git')} command not found. Install it before proceeding or set ${chalk.bold(
`${chalk.bold('git')} found, but ${chalk.bold('git --help')} exited with status ${
error?.status
}${error?.stderr ? `:` : '.'}`
);
if (error?.stderr) {
Log.error(error?.stderr);
}
Log.error(
`Repair your Git installation, or set ${chalk.bold(
'EAS_NO_VCS=1'
)} to use EAS CLI without Git (or any other version control system).`
);
+2
View File
@@ -6,6 +6,7 @@
"scripts": {
"changelog-entry": "ts-node-esm src/changelogEntry.ts",
"next-version": "ts-node-esm src/nextVersion.ts",
"lint-changelog": "ts-node-esm src/lintChangelog.ts",
"release": "./src/release.sh",
"release-changelog": "ts-node-esm src/releaseChangelog.ts",
"update-local-plugin": "./src/updateLocalPlugin.sh",
@@ -17,6 +18,7 @@
"node": ">=14.16"
},
"dependencies": {
"chalk": "5.1.2",
"dateformat": "4.5.1",
"fs-extra": "10.1.0",
"lodash-es": "4.17.21",
+1 -6
View File
@@ -1,11 +1,6 @@
import { URL } from 'node:url';
export enum EntryCategory {
BreakingChange = 'breaking-change',
NewFeature = 'new-feature',
BugFix = 'bug-fix',
Chore = 'chore',
}
import { EntryCategory } from './entry.js';
export const CHANGELOG_PATH = new URL('../../../CHANGELOG.md', import.meta.url);
+92
View File
@@ -0,0 +1,92 @@
import { nullthrows } from '../nullthrows.js';
export interface Entry {
category: EntryCategory;
message: string;
author: string;
prNumber: number;
}
export enum EntryCategory {
BreakingChange = 'breaking-change',
NewFeature = 'new-feature',
BugFix = 'bug-fix',
Chore = 'chore',
}
export function formatEntry({ message, prNumber, author }: Entry): string {
const capitalizedMessage = `${message[0].toUpperCase()}${message.slice(1)}`;
const messageWithDot = capitalizedMessage.endsWith('.')
? capitalizedMessage
: `${capitalizedMessage}.`;
return `${messageWithDot} ([#${prNumber}](https://github.com/expo/eas-cli/pull/${prNumber}) by [@${author}](https://github.com/${author}))`;
}
const COMMIT_OR_PR_LINK = /\[#?\w+\]\(https:\/\/github\.com\/expo\/eas-cli\/(commit|pull)\/\w+\)/;
const COMMIT_OR_PR_LINK_GROUPS =
/\[(?<number>#?\w+)\]\(https:\/\/github\.com\/expo\/eas-cli\/(?<path>(commit|pull)\/\w+)\)/;
const COMMIT_OR_PR_LINKS = new RegExp(
`${COMMIT_OR_PR_LINK.source}(, ${COMMIT_OR_PR_LINK.source})*`
);
const AUTHOR_LINK = /\[@[\w-]+\]\(https:\/\/github\.com\/(apps\/)?[\w-]+\)/;
const AUTHOR_LINK_GROUPS =
/\[@(?<author1>[\w-]+)\]\(https:\/\/github\.com\/(apps\/)?(?<author2>[\w-]+)\)/;
const AUTHORS = new RegExp(`by ${AUTHOR_LINK.source}(, ${AUTHOR_LINK.source})*`);
const ENTRY_REGEX = new RegExp(
`.+ \\(${COMMIT_OR_PR_LINKS.source} ${AUTHORS.source}(, ${COMMIT_OR_PR_LINKS.source} ${AUTHORS.source})*\\)(\n.*)*`
);
export function validateEntry(entry: string): Error[] | void {
if (!entry.match(ENTRY_REGEX)) {
return [new Error('Invalid changelog entry, check formatting')];
}
const errors: Error[] = [];
const commitsOrPRs = nullthrows(entry.match(new RegExp(COMMIT_OR_PR_LINK, 'g')));
for (const commitOrPR of commitsOrPRs) {
const maybeError = validateCommitOrPR(commitOrPR);
if (maybeError) {
errors.push(maybeError);
}
}
const authors = nullthrows(entry.match(new RegExp(AUTHOR_LINK, 'g')));
for (const author of authors) {
const maybeError = validateAuthor(author);
if (maybeError) {
errors.push(maybeError);
}
}
if (errors.length > 0) {
return errors;
}
}
function validateCommitOrPR(commitOrPR: string): Error | void {
const matched = commitOrPR.match(COMMIT_OR_PR_LINK_GROUPS);
const { number, path } = nullthrows(matched?.groups);
if (number.startsWith('#')) {
const prNumber = number.slice(1);
if (`pull/${prNumber}` !== path) {
return new Error('The PR number does not match the URL');
}
} else {
if (!path.startsWith(`commit/${number}`)) {
return new Error('The commit hash does not match the URL');
}
}
}
function validateAuthor(author: string): Error | void {
const matched = author.match(AUTHOR_LINK_GROUPS);
const { author1, author2 } = nullthrows(matched?.groups);
if (author1 !== author2) {
return new Error(
`The URL and user handle should point at the same user, ${author1} !== ${author2}`
);
}
}
+2 -16
View File
@@ -1,4 +1,5 @@
import { CATEGORY_HEADERS, EntryCategory } from './changelog/consts.js';
import { CATEGORY_HEADERS } from './changelog/consts.js';
import { Entry, EntryCategory, formatEntry } from './changelog/entry.js';
import { readAndParseChangelogAsync, writeChangelogAsync } from './changelog/file.js';
import * as markdown from './markdown.js';
import { nullthrows } from './nullthrows.js';
@@ -6,13 +7,6 @@ import { nullthrows } from './nullthrows.js';
const [rawCategory, ...rest] = process.argv.slice(2);
const message = rest.join(' ');
interface Entry {
category: EntryCategory;
message: string;
author: string;
prNumber: number;
}
(async function main(rawCategory: string, rawMessage: string): Promise<void> {
const [category, message] = sanitizeInput(rawCategory, rawMessage);
@@ -64,14 +58,6 @@ function addEntry(tokens: markdown.Tokens, entry: Entry): void {
listToken.items.push(listItemToken);
}
function formatEntry({ message, prNumber, author }: Entry): string {
const capitalizedMessage = `${message[0].toUpperCase()}${message.slice(1)}`;
const messageWithDot = capitalizedMessage.endsWith('.')
? capitalizedMessage
: `${capitalizedMessage}.`;
return `${messageWithDot} ([#${prNumber}](https://github.com/expo/eas-cli/pull/${prNumber}) by [@${author}](https://github.com/${author}))`;
}
function sanitizeInput(
rawCategory: string,
rawMessage: string
+51
View File
@@ -0,0 +1,51 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import { validateEntry } from './changelog/entry.js';
import { readAndParseChangelogAsync } from './changelog/file.js';
import * as markdown from './markdown.js';
interface InvalidEntry {
entry: string;
errors: Error[];
}
(async function main(): Promise<void> {
const invalidEntries: InvalidEntry[] = [];
const tokens = await readAndParseChangelogAsync();
const listTokens = tokens.filter(
(token): token is markdown.ListToken => token.type === markdown.TokenType.LIST
);
for (const listToken of listTokens) {
for (const item of listToken.items) {
if (item.raw) {
const errors = validateEntry(item.raw);
if (errors) {
invalidEntries.push({
entry: item.raw,
errors,
});
}
}
}
}
if (invalidEntries.length === 0) {
console.log('CHANGELOG.md is valid');
return;
}
console.error('CHANGELOG.md is invalid:');
for (const invalidEntry of invalidEntries) {
console.error(invalidEntry.entry.trim());
for (const error of invalidEntry.errors) {
console.error(chalk.red(` ${error}`));
}
}
process.exit(1);
})().catch(err => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { CATEGORY_HEADERS, EntryCategory } from './changelog/consts.js';
import { CATEGORY_HEADERS } from './changelog/consts.js';
import { EntryCategory } from './changelog/entry.js';
import { readAndParseChangelogAsync } from './changelog/file.js';
import * as markdown from './markdown.js';
+5
View File
@@ -101,6 +101,11 @@ arg@^4.1.0:
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
[email protected]:
version "5.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.1.2.tgz#d957f370038b75ac572471e83be4c5ca9f8e8c45"
integrity sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
Generated
+5
View File
@@ -5890,6 +5890,11 @@ dot-prop@^6.0.1:
dependencies:
is-obj "^2.0.0"
[email protected]:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
dotenv@^16.0.0:
version "16.0.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"