Compare commits

..
Author SHA1 Message Date
Quinlan Jung f20d970616 tmp 2024-05-20 15:55:47 +02:00
Quinlan Jung f3712d5dfb amend apple team 2024-05-20 15:55:47 +02:00
Quinlan Jung cba211d82c manage asc api key 2024-05-20 15:55:19 +02:00
Expo CI 61109ea2c1 update CHANGELOG.md 2024-05-17 07:32:39 +00:00
Szymon Dziedzic f889929f5f v9.0.7 2024-05-17 09:27:12 +02:00
Szymon Dziedzic 4bb314192e upgrade eas-cli-local-build-plugin to 1.0.112 2024-05-17 09:27:05 +02:00
Szymon Dziedzic 1a170283db [eas-cli] add --repack flag to eas build command (#2384)
* [eas-cli] add `--repack` flag to `eas build` command

* update Android prepare job
2024-05-17 09:18:38 +02:00
Expo CI ca803eed3f update CHANGELOG.md 2024-05-15 10:57:34 +00:00
Stanisław Chmiela 4b26472975 v9.0.6 2024-05-15 12:52:40 +02:00
Stanisław Chmiela 70b854abb1 [eas-cli] Add turtleJobRunId to UpdatePublishMutation input (#2381)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We want to save the relationship between a job run and an update.

# How

See https://github.com/expo/universe/pull/15329.

# Test Plan

Tested with staging.
2024-05-15 12:51:49 +02:00
Expo CI 2576a97704 update CHANGELOG.md 2024-05-13 15:59:00 +00:00
Szymon Dziedzic 15e50b1536 v9.0.5 2024-05-13 17:53:24 +02:00
Szymon Dziedzic d6f3784cc0 [eas-cli] install dependencies before resolving expo config (#2380) 2024-05-13 17:52:23 +02:00
Expo CI a6f7d37d71 update CHANGELOG.md 2024-05-13 14:54:54 +00:00
Szymon Dziedzic 675c36a157 v9.0.4 2024-05-13 16:49:55 +02:00
Bartosz Kaszubowski b2961a275b [eas-cli] diagnostics: add loader and progress information (#2378)
* [eas-cli] diagnostics: add loader and progress information

* add changelog entry
2024-05-13 16:47:13 +02:00
Szymon Dziedzic 57928d1fd6 [eas-cli] create keystore for Android emulator builds in init:onboarding (#2379)
* [eas-cli] create keystore for Android emulator builds in `init:onboarding`

* use correct build profile
2024-05-13 16:46:32 +02:00
Szymon Dziedzic ff9d533248 [eas-cli] add init:onboarding flow for no gh repo connected (#2373)
* [eas-cli] add `init:onboarding` flow for no gh repo connected

* fix types

* don't wait
2024-05-13 16:43:13 +02:00
Expo CI e1ebcc0b8a update CHANGELOG.md 2024-05-10 17:38:09 +00:00
Stanisław Chmiela 628a7262cf v9.0.3 2024-05-10 19:33:08 +02:00
Stanisław Chmiela 05be162ec1 [eas-cli] Prevent ora flooding logs (#2375)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

I noticed logs look off in CI when running `eas update --auto`.

<img width="1000" alt="Zrzut ekranu 2024-05-10 o 17 50 10" src="https://github.com/expo/eas-cli/assets/1151041/ed50d014-62f1-4342-bde2-0673f49d52b4">

# How

I pinpointed it to Ora being restarted on every log.

# Test Plan

Ran `CI=1 easd update --auto` and got ⬇️ which I think is much more readable.

```
CI=1 easd update --auto
- Exporting...
[expo-cli] Starting Metro Bundler
[expo-cli] Android Bundling complete 3492ms
[expo-cli] iOS Bundling complete 3493ms
[expo-cli] iOS Building Hermes bytecode for the bundle
[expo-cli] Android Building Hermes bytecode for the bundle
[expo-cli] 
[expo-cli] Bundle                      Size
[expo-cli] ┌ index.ios.hbc           858 kB
[expo-cli] ├ index.android.hbc       864 kB
[expo-cli] ├ index.ios.hbc.map      3.45 MB
[expo-cli] └ index.android.hbc.map  3.48 MB
[expo-cli] 
[expo-cli] 💡 JavaScript bundle sizes affect startup time. Learn more: https://expo.fyi/javascript-bundle-sizes
[expo-cli] Finished saving JS Bundles
[expo-cli] Dumping asset map
[expo-cli] Dumping source maps
[expo-cli] Preparing additional debugging files
[expo-cli] Export was successful. Your exported files can be found in dist
✔ Exported bundle(s)
- Uploading...
✔ Uploaded 2 app bundles
✔ Uploading assets skipped - no new assets found
ℹ 1 iOS asset, 1 Android asset (maximum: 20000 total per update). Learn more about asset limits: https://expo.fyi/eas-update-asset-limits
- Publishing...
✔ Published!
```
2024-05-10 19:31:55 +02:00
Bartosz Kaszubowski ff2fa425bd [eas-cli] fix typo in onboarding commit message (#2376) 2024-05-10 19:19:13 +02:00
Expo Bot a621e395fc Upgrade eas-build dependencies (#2372) 2024-05-10 17:53:25 +02:00
Stanisław Chmiela 2260f74434 s/Website link/EAS Dashboard when displaying update info (#2374)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Implementing @tchayen's suggestion.

# How

Changed wording

# Test Plan

None.
2024-05-10 17:47:31 +02:00
Szymon Dziedzic 6dc9253d2d [eas-cli] improve prompt asking user to specify target clone dir in eas init:onboarding (#2371) 2024-05-10 14:56:51 +02:00
Expo CI b6570bdfcf update CHANGELOG.md 2024-05-09 17:44:21 +00:00
Szymon Dziedzic 4d1675c96f v9.0.2 2024-05-09 19:39:29 +02:00
Szymon Dziedzic 00d9009967 [eas-cli] make sure that spinner is not used for interactive git clone command in inint:onboarding and fallback to cloning via https (#2368)
* [eas-cli] make sure that spinner is not used for interactive `git clone` command in `inint:onboarding`

* fallback to https
2024-05-09 19:38:48 +02:00
Szymon Dziedzic b68840b76b fix eas-json bump workflow 2024-05-09 18:47:49 +02:00
Expo CI 6d8be295bf update CHANGELOG.md 2024-05-09 14:12:44 +00:00
Piotr Szeremeta 40c1cbe9f6 v9.0.1 2024-05-09 16:07:32 +02:00
Szymon Dziedzic efae679089 [eas-cli] improvements to eas onboarding after dogfooding (#2367) 2024-05-09 15:05:32 +02:00
Szymon Dziedzic 6a2a3fe037 [eas-json] update image tags in eas.schema.json (#2363)
* [eas-json] update image tags in `eas.schema.json`

* update CHANGELOG.md

* [eas-json] delete detailed image descriptions for `sdk-xy` tags
2024-05-09 14:55:35 +02:00
Tomasz Czajęcki 917987a41e [eas-cli] fix onboarding mutation (#2365)
* fix onboarding mutation

* Skip passing __typename
2024-05-09 14:48:09 +03:00
Bartosz Kaszubowski bbe8fb9698 [eas-cli] onboarding logs content tweaks (#2366) 2024-05-09 13:09:59 +02:00
Szymon Dziedzic d656103357 fix update eas-json in universe workflow 2024-05-08 17:04:42 +02:00
Expo CI 4b7ad9f23a update CHANGELOG.md 2024-05-08 15:01:08 +00:00
Szymon Dziedzic 4db3979ab5 v9.0.0 2024-05-08 16:56:18 +02:00
Szymon Dziedzic 63e0bf2a24 upgrade eas-cli-local-build-plugin to 1.0.107 2024-05-08 16:55:54 +02:00
Szymon Dziedzic a740aa174b [ENG-12115][eas-cli] add eas onboarding flow (#2338)
* [eas-cli] add eas onboarding flow

* mark command as hidden and prompt user before starting any action

* add gql queries

* regenerate gql schema

* fix type issues

* mark isCLIDone as true

* apply suggested changes

* improvements after cr

* more improvements
2024-05-08 16:52:14 +02:00
Szymon Dziedzic f9ef83ba52 [eas-cli] don't allow to specify custom global version of deprecated expo-cli (#2361)
* [eas-cli] don't allow to specify custom global version of deprecated `expo-cli`

* update CHANGELOG.md

* update dependencies

* print a warning vs throwing an error

* mark as deprecated

* print warning nicer

* fix changelog entry

* fix changelog
2024-05-08 12:48:26 +02:00
Expo BotandStanisław Chmiela de8956c2ec Upgrade eas-build dependencies (#2360)
* Upgrade eas-build dependencies

* update CHANGELOG.md

* Fix typecheck

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2024-05-08 10:21:20 +02:00
Will Schurman 41622c0cd3 [eas-cli] Pass env from process.env and build profile to expo-updates CLI calls where applicable (#2359) 2024-05-06 09:06:27 -07:00
Quinlan Jung d54abee7c3 [ENG-8801][eas-cli] allow modification of provisioning profile in CI, add --freeze-credentials flag (#2347)
* freeze creds flag

* allow profile to be modified in non interactive mode

* Temporary Commit at 4/24/2024, 4:15:30 PM

* Temporary Commit at 4/25/2024, 4:33:46 PM

* Temporary Commit at 4/25/2024, 5:24:12 PM

* update CHANGELOG.md

* Temporary Commit at 4/25/2024, 6:30:26 PM

* Temporary Commit at 5/2/2024, 1:10:17 PM
2024-05-02 13:38:42 -07:00
Will Schurman cdd046b647 [eas-cli] Remove more classic updates code (#2357) 2024-05-02 11:01:48 -07:00
dependabot[bot] 4577cf957d Bump ejs from 3.1.8 to 3.1.10 (#2356)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.8 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.8...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-01 13:48:50 +02:00
Expo CI 84b56f86eb update CHANGELOG.md 2024-05-01 10:24:12 +00:00
Szymon Dziedzic b2580d0cc2 v8.0.0 2024-05-01 12:19:32 +02:00
Szymon Dziedzic 0f18ea01a9 upgrade eas-cli-local-build-plugin to 1.0.102 2024-05-01 12:19:22 +02:00
Szymon Dziedzic fe9927862c [ENG-12122][eas-cli] don't prompt users to set push notifications by default if they don't have the expo-notifications installed (#2343)
* [eas-cli] don't prompt to set up push notifications if `promptToConfigurePushNotifications` is `undefined` and `expo-notifications` is not present

* update eas.schema.json

* update CHANGELOG.md
2024-05-01 10:23:48 +02:00
lzkb bc2f6e60f4 [eas-cli] Show the -s, --sso option in the login command help. (#2353)
* Show the -s, --sso option in the login command help.

* Update CHANGELOG
2024-04-29 19:54:03 -07:00
Will Schurman 6ac1e54f67 [eas-cli] Improve login info message for other login options (#2352) 2024-04-29 08:56:10 -07:00
Expo Bot 479e8d0b5d Upgrade eas-build dependencies (#2351)
* Upgrade eas-build dependencies

* update CHANGELOG.md
2024-04-29 16:45:27 +02:00
Quinlan Jung 7ed62ec7f1 [ENG-11658][eas-cli] dont generate channel on eas update without flag (#2346)
* Temporary Commit at 4/25/2024, 2:50:35 PM

* update CHANGELOG.md
2024-04-26 11:23:26 -07:00
Expo CI 87000c36c1 update CHANGELOG.md 2024-04-26 16:40:41 +00:00
Szymon Dziedzic a9d08f39ad v7.8.5 2024-04-26 18:35:33 +02:00
Szymon Dziedzic 55c156dd00 Revert "v7.8.5"
This reverts commit 85de580d84.
2024-04-26 18:33:59 +02:00
Szymon Dziedzic 736d414cd6 [ci] fix build macos workflow (#2348) 2024-04-26 18:33:00 +02:00
Szymon Dziedzic 85de580d84 v7.8.5 2024-04-26 17:07:23 +02:00
Szymon Dziedzic 1a65b2cc4e [ENG-12118][eas-cli] improve error message if server returns UNAUTHORIZED_ERROR (#2345)
* [eas-cli] improve error message if server returns `UNAUTHORIZED_ERROR`

* update CHANGELOG.md
2024-04-26 12:35:26 +02:00
Will Schurman b3fca978d0 [eas-cli] Fill in min expo-updates version for expo-updates CLI (#2344) 2024-04-25 09:18:13 -07:00
Szymon Dziedzic ba4c4e5954 fix changelog 2024-04-24 18:38:13 +02:00
Will Schurman 383caa0354 [eas-cli] Add explicit workflow arg to expo-update CLI calls (#2340) 2024-04-24 09:15:37 -07:00
Szymon Dziedzic 2e43d4bba0 v7.8.4 2024-04-24 18:13:54 +02:00
Szymon Dziedzic b995d1d713 upgrade eas-cli-local-build-plugin to 1.0.101 2024-04-24 18:13:47 +02:00
Szymon Dziedzic d307f2343b [ENG-12120][eas-cli] make multi-select for revoking distribution certificates more readable (#2342)
* [eas-cli] make multi-select for revoking distribution certificates more readable

* update CHANGELOG.md
2024-04-24 18:03:07 +02:00
Szymon Dziedzic 9eeaee544a [ENG-12116][eas-cli] improve error message displayed when EAS CLI version doesn't satisfy minimal version required specified in eas.json (#2341)
* [eas-cli] improve error message displayed when EAS CLI version doesn't satisfy minimal version required specified in `eas.json`

* fix typo

* update CHANGELOG.md
2024-04-24 17:58:55 +02:00
Radosław Krzemień d9092e15f2 [eas-cli] [ENG-11602] Update eas.schema.json (#2337)
* [eas-cli] Update eas.schema.json

Updated the eas.schema.json with the current image values for Android images

See: https://linear.app/expo/issue/ENG-11310/fix-warnings-errors-that-come-up-when-adding-eas-to-a-react-native-cli

* [eas-cli] Update eas.schema.json

Updated the eas.schema.json with the current image values for Android images

See: https://linear.app/expo/issue/ENG-11602/preinstall-gradle-and-ndks-used-in-the-3-latests-sdks-on-android

* [eas-cli] Update eas.schema.json

Returned the auto option

See: https://linear.app/expo/issue/ENG-11602/preinstall-gradle-and-ndks-used-in-the-3-latests-sdks-on-android

* [eas-cli] Update eas.schema.json

Updated descriptions

See: https://linear.app/expo/issue/ENG-11602/preinstall-gradle-and-ndks-used-in-the-3-latests-sdks-on-android

* [eas-cli] Update eas.schema.json

Removed stable option

See: https://linear.app/expo/issue/ENG-11602/preinstall-gradle-and-ndks-used-in-the-3-latests-sdks-on-android

* update CHANGELOG.md
2024-04-23 20:17:30 +02:00
Expo CI f90f085dcc update CHANGELOG.md 2024-04-23 15:33:25 +00:00
Szymon Dziedzic 61bb001059 v7.8.3 2024-04-23 17:25:28 +02:00
Szymon Dziedzic 9ef86f51fa [ENG-12057][eas-cli] don't prompt user to set android.package and ios.bundleIdentifier value when runing in non-interactive mode (#2336)
* [eas-cli] don't prompt user to set `android.package` and `ios.bundleIdentifier` value when runing in non-interactive mode

* update CHANGELOG.md

* rename test
2024-04-23 17:16:29 +02:00
Quinlan Jung 44cd3b2669 [eas-cli] amend credential removal wording (#2334)
* Temporary Commit at 4/18/2024, 8:40:55 PM

* update CHANGELOG.md
2024-04-19 10:41:18 -07:00
Szymon Dziedzic 0c706048be [ENG-9817][ci] bump @expo/eas-json version in universe after the release (#2142)
* [ci] bump `@expo/eas-json` version in universe after the release

* Update .github/workflows/release.yml

* test

* edit eas.json

* Revert "edit eas.json"

This reverts commit 3979a1b1fb.

* Revert "test"

This reverts commit 69db39f548.
2024-04-19 10:22:44 +02:00
Expo CI 211a3fb6bf update CHANGELOG.md 2024-04-15 18:29:07 +00:00
Szymon Dziedzic baeb88f5be v7.8.2 2024-04-15 20:23:12 +02:00
Szymon Dziedzic ec3c27f744 upgrade eas-cli-local-build-plugin to 1.0.100 2024-04-15 20:21:47 +02:00
Szymon Dziedzic 054732c245 Revert "v7.8.2"
This reverts commit 0f45f938b1.
2024-04-15 20:18:21 +02:00
Szymon Dziedzic 70c876776d [ci] use correct workingdir for getting x86 macos build (#2331) 2024-04-15 20:12:05 +02:00
Quinlan Jung 0f45f938b1 v7.8.2 2024-04-15 10:32:58 -07:00
Quinlan Jung c8313f36e7 [ENG-11823][2/2] move credentials endpoints to paginated (#2327)
* [eas-cli] move credentials endpoints to paginated

* switch over android creds to paginated endpoints

* update CHANGELOG.md
2024-04-15 10:19:15 -07:00
Quinlan Jung a8cde5e81d [1/2][eas-cli] paginated progress bar (#2326)
* [eas-cli] paginated progress bar

* update CHANGELOG.md
2024-04-15 10:11:29 -07:00
Will Schurman d05142f9fc [eas-cli] Fix display of errors when expo-updates CLI command fails (#2324) 2024-04-12 13:36:54 -07:00
Szymon Dziedzic 8e26c7c508 [ci] build darwin-arm64 executable as well when releasing new EAS CLI version (#2323) 2024-04-12 18:43:58 +02:00
Expo CI c29018df20 update CHANGELOG.md 2024-04-11 10:04:02 +00:00
Szymon Dziedzic 8b7d977b5b v7.8.1 2024-04-11 11:56:23 +02:00
Szymon Dziedzic 12f98c2471 upgrade eas-cli-local-build-plugin to 1.0.97 2024-04-11 11:56:17 +02:00
Szymon Dziedzic d6186edb69 [eas-cli] stop querying Build.resourceClass (#2320)
* [eas-cli] stop querying `Build.resourceClass`

* update CHANGELOG.md
2024-04-11 11:45:20 +02:00
dependabot[bot] b20cf472bf Bump tar from 6.2.0 to 6.2.1 (#2319)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-11 11:42:01 +02:00
Szymon Dziedzic 0ab64d5eaa [eas-cli] fix README.md command files URLs (#2318)
* [eas-cli] fix `README.md` command files URLs

* update CHANGELOG.md
2024-04-11 11:41:36 +02:00
Expo Bot 40afed198f Upgrade eas-build dependencies (#2316)
* Upgrade eas-build dependencies

* update CHANGELOG.md
2024-04-08 21:13:53 +02:00
Expo CI 5337929696 update CHANGELOG.md 2024-04-08 12:37:01 +00:00
Szymon Dziedzic 4c97fc31f8 v7.8.0 2024-04-08 14:29:06 +02:00
Szymon Dziedzic 42252cd355 upgrade eas-cli-local-build-plugin to 1.0.96 2024-04-08 14:28:59 +02:00
Szymon Dziedzic 784b52bddb [eas-cli] add --build-logger-level flag to eas build command (#2313)
* [eas-cli] add `--build-logger-level` flag to `eas build` command

* update CHANGELOG.md

* fix types

* fix changelog

* apply suggestions from cr

* fix changelog
2024-04-08 14:11:47 +02:00
Szymon Dziedzic d85b3283f0 [eas-cli] mark default image tag and old images as deprecated and add new image tags to eas.schema.json (#2298)
* [eas-cli] mark `default` image tag as deprecated

* add new image tags to `eas.schema.json`

* update CHANGELOG.md

* update CHANGELOG.md

* deprecate JDK 8 and NDK r19c images

* update CHANGELOG.md

* use better warn message for Xcode < 14 images and link to our changelog post

* update warn message

* don't deprecate Androdi images in this PR

* fix changelog

* use correct latest image
2024-04-08 12:15:03 +02:00
Expo CI 0c332eb6d3 update CHANGELOG.md 2024-04-05 19:24:01 +00:00
Szymon Dziedzic 26bb7052ef v7.7.0 2024-04-05 21:16:01 +02:00
Szymon Dziedzic 8333d3361a upgrade eas-cli-local-build-plugin to 1.0.95 2024-04-05 21:15:52 +02:00
Szymon Dziedzic f62a8cad27 [eas-json] add Xcode 15.3 image (#2312)
* [eas-json] add Xcode 15.3 image

* update CHANGELOG.md

* fix changelog
2024-04-05 20:30:22 +02:00
Piotr Szeremeta b52145c0a7 Fix project metadata spinner persisting after upload (#2311) 2024-04-03 17:18:01 +02:00
Piotr SzeremetaandSzymon Dziedzic e06fc189aa [ENG-11445] Add '--verbose-fastlane' flag for verbose fastlane logs (#2276)
* [ENG-11445] Add verbose-fastlane flag for verbose fastlane logs

Make isVerboseFastlaneEnabled required parameter

Co-authored-by: Szymon Dziedzic <[email protected]>

* update CHANGELOG.md

* [ENG-11445] Add verbose-fastlane for android builds

* [ENG-11445] Update graphql

* [ENG-11445] Fix tests

---------

Co-authored-by: Szymon Dziedzic <[email protected]>
2024-04-03 12:05:12 +02:00
Piotr Szeremeta b346f96c63 [ENG-10412] Upload projectMetadata file (#2149)
* [ENG-10412] Upload projectMetadata file

* Lint

* [ENG-10412] Apply review suggestions

* update graphql
2024-04-03 11:47:44 +02:00
Juwan Wheatley 26ba0719ce [eas-cli] add a credentials:configure-build subcommand that prepares a project for EAS Build without executing the build from the CLI (#2282)
* [eas-cli] create new `init-build` command under the `credentials` topic

* get hacky version of cli command working

* add all option to flag

* extend the Manage<Platform> actions to reduce code duplication

* prompt user for platform if it doesn't exist

* prompt the user to choose a profile

* add a changelog entry

* call SetUpBuildCredentials for android
2024-03-28 15:55:19 -04:00
dependabot[bot] c33dff99d2 Bump express from 4.18.2 to 4.19.2 (#2302)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 08:48:50 +01:00
Expo BotandStanisław Chmiela 3d7d851d07 Upgrade eas-build dependencies (#2304)
Co-authored-by: Stanisław Chmiela <[email protected]>
2024-03-27 21:29:08 +01:00
Expo CI fad3f1346c update CHANGELOG.md 2024-03-27 16:02:19 +00:00
Stanisław Chmiela dece2e5242 v7.6.2 2024-03-27 16:55:26 +01:00
Expo Bot f579441e05 Upgrade eas-build dependencies (#2301) 2024-03-27 16:53:41 +01:00
Expo CI e40634297b update CHANGELOG.md 2024-03-25 14:02:09 +00:00
Szymon Dziedzic 17ea5d4f01 v7.6.1 2024-03-25 14:55:03 +01:00
Szymon Dziedzic d803e52085 upgrade eas-cli-local-build-plugin to 1.0.89 2024-03-25 14:54:43 +01:00
Expo Bot e400515fa7 Upgrade eas-build dependencies (#2293)
* Upgrade eas-build dependencies

* update CHANGELOG.md
2024-03-22 17:04:55 +01:00
Szymon Dziedzic e6233b3028 [eas-cli] use GitNoCommitClient for eas build:internal command (#2295)
* [eas-cli] use `GitNoCommitClient` for `eas build:internal` command

* Revert "[eas-cli] use `GitNoCommitClient` for `eas build:internal` command"

This reverts commit d83a9a5a5e.

* fix resolving vcs client in context

* update CHANGELOG.md

* fix tests
2024-03-22 16:55:49 +01:00
Quinlan Jung eec975c171 [eas-cli] fix asset limit punctuation (#2296)
* Temporary Commit at 3/21/2024, 12:10:10 PM

* update CHANGELOG.md
2024-03-21 13:08:55 -07:00
Expo Bot 08ea07bf5c Upgrade eas-build dependencies (#2291)
* Upgrade eas-build dependencies

* update CHANGELOG.md
2024-03-18 17:52:07 +01:00
Expo CI e5d94ad15c update CHANGELOG.md 2024-03-18 16:23:58 +00:00
Szymon Dziedzic a51fbd37ca v7.6.0 2024-03-18 17:16:25 +01:00
Szymon Dziedzic 1c78e09676 upgrade eas-cli-local-build-plugin to 1.0.86 2024-03-18 17:16:11 +01:00
Szymon Dziedzic d1edec6549 [eas-cli] bump @expo/apple-utils to switch between developer.apple.com and developer-mdn.apple.com domains when one of them doesn't work (#2290)
* [eas-cli] bump `@expo/apple-utils` to switch between `developer.apple.com` and `developer-mdn.apple.com` domains when one of them is not working

* update CHANGELOG.md

* bump version
2024-03-18 17:12:06 +01:00
Szymon Dziedzic ab6483f7a5 [eas-cli] send a path to custom build config to EAS Build server with posix separator for Windows (#2285)
* [eas-cli] send a path to custom build config to EAS Build server with posix separator for Windows

* add process.platform to if statement

* update CHANGELOG.md

* apply suggestions from cr

* remove unnecessary check

* apply suggested change
2024-03-18 17:00:49 +01:00
Stanisław Chmiela 32848f6255 Log dirty files in non-interactive when bailing (#2288) 2024-03-18 13:03:42 +01:00
dependabot[bot] 7e2aae333d Bump follow-redirects from 1.15.4 to 1.15.6 (#2284)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-15 10:29:05 +01:00
Expo Bot 191cb2396c Upgrade eas-build dependencies (#2283) 2024-03-14 21:54:34 +01:00
Expo Bot 33fb5fad03 Upgrade eas-build dependencies (#2277)
* Upgrade eas-build dependencies

* update CHANGELOG.md
2024-03-12 10:59:51 +01:00
Expo CI 3ec6e5dd14 update CHANGELOG.md 2024-03-11 11:38:53 +00:00
Szymon Dziedzic b6acbf05d8 v7.5.0 2024-03-11 12:31:11 +01:00
Szymon Dziedzic 957874d8cc upgrade eas-cli-local-build-plugin to 1.0.83 2024-03-11 12:31:01 +01:00
Expo Bot 1a5fbc2342 Upgrade eas-build dependencies (#2274)
* Upgrade eas-build dependencies

* update CHANGELOG.md
2024-03-11 12:29:22 +01:00
Szymon Dziedzic 68cd7e6fba [ENG-11416][eas-cli] add --auto-submit flag to eas build:internal command (#2271)
* [eas-cli] add `--auto-submit` option to `eas build:internal` command

fix types

revert unnecessary changes

one more revert

* update CHANGELOG.md

* try to fix changelog

* try again

* fix changelog
2024-03-10 17:24:50 +01:00
Expo CI 628a3f708a update CHANGELOG.md 2024-03-10 02:57:28 +00:00
166 changed files with 15181 additions and 3406 deletions

No files matched your search

+29 -1
View File
@@ -69,16 +69,22 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 20
- run: brew install python-setuptools
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64
yarn oclif pack:tarballs --no-xz --targets darwin-x64,darwin-arm64
- id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm64
run: echo "filename=$(ls eas-*-arm64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
env:
@@ -88,6 +94,15 @@ jobs:
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload darwin-arm64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm64.outputs.filename }}
asset_name: ${{ steps.arm64.outputs.filename }}
asset_content_type: application/gzip
build-windows:
name: Build for Windows
runs-on: ubuntu-latest
@@ -211,3 +226,16 @@ jobs:
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
open-pr-in-universe:
name: Open a PR with `@expo/eas-json` bump in universe
needs: publish-to-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Open a PR with `@expo/eas-json` version bump in universe
uses: benc-uk/workflow-dispatch@v1
with:
repo: expo/universe
ref: main
workflow: update-eas-json.yml
token: ${{ secrets.EXPO_BOT_PAT }}
+185 -2
View File
@@ -8,6 +8,189 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
### 🧹 Chores
## [9.0.7](https://github.com/expo/eas-cli/releases/tag/v9.0.7) - 2024-05-17
## [9.0.6](https://github.com/expo/eas-cli/releases/tag/v9.0.6) - 2024-05-15
## [9.0.5](https://github.com/expo/eas-cli/releases/tag/v9.0.5) - 2024-05-13
## [9.0.4](https://github.com/expo/eas-cli/releases/tag/v9.0.4) - 2024-05-13
### 🧹 Chores
- Add loader and progress information to the `diagnostics` command. ([#2378](https://github.com/expo/eas-cli/pull/2378) by [@simek](https://github.com/simek))
## [9.0.3](https://github.com/expo/eas-cli/releases/tag/v9.0.3) - 2024-05-10
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2372](https://github.com/expo/eas-cli/pull/2372) by [@expo-bot](https://github.com/expo-bot))
## [9.0.2](https://github.com/expo/eas-cli/releases/tag/v9.0.2) - 2024-05-09
## [9.0.1](https://github.com/expo/eas-cli/releases/tag/v9.0.1) - 2024-05-09
### 🧹 Chores
- Update image tags in `eas.schema.json`. ([#2363](https://github.com/expo/eas-cli/pull/2363) by [@szdziedzic](https://github.com/szdziedzic))
## [9.0.0](https://github.com/expo/eas-cli/releases/tag/v9.0.0) - 2024-05-08
### 🛠 Breaking changes
- Allow modification of provisioning profile in CI, add --freeze-credentials flag. ([#2347](https://github.com/expo/eas-cli/pull/2347) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Pass env from process.env and build profile to expo-updates CLI calls where applicable. ([#2359](https://github.com/expo/eas-cli/pull/2359) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove more classic updates code. ([#2357](https://github.com/expo/eas-cli/pull/2357) by [@wschurman](https://github.com/wschurman))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2360](https://github.com/expo/eas-cli/pull/2360) by [@expo-bot](https://github.com/expo-bot))
- Don't pass custom global `"expoCli"` version set in `eas.json` to EAS Build process and warn users when setting it. ([#2361](https://github.com/expo/eas-cli/pull/2361) by [@szdziedzic](https://github.com/szdziedzic))
## [8.0.0](https://github.com/expo/eas-cli/releases/tag/v8.0.0) - 2024-05-01
### 🛠 Breaking changes
- Stop creating a channel on `eas update` and `eas update:roll-back-to-embedded` unless the `--channel` flag is specified. ([#2346](https://github.com/expo/eas-cli/pull/2346) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Improve login info message for other login options. ([#2352](https://github.com/expo/eas-cli/pull/2352) by [@wschurman](https://github.com/wschurman))
- Show the -s, --sso option in the login command help. ([#2353](https://github.com/expo/eas-cli/pull/2353) by [@lzkb](https://github.com/lzkb))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2351](https://github.com/expo/eas-cli/pull/2351) by [@expo-bot](https://github.com/expo-bot))
- Don't prompt users to set push notifications by default if they don't have the `expo-notifications` installed. ([#2343](https://github.com/expo/eas-cli/pull/2343) by [@szdziedzic](https://github.com/szdziedzic))
## [7.8.5](https://github.com/expo/eas-cli/releases/tag/v7.8.5) - 2024-04-26
### 🐛 Bug fixes
- Add explicit workflow arg to expo-update CLI calls. ([#2340](https://github.com/expo/eas-cli/pull/2340) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Improve error message if the server returns `UNAUTHORIZED_ERROR`. ([#2345](https://github.com/expo/eas-cli/pull/2345) by [@szdziedzic](https://github.com/szdziedzic))
- Fill in min expo-updates version for expo-updates CLI. ([#2344](https://github.com/expo/eas-cli/pull/2344) by [@wschurman](https://github.com/wschurman))
## [7.8.4](https://github.com/expo/eas-cli/releases/tag/v7.8.4) - 2024-04-24
### 🧹 Chores
- Update the list of available Android images. ([#2337](https://github.com/expo/eas-cli/pull/2337) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Make multi-select for revoking distribution certificates more readable. ([#2342](https://github.com/expo/eas-cli/pull/2342) by [@szdziedzic](https://github.com/szdziedzic))
- Improve error message displayed when EAS CLI version doesn't satisfy minimal version required specified in eas.json. ([#2341](https://github.com/expo/eas-cli/pull/2341) by [@szdziedzic](https://github.com/szdziedzic))
## [7.8.3](https://github.com/expo/eas-cli/releases/tag/v7.8.3) - 2024-04-23
### 🐛 Bug fixes
- Don't prompt to set `android.package` and `ios.bundleIdentifier` values when running in non-interactive mode. ([#2336](https://github.com/expo/eas-cli/pull/2336) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Amend credential removal wording. ([#2334](https://github.com/expo/eas-cli/pull/2334) by [@quinlanj](https://github.com/quinlanj))
## [7.8.2](https://github.com/expo/eas-cli/releases/tag/v7.8.2) - 2024-04-15
### 🐛 Bug fixes
- Fix display of errors when expo-updates CLI command fails. ([#2324](https://github.com/expo/eas-cli/pull/2324) by [@wschurman](https://github.com/wschurman))
- Move credentials endpoints to paginated counterparts. ([#2327](https://github.com/expo/eas-cli/pull/2327) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Add progress bar for fetching paginated datasets. ([#2326](https://github.com/expo/eas-cli/pull/2326) by [@quinlanj](https://github.com/quinlanj))
## [7.8.1](https://github.com/expo/eas-cli/releases/tag/v7.8.1) - 2024-04-11
### 🐛 Bug fixes
- Fix command source files URLs in autogenerated `README`. ([#2318](https://github.com/expo/eas-cli/pull/2318) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2316](https://github.com/expo/eas-cli/pull/2316) by [@expo-bot](https://github.com/expo-bot))
- Stop querying `Build.resourceClass` field. ([#2320](https://github.com/expo/eas-cli/pull/2320) by [@szdziedzic](https://github.com/szdziedzic))
## [7.8.0](https://github.com/expo/eas-cli/releases/tag/v7.8.0) - 2024-04-08
### 🎉 New features
- Add `auto`, `sdk-50` and `sdk-49` image tags. ([#2298](https://github.com/expo/eas-cli/pull/2298) by [@szdziedzic](https://github.com/szdziedzic))
- Add `--build-logger-level` flag to `eas build` command. ([#2313](https://github.com/expo/eas-cli/pull/2313) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Deprecate the `default` image tag. ([#2298](https://github.com/expo/eas-cli/pull/2298) by [@szdziedzic](https://github.com/szdziedzic))
- Deprecate iOS images with Xcode version lower then 15. ([#2298](https://github.com/expo/eas-cli/pull/2298) by [@szdziedzic](https://github.com/szdziedzic))
## [7.7.0](https://github.com/expo/eas-cli/releases/tag/v7.7.0) - 2024-04-05
### 🎉 New features
- Generate metadata file for project archive ([#2149](https://github.com/expo/eas-cli/pull/2149) by [@khamilowicz](https://github.com/khamilowicz))
- Add --verbose-fastlane flag to eas submit command for more robust fastlane pilot logs. ([#2276](https://github.com/expo/eas-cli/pull/2276) by [@khamilowicz](https://github.com/khamilowicz))
- Add `eas credentials:configure-build` subcommand. ([#2282](https://github.com/expo/eas-cli/pull/2282) by [@fiberjw](https://github.com/fiberjw))
### 🧹 Chores
- Add info about the Xcode 15.3 image to `eas.schema.json`. ([#2312](https://github.com/expo/eas-cli/pull/2312) by [@szdziedzic](https://github.com/szdziedzic))
## [7.6.2](https://github.com/expo/eas-cli/releases/tag/v7.6.2) - 2024-03-27
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2301](https://github.com/expo/eas-cli/pull/2301) by [@expo-bot](https://github.com/expo-bot))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2304](https://github.com/expo/eas-cli/pull/2304) by [@expo-bot](https://github.com/expo-bot))
## [7.6.1](https://github.com/expo/eas-cli/releases/tag/v7.6.1) - 2024-03-25
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2291](https://github.com/expo/eas-cli/pull/2291) by [@expo-bot](https://github.com/expo-bot))
- Fix asset limit punctuation. ([#2296](https://github.com/expo/eas-cli/pull/2296) by [@quinlanj](https://github.com/quinlanj))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2293](https://github.com/expo/eas-cli/pull/2293) by [@expo-bot](https://github.com/expo-bot))
## [7.6.0](https://github.com/expo/eas-cli/releases/tag/v7.6.0) - 2024-03-18
### 🎉 New features
- Print uncommitted files in non-interactive mode if they fail the execution. ([#2288](https://github.com/expo/eas-cli/pull/2288) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- Use a custom build config path with POSIX separator when sending data to the EAS Build server. ([#2285](https://github.com/expo/eas-cli/pull/2285) by [@szdziedzic](https://github.com/szdziedzic))
- Improve resolving vcsClient as a part of the project context. ([#2295](https://github.com/expo/eas-cli/pull/2295) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2277](https://github.com/expo/eas-cli/pull/2277) by [@expo-bot](https://github.com/expo-bot))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2283](https://github.com/expo/eas-cli/pull/2283) by [@expo-bot](https://github.com/expo-bot))
- Bump the `@expo/apple-utils` version to switch between the `developer.apple.com` and `developer-mdn.apple.com` domains when one of them doesn't work. ([#2290](https://github.com/expo/eas-cli/pull/2290) by [@szdziedzic](https://github.com/szdziedzic))
## [7.5.0](https://github.com/expo/eas-cli/releases/tag/v7.5.0) - 2024-03-11
### 🎉 New features
- Add `--auto-submit` option to `eas build:internal` command. ([#2271](https://github.com/expo/eas-cli/pull/2271) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2274](https://github.com/expo/eas-cli/pull/2274) by [@expo-bot](https://github.com/expo-bot))
## [7.4.0](https://github.com/expo/eas-cli/releases/tag/v7.4.0) - 2024-03-10
### 🎉 New features
- Use new expo-updates configuration:syncnative for versioned native sync. ([#2269](https://github.com/expo/eas-cli/pull/2269) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
@@ -15,14 +198,14 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix expo-updates package version detection for canaries. ([#2243](https://github.com/expo/eas-cli/pull/2243) by [@wschurman](https://github.com/wschurman))
- Add missing `config` property to `eas.json` schema. ([#2248](https://github.com/expo/eas-cli/pull/2248) by [@sjchmiela](https://github.com/sjchmiela))
- Use expo-updates runtime version CLI to generate runtime versions. ([#2251](https://github.com/expo/eas-cli/pull/2251) by [@wschurman](https://github.com/wschurman))
- Update @expo/apple-utils to handle changes in API
- Update @expo/apple-utils to handle changes in API. ([73ba19de6662763cc6bff9fac6b7700ffbd0e88a](https://github.com/expo/eas-cli/commit/73ba19de6662763cc6bff9fac6b7700ffbd0e88a) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2237](https://github.com/expo/eas-cli/pull/2237) by [@expo-bot](https://github.com/expo-bot))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2240](https://github.com/expo/eas-cli/pull/2240) by [@expo-bot](https://github.com/expo-bot))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2253](https://github.com/expo/eas-cli/pull/2253) by [@expo-bot](https://github.com/expo-bot))
- Include src/**/build directories in vscode search and replace. ([#2250](https://github.com/expo/eas-cli/pull/2250) by [@wschurman](https://github.com/wschurman))
- Include src/\*\*/build directories in vscode search and replace. ([#2250](https://github.com/expo/eas-cli/pull/2250) by [@wschurman](https://github.com/wschurman))
- Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2259](https://github.com/expo/eas-cli/pull/2259) by [@expo-bot](https://github.com/expo-bot))
## [7.3.0](https://github.com/expo/eas-cli/releases/tag/v7.3.0) - 2024-02-19
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "7.4.0",
"version": "9.0.7",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+111 -77
View File
@@ -85,6 +85,7 @@ eas --help COMMAND
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas credentials:configure-build`](#eas-credentialsconfigure-build)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:delete`](#eas-devicedelete)
* [`eas device:list`](#eas-devicelist)
@@ -127,7 +128,10 @@ log in with your Expo account
```
USAGE
$ eas account:login
$ eas account:login [-s]
FLAGS
-s, --sso Login with SSO
DESCRIPTION
log in with your Expo account
@@ -136,7 +140,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -153,7 +157,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -170,7 +174,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -184,7 +188,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -215,7 +219,7 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v2.3.10/src/commands/autocomplete/index.ts)_
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v2.3.10/packages/eas-cli/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
@@ -236,7 +240,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -257,7 +261,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -277,7 +281,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -297,7 +301,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -320,7 +324,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -329,28 +333,35 @@ start a build
```
USAGE
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache]
[--auto-submit | --auto-submit-with-profile <value>] [-m <value>] [--json --non-interactive]
[--auto-submit | --auto-submit-with-profile <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
-m, --message=<value> A short message describing the build
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to
"production" if defined in eas.json.
-m, --message=<value> A short message describing the build
-p, --platform=(android|ios|all)
--auto-submit Submit on build complete using the submit profile with the same name as the
build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
--clear-cache Clear cache before the build
--json Enable JSON output, non-JSON messages will be printed to stderr.
--local Run build locally [experimental]
--non-interactive Run the command in non-interactive mode.
--output=<value> Output path for local build
--[no-]wait Wait for build(s) to complete
--auto-submit Submit on build complete using the submit profile with the
same name as the build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with
provided name
--build-logger-level=(trace|debug|info|warn|error|fatal) The level of logs to output during the build process.
Defaults to "info".
--clear-cache Clear cache before the build
--freeze-credentials Prevent the build from updating credentials in
non-interactive mode
--json Enable JSON output, non-JSON messages will be printed to
stderr.
--local Run build locally [experimental]
--non-interactive Run the command in non-interactive mode.
--output=<value> Output path for local build
--[no-]wait Wait for build(s) to complete
DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -369,7 +380,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -386,7 +397,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -405,7 +416,7 @@ DESCRIPTION
delete a build
```
_See code: [src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:inspect`
@@ -440,7 +451,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/v7.4.0/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -481,7 +492,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -507,7 +518,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -533,7 +544,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -542,7 +553,7 @@ submit app binary to App Store and/or Play Store
```
USAGE
$ eas build:submit [-p android|ios|all] [-e <value>] [--latest | --id <value> | --path <value> | --url <value>]
[--verbose] [--wait] [--non-interactive]
[--verbose] [--wait] [--verbose-fastlane] [--non-interactive]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
@@ -554,6 +565,7 @@ FLAGS
--path=<value> Path to the .apk/.aab/.ipa file
--url=<value> App archive url
--verbose Always print logs from EAS Submit
--verbose-fastlane Enable verbose logging for the submission process
--[no-]wait Wait for submission to complete
DESCRIPTION
@@ -582,7 +594,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -601,7 +613,7 @@ DESCRIPTION
update version of an app
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -620,7 +632,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/v7.4.0/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -637,7 +649,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -658,7 +670,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -680,7 +692,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -700,7 +712,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -732,7 +744,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -755,7 +767,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -776,7 +788,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -793,7 +805,25 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/credentials.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
Set up credentials for building your project.
```
USAGE
$ eas credentials:configure-build [-p android|ios] [-e <value>]
FLAGS
-e, --profile=PROFILE_NAME The name of the build profile in eas.json.
-p, --platform=(android|ios)
DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas device:create`
@@ -807,7 +837,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/v7.4.0/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -827,7 +857,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -848,7 +878,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -869,7 +899,7 @@ DESCRIPTION
rename a registered device
```
_See code: [src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -883,7 +913,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -897,7 +927,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -917,7 +947,7 @@ DESCRIPTION
Display help for eas.
```
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.23/src/commands/help.ts)_
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.23/packages/eas-cli/src/commands/help.ts)_
## `eas init`
@@ -945,7 +975,10 @@ log in with your Expo account
```
USAGE
$ eas login
$ eas login [-s]
FLAGS
-s, --sso Login with SSO
DESCRIPTION
log in with your Expo account
@@ -985,7 +1018,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1002,7 +1035,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/v7.4.0/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1019,7 +1052,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/v7.4.0/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -1033,7 +1066,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1047,7 +1080,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1069,7 +1102,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -1092,7 +1125,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/v7.4.0/src/commands/secret/create.ts)_
_See code: [packages/eas-cli/src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1110,7 +1143,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/secret/delete.ts)_
_See code: [packages/eas-cli/src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1124,7 +1157,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/secret/list.ts)_
_See code: [packages/eas-cli/src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1144,7 +1177,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/secret/push.ts)_
_See code: [packages/eas-cli/src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1153,7 +1186,7 @@ submit app binary to App Store and/or Play Store
```
USAGE
$ eas submit [-p android|ios|all] [-e <value>] [--latest | --id <value> | --path <value> | --url <value>]
[--verbose] [--wait] [--non-interactive]
[--verbose] [--wait] [--verbose-fastlane] [--non-interactive]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
@@ -1165,6 +1198,7 @@ FLAGS
--path=<value> Path to the .apk/.aab/.ipa file
--url=<value> App archive url
--verbose Always print logs from EAS Submit
--verbose-fastlane Enable verbose logging for the submission process
--[no-]wait Wait for submission to complete
DESCRIPTION
@@ -1174,7 +1208,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1207,7 +1241,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1225,7 +1259,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1246,7 +1280,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1268,7 +1302,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1295,7 +1329,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1322,7 +1356,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1341,7 +1375,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1361,7 +1395,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1382,7 +1416,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1402,7 +1436,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1420,7 +1454,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1442,7 +1476,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1459,7 +1493,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v7.4.0/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v9.0.7/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+4427 -1248
View File
File diff suppressed because it is too large. Load diff
+8 -7
View File
@@ -1,21 +1,22 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "7.4.0",
"version": "9.0.7",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "1.5.0",
"@expo/apple-utils": "1.7.0",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "8.5.4",
"@expo/config-plugins": "7.8.4",
"@expo/config-types": "50.0.0",
"@expo/eas-build-job": "1.0.78",
"@expo/eas-json": "7.4.0",
"@expo/eas-build-job": "1.0.108",
"@expo/eas-json": "9.0.4",
"@expo/json-file": "8.2.37",
"@expo/logger": "1.0.57",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "1.1.2",
@@ -27,7 +28,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.81",
"@expo/steps": "1.0.108",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -76,7 +77,7 @@
"resolve-from": "5.0.0",
"semver": "7.5.4",
"slash": "3.0.0",
"tar": "6.2.0",
"tar": "6.2.1",
"terminal-link": "2.1.1",
"tslib": "2.6.2",
"turndown": "7.1.2",
@@ -109,7 +110,7 @@
"@types/wrap-ansi": "3.0.0",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"express": "4.18.2",
"express": "4.19.2",
"memfs": "3.4.13",
"mockdate": "3.0.5",
"nock": "13.4.0",
+1 -2
View File
@@ -3,8 +3,7 @@ const fs = require('fs/promises');
(async () => {
// Patch `oclif readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
readmeContent = readmeContent.replace(/src\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent);
// eslint-disable-next-line no-console
@@ -24,6 +24,7 @@ export function getMockAppFragment(): AppFragment {
return {
id: mockProjectId,
slug: 'testapp',
name: 'testapp',
fullName: '@testuser/testpp',
ownerAccount: {
id: 'test-account-id',
@@ -3,13 +3,7 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -91,7 +85,6 @@ function createMockBuildFragment({
},
},
status: BuildStatus.InQueue,
resourceClass: BuildResourceClass.AndroidMedium,
isForIosSimulator: false,
};
}
@@ -3,13 +3,7 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToDeleteAsync } from '../../commands/build/delete';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -91,7 +85,6 @@ function createMockBuildFragment({
},
},
status: BuildStatus.InQueue,
resourceClass: BuildResourceClass.AndroidMedium,
isForIosSimulator: false,
};
}
@@ -8,7 +8,7 @@ import os from 'os';
import path from 'path';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import { getVcsClient } from '../../../vcs';
import { resolveVcsClient } from '../../../vcs';
import {
BumpStrategy,
bumpVersionAsync,
@@ -19,7 +19,7 @@ import {
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
const vcsClient = getVcsClient();
const vcsClient = resolveVcsClient();
afterAll(async () => {
// do not remove the following line
+3 -2
View File
@@ -1,4 +1,4 @@
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -107,12 +107,13 @@ export async function prepareAndroidBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
): Promise<Job> => {
): Promise<Android.Job> => {
return await prepareJobAsync(ctx, jobData);
},
sendBuildRequestAsync: async (
@@ -2,6 +2,7 @@ import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
@@ -14,7 +15,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
@@ -29,6 +29,9 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
};
}
@@ -3,16 +3,15 @@ import {
ArchiveSource,
BuildMode,
BuildTrigger,
Job,
Platform,
sanitizeJob,
sanitizeBuildJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { BuildContext } from '../context';
@@ -29,7 +28,7 @@ const cacheDefaults = {
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
): Promise<Android.Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
@@ -54,7 +53,7 @@ export async function prepareJobAsync(
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
? getCustomBuildConfigPathForJob(buildProfile.config)
: undefined;
const job: Android.Job = {
@@ -69,7 +68,6 @@ export async function prepareJobAsync(
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
@@ -80,7 +78,6 @@ export async function prepareJobAsync(
secrets: {
...buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
@@ -96,14 +93,20 @@ export async function prepareJobAsync(
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
mode: buildProfile.config || ctx.repack ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
};
return sanitizeJob(job);
return sanitizeBuildJob(job) as Android.Job;
}
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -18,11 +18,13 @@ export async function syncProjectConfigurationAsync({
exp,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
@@ -30,7 +32,7 @@ export async function syncProjectConfigurationAsync({
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env });
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
+73 -11
View File
@@ -1,4 +1,10 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import {
ArchiveSource,
ArchiveSourceType,
BuildJob,
Metadata,
Platform,
} from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
@@ -26,7 +32,12 @@ import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
import {
LocalFile,
makeProjectMetadataFileAsync,
makeProjectTarballAsync,
reviewAndCommitChangesAsync,
} from './utils/repository';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
@@ -65,14 +76,14 @@ export interface JobData<Credentials> {
projectArchive: ArchiveSource;
}
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob> {
ctx: BuildContext<TPlatform>;
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<BuildJob>;
sendBuildRequestAsync(
appId: string,
job: TJob,
@@ -97,7 +108,7 @@ function resolveBuildParamsInput<T extends Platform>(
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends Job,
TJob extends BuildJob,
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
@@ -133,9 +144,10 @@ export async function prepareBuildRequestForPlatformAsync<
let projectArchive: ArchiveSource | undefined;
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
const projectPath = (await makeProjectTarballAsync(ctx.vcsClient)).path;
projectArchive = {
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync(ctx.vcsClient)).path,
path: projectPath,
};
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
projectArchive = {
@@ -145,7 +157,7 @@ export async function prepareBuildRequestForPlatformAsync<
} else if (!ctx.localBuildOptions.localBuildMode) {
projectArchive = {
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
...(await uploadProjectAsync(ctx)),
};
}
assert(projectArchive);
@@ -231,7 +243,10 @@ export function handleBuildRequestError(error: any, platform: Platform): never {
async function uploadProjectAsync<TPlatform extends Platform>(
ctx: BuildContext<TPlatform>
): Promise<string> {
): Promise<{
bucketKey: string;
metadataLocation?: string;
}> {
let projectTarballPath;
try {
return await withAnalyticsAsync(
@@ -260,7 +275,6 @@ async function uploadProjectAsync<TPlatform extends Platform>(
}
projectTarballPath = projectTarball.path;
const bucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectSources,
@@ -274,7 +288,11 @@ async function uploadProjectAsync<TPlatform extends Platform>(
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
})
);
return bucketKey;
const { metadataLocation } = await uploadMetadataFileAsync<TPlatform>(projectTarball, ctx);
if (metadataLocation) {
return { bucketKey, metadataLocation };
}
return { bucketKey };
},
{
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
@@ -285,9 +303,11 @@ async function uploadProjectAsync<TPlatform extends Platform>(
);
} catch (err: any) {
let errMessage = 'Failed to upload the project tarball to EAS Build';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
throw new EasBuildProjectArchiveUploadError(errMessage);
} finally {
if (projectTarballPath) {
@@ -296,7 +316,49 @@ async function uploadProjectAsync<TPlatform extends Platform>(
}
}
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
async function uploadMetadataFileAsync<TPlatform extends Platform>(
projectTarball: LocalFile,
ctx: BuildContext<TPlatform>
): Promise<{ metadataLocation: string | null }> {
let projectMetadataFile: LocalFile | null = null;
try {
projectMetadataFile = await makeProjectMetadataFileAsync(projectTarball.path);
const metadataLocation = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectMetadata,
projectMetadataFile.path,
createProgressTracker({
total: projectMetadataFile.size,
message: ratio =>
`Uploading metadata to EAS Build (${formatBytes(
projectMetadataFile!.size * ratio
)} / ${formatBytes(projectMetadataFile!.size)})`,
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
})
);
return { metadataLocation };
} catch (err: any) {
let errMessage = 'Failed to upload metadata to EAS Build';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return { metadataLocation: null };
} finally {
if (projectMetadataFile) {
await fs.remove(projectMetadataFile.path);
}
}
}
async function sendBuildRequestAsync<
TPlatform extends Platform,
Credentials,
TJob extends BuildJob,
>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata,
+1 -1
View File
@@ -40,7 +40,7 @@ async function configureAsync({
nonInteractive,
vcsClient,
}: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync(vcsClient);
await maybeBailOnRepoStatusAsync(vcsClient, nonInteractive);
await createEasJsonAsync(projectDir, vcsClient);
+3
View File
@@ -1,6 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { NodePackageManager } from '@expo/package-manager';
import { LocalBuildOptions } from './local';
@@ -60,4 +61,6 @@ export interface BuildContext<T extends Platform> {
developmentClient: boolean;
requiredPackageManager: NodePackageManager['name'] | null;
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
}
+28 -1
View File
@@ -1,6 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import { LoggerLevel } from '@expo/logger';
import { resolvePackageManager } from '@expo/package-manager';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';
@@ -15,6 +16,9 @@ import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/An
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
import { AppStoreApiKeyPurpose } from '../credentials/ios/actions/AscApiKeyUtils';
import { getAppFromContextAsync } from '../credentials/ios/actions/BuildCredentialsUtils';
import { GetAscApiKey } from '../credentials/ios/actions/GetAscApiKey';
import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
@@ -39,6 +43,9 @@ export async function createBuildContextAsync<T extends Platform>({
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
repack,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -57,6 +64,9 @@ export async function createBuildContextAsync<T extends Platform>({
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
@@ -83,6 +93,7 @@ export async function createBuildContextAsync<T extends Platform>({
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
});
const devClientProperties = getDevClientEventProperties({
@@ -136,6 +147,8 @@ export async function createBuildContextAsync<T extends Platform>({
customBuildConfigMetadata,
developmentClient,
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -145,10 +158,24 @@ export async function createBuildContextAsync<T extends Platform>({
} as BuildContext<T>;
} else {
const common = commonContext as CommonContext<Platform.IOS>;
return {
const iosBuildCtx = {
...common,
ios: await createIosContextAsync(common),
} as BuildContext<T>;
const appLookupParams = {
...(await getAppFromContextAsync(credentialsCtx)),
bundleIdentifier: (iosBuildCtx as BuildContext<Platform.IOS>).ios.bundleIdentifier,
};
const getAscApiKeyAction = new GetAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.BUILD_SERVICE
);
const ascApiKeyManagedByExpo = await getAscApiKeyAction.runAsync(credentialsCtx);
if (ascApiKeyManagedByExpo) {
// TODO: set credentialCtx with the correct options
}
return iosBuildCtx;
}
}
+12
View File
@@ -6,6 +6,7 @@ import {
Metadata,
Workflow,
} from '@expo/eas-build-job';
import { LoggerLevel } from '@expo/logger';
import assert from 'assert';
import {
@@ -18,6 +19,7 @@ import {
BuildTrigger as GraphQLBuildTrigger,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
WorkerLoggerLevel,
} from '../graphql/generated';
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
@@ -25,6 +27,7 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
return {
type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey,
metadataLocation: archiveSource.metadataLocation,
};
} else if (archiveSource.type === ArchiveSourceType.URL) {
return {
@@ -107,3 +110,12 @@ export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildT
}
throw new Error('Unknown build trigger');
}
export const loggerLevelToGraphQLWorkerLoggerLevel: Record<LoggerLevel, WorkerLoggerLevel> = {
[LoggerLevel.TRACE]: WorkerLoggerLevel.Trace,
[LoggerLevel.DEBUG]: WorkerLoggerLevel.Debug,
[LoggerLevel.INFO]: WorkerLoggerLevel.Info,
[LoggerLevel.WARN]: WorkerLoggerLevel.Warn,
[LoggerLevel.ERROR]: WorkerLoggerLevel.Error,
[LoggerLevel.FATAL]: WorkerLoggerLevel.Fatal,
};
@@ -6,7 +6,7 @@ import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { readPlistAsync } from '../../../utils/plist';
import { getVcsClient } from '../../../vcs';
import { resolveVcsClient } from '../../../vcs';
import {
BumpStrategy,
bumpVersionAsync,
@@ -19,7 +19,7 @@ import {
jest.mock('fs');
const vcsClient = getVcsClient();
const vcsClient = resolveVcsClient();
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
IOSConfig.Target,
+3 -2
View File
@@ -1,4 +1,4 @@
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { Ios, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import { ensureIosCredentialsAsync } from './credentials';
@@ -89,12 +89,13 @@ export async function prepareIosBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Job> => {
): Promise<Ios.Job> => {
return await prepareJobAsync(ctx, {
...jobData,
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
+4 -1
View File
@@ -3,6 +3,7 @@ import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
@@ -15,7 +16,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
builderEnvironment: job.builderEnvironment,
@@ -31,6 +31,9 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
};
}
+12 -9
View File
@@ -3,9 +3,8 @@ import {
BuildMode,
BuildTrigger,
Ios,
Job,
Platform,
sanitizeJob,
sanitizeBuildJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import nullthrows from 'nullthrows';
@@ -14,7 +13,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { BuildContext } from '../context';
@@ -32,7 +31,7 @@ const cacheDefaults = {
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
): Promise<Ios.Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
@@ -46,7 +45,7 @@ export async function prepareJobAsync(
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
? getCustomBuildConfigPathForJob(buildProfile.config)
: undefined;
const job: Ios.Job = {
@@ -63,7 +62,6 @@ export async function prepareJobAsync(
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
@@ -74,7 +72,6 @@ export async function prepareJobAsync(
secrets: {
buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator,
@@ -91,15 +88,21 @@ export async function prepareJobAsync(
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
mode: buildProfile.config || ctx.repack ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
};
return sanitizeJob(job);
return sanitizeBuildJob(job) as Ios.Job;
}
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { IosVersionAutoIncrement } from '@expo/eas-json';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
@@ -15,19 +15,21 @@ export async function syncProjectConfigurationAsync({
targets,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(vcsClient, projectDir, exp);
await syncUpdatesConfigurationAsync({ vcsClient, projectDir, exp, workflow, env });
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else {
+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 = '1.0.71';
const PLUGIN_PACKAGE_VERSION = '1.0.112';
export enum LocalBuildMode {
/**
+17 -43
View File
@@ -9,26 +9,16 @@ import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios
import { LocalBuildMode } from './local';
import { BuildDistributionType } from './types';
import Log from '../log';
import {
getUsername,
isClassicUpdatesSupportedAsync,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from '../update/android/UpdatesModule';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from '../update/ios/UpdatesModule';
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule';
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule';
import { easCliVersion } from '../utils/easCli';
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const channelObject = await resolveChannelAsync(ctx);
const distribution = ctx.buildProfile.distribution ?? BuildDistributionType.STORE;
const metadata: Metadata = {
trackingContext: ctx.analyticsEventProperties,
@@ -37,9 +27,17 @@ export async function collectMetadataAsync<T extends Platform>(
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: (await resolveRuntimeVersionAsync(ctx)) ?? undefined,
runtimeVersion:
(await resolveRuntimeVersionAsync({
exp: ctx.exp,
platform: ctx.platform,
workflow: ctx.workflow,
projectDir: ctx.projectDir,
env: ctx.buildProfile.env,
cwd: ctx.projectDir,
})) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelOrReleaseChannel,
...channelObject,
distribution,
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
@@ -117,9 +115,9 @@ function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string
}
}
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
async function resolveChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | { releaseChannel: string } | null> {
): Promise<{ channel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
@@ -131,31 +129,7 @@ async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
return { channel };
}
if (!(await isClassicUpdatesSupportedAsync(ctx.projectDir))) {
return null;
}
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
return { releaseChannel };
}
async function getNativeReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
}
return null;
}
async function getNativeChannelAsync<T extends Platform>(
+81 -19
View File
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
BuildProfile,
@@ -9,6 +9,7 @@ import {
ResourceClass,
SubmitProfile,
} from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -19,7 +20,7 @@ import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildOptions } from './local';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
@@ -70,7 +71,6 @@ import {
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
@@ -93,6 +93,9 @@ export interface BuildFlags {
localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
}
export async function runBuildAndSubmitAsync(
@@ -103,7 +106,9 @@ export async function runBuildAndSubmitAsync(
flags: BuildFlags,
actor: Actor,
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
): Promise<void> {
): Promise<{
buildIds: string[];
}> {
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
@@ -132,6 +137,36 @@ export async function runBuildAndSubmitAsync(
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
for (const buildProfile of buildProfiles) {
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
Log.warn(
`The "image" field in the build profile "${buildProfile.profileName}" is set to "${buildProfile.profile.image}". This tag is deprecated and will be removed in the future. Use other images or tags listed here: https://docs.expo.dev/build-reference/infrastructure/`
);
}
if (
buildProfile.profile.image &&
[
'macos-monterey-12.4-xcode-13.4',
'macos-monterey-12.3-xcode-13.3',
'macos-monterey-12.1-xcode-13.2',
'macos-ventura-13.4-xcode-14.3.1',
'macos-ventura-13.3-xcode-14.3',
'macos-monterey-12.6-xcode-14.2',
'macos-monterey-12.6-xcode-14.1',
'macos-monterey-12.6-xcode-14.0',
].includes(buildProfile.profile.image)
) {
Log.warn(
`The "image" field in the build profile "${buildProfile.profileName}" is set to "${
buildProfile.profile.image
}". This image is using Xcode in version lower then 15. Apple's new requirement states that starting on April 29th, 2024, apps uploaded to App Store Connect must be built with Xcode 15 for iOS 17, iPadOS 17, tvOS 17, or watchOS 10. Start using Xcode 15 image today by setting "macos-sonoma-14.4-xcode-15.3" or "latest" as your image value. This image will be removed from EAS Build on May 29th, 2024. ${learnMore(
'https://expo.dev/changelog/2024/03-29-eas-build-upcoming-ios-images-updates'
)}`
);
}
}
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
@@ -180,13 +215,25 @@ export async function runBuildAndSubmitAsync(
buildCtxByPlatform[platform] = buildCtx;
}
if (flags.localBuildOptions.localBuildMode) {
return;
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
const startedBuild = await BuildQuery.byIdAsync(
graphqlClient,
nullthrows(process.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not defined')
);
startedBuilds.push({ build: startedBuild, buildProfile: buildProfiles[0] });
}
if (!flags.localBuildOptions.localBuildMode) {
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
}
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
@@ -219,16 +266,26 @@ export async function runBuildAndSubmitAsync(
submissions.push(submission);
}
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
if (!flags.localBuildOptions.localBuildMode) {
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
}
if (flags.localBuildOptions.localBuildMode) {
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
}
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
}
return;
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
}
const { accountName } = Object.values(buildCtxByPlatform)[0];
@@ -269,6 +326,9 @@ export async function runBuildAndSubmitAsync(
}
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
}
async function prepareAndStartBuildAsync({
@@ -314,6 +374,9 @@ async function prepareAndStartBuildAsync({
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel,
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
});
if (moreBuilds) {
@@ -326,12 +389,6 @@ async function prepareAndStartBuildAsync({
);
}
await validateBuildProfileConfigMatchesProjectConfigAsync(
buildCtx.exp,
buildProfile,
buildCtx.projectId,
flags.nonInteractive
);
if (buildProfile.profile.channel) {
await validateExpoUpdatesInstalledAsProjectDependencyAsync({
exp: buildCtx.exp,
@@ -341,6 +398,7 @@ async function prepareAndStartBuildAsync({
sdkVersion: buildCtx.exp.sdkVersion,
nonInteractive: flags.nonInteractive,
buildProfile,
env: buildProfile.profile.env,
});
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
@@ -421,6 +479,7 @@ async function prepareAndStartSubmissionAsync({
projectId: buildCtx.projectId,
exp: buildCtx.exp,
vcsClient: buildCtx.vcsClient,
isVerboseFastlaneEnabled: false,
});
if (moreBuilds) {
@@ -484,6 +543,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
buildProfile,
nonInteractive,
sdkVersion,
env,
}: {
exp: ExpoConfig;
projectId: string;
@@ -492,6 +552,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
buildProfile: ProfileData<'build'>;
nonInteractive: boolean;
sdkVersion?: string;
env: Env | undefined;
}): Promise<void> {
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
return;
@@ -519,6 +580,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
projectDir,
platform: RequestedPlatform.All,
vcsClient,
env,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
@@ -51,10 +51,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
value: build.releaseChannel,
},
{
label: 'Channel',
value: build.channel,
+61 -4
View File
@@ -12,7 +12,10 @@ import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { Client } from '../../vcs/vcs';
export async function maybeBailOnRepoStatusAsync(vcsClient: Client): Promise<void> {
export async function maybeBailOnRepoStatusAsync(
vcsClient: Client,
nonInteractive: boolean
): Promise<void> {
if (!(await vcsClient.isCommitRequiredAsync())) {
return;
}
@@ -28,6 +31,11 @@ export async function maybeBailOnRepoStatusAsync(vcsClient: Client): Promise<voi
});
if (!answer) {
if (nonInteractive) {
Log.log('The following files need to be committed:');
await vcsClient.showChangedFilesAsync();
}
throw new Error('Commit all changes. Aborting...');
}
}
@@ -47,6 +55,9 @@ export async function ensureRepoIsCleanAsync(
)}.`
);
if (nonInteractive) {
Log.log('The following files need to be committed:');
await vcsClient.showChangedFilesAsync();
throw new Error('Commit all changes. Aborting...');
}
const answer = await confirmAsync({
@@ -83,9 +94,55 @@ export async function commitPromptAsync(
});
}
export async function makeProjectTarballAsync(
vcsClient: Client
): Promise<{ path: string; size: number }> {
export type LocalFile = {
path: string;
size: number;
};
export async function makeProjectMetadataFileAsync(archivePath: string): Promise<LocalFile> {
const spinner = ora('Creating project metadata file');
const timerLabel = 'makeProjectMetadataFileAsync';
const timer = setTimeout(
() => {
spinner.start();
},
Log.isDebug ? 1 : 1000
);
startTimer(timerLabel);
const metadataLocation = path.join(getTmpDirectory(), `${uuidv4()}-eas-build-metadata.json`);
const archiveContent: string[] = [];
try {
await tar.list({
file: archivePath,
onentry: (entry: tar.ReadEntry) => {
if (entry.type === 'File' && !entry.path.includes('.git/')) {
archiveContent.push(entry.path);
}
},
});
await fs.writeJSON(metadataLocation, {
archiveContent,
});
} catch (e) {
clearTimeout(timer);
if (spinner.isSpinning) {
spinner.fail();
}
throw e;
}
clearTimeout(timer);
const duration = endTimer(timerLabel);
const prettyTime = formatMilliseconds(duration);
spinner.succeed(`Created project metadata file ${chalk.dim(prettyTime)}`);
return { path: metadataLocation, size: await fs.stat(metadataLocation).then(stat => stat.size) };
}
export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalFile> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
@@ -1,5 +1,6 @@
import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
@@ -24,6 +25,7 @@ import {
} from '../analytics/AnalyticsManager';
import Log from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
export type ContextInput<
T extends {
@@ -139,7 +141,7 @@ export default abstract class EasCommand extends Command {
} = object,
>(
commandClass: { contextDefinition: ContextInput<C> },
{ nonInteractive }: { nonInteractive: boolean }
{ nonInteractive, vcsClientOverride }: { nonInteractive: boolean; vcsClientOverride?: Client }
): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition;
@@ -152,6 +154,7 @@ export default abstract class EasCommand extends Command {
nonInteractive,
sessionManager: this.sessionManager,
analytics: this.analytics,
vcsClientOverride,
}),
]);
}
@@ -208,7 +211,13 @@ export default abstract class EasCommand extends Command {
const defaultMsg = `${messageLine}${requestIdLine}`;
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
return `You don't have the required permissions to perform this operation.\n\n${defaultMsg}`;
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nThis can sometimes happen if you are logged in as incorrect user.\nRun ${chalk.bold(
'eas whoami'
)} to check the username you are logged in as.\nRun ${chalk.bold(
'eas login'
)} to change the account.\n\nOriginal error message: ${defaultMsg}`;
}
return defaultMsg;
@@ -1,10 +1,12 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import SessionManager from '../../user/SessionManager';
import { Client } from '../../vcs/vcs';
export interface ContextOptions {
sessionManager: SessionManager;
analytics: Analytics;
nonInteractive: boolean;
vcsClientOverride?: Client;
}
export default abstract class ContextField<T> {
@@ -1,9 +1,18 @@
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import ContextField from './ContextField';
import { getVcsClient } from '../../vcs';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { resolveVcsClient } from '../../vcs';
import { Client } from '../../vcs/vcs';
export default class VcsClientContextField extends ContextField<Client> {
async getValueAsync(): Promise<Client> {
return getVcsClient();
async getValueAsync({ vcsClientOverride }: { vcsClientOverride?: Client }): Promise<Client> {
if (vcsClientOverride) {
return vcsClientOverride;
}
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
return resolveVcsClient(config?.requireCommit);
}
}
@@ -55,6 +55,7 @@ describe(getProjectIdAsync, () => {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
@@ -79,6 +80,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
@@ -99,6 +101,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
@@ -123,6 +126,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
@@ -171,6 +175,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
@@ -195,6 +200,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
@@ -264,6 +270,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
@@ -8,20 +8,20 @@ import semver from 'semver';
import { learnMore } from '../../../log';
import { easCliVersion } from '../../../utils/easCli';
import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
import { resolveVcsClient } from '../../../vcs';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json (${
config.version
}).\n\nThis error probably means that you need update your eas-cli to a newer version.\nRun ${chalk.bold(
'npm install -g eas-cli'
)} to update the eas-cli to the latest version.`
);
}
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
}
async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
@@ -92,7 +92,7 @@ export async function findProjectRootAsync({
} else {
let vcsRoot;
try {
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
vcsRoot = path.normalize(await resolveVcsClient().getRootPathAsync());
} catch {}
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
throw new Error(
@@ -3,7 +3,7 @@ import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import { createGraphqlClient } from './createGraphqlClient';
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
@@ -87,6 +87,21 @@ export async function getProjectIdAsync(
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const projectId = await validateOrSetProjectIdAsync({ exp, graphqlClient, actor, options });
return projectId;
}
export async function validateOrSetProjectIdAsync({
exp,
graphqlClient,
actor,
options,
}: {
exp: ExpoConfig;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options: { env?: Env; nonInteractive: boolean };
}): Promise<string> {
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
if (typeof localProjectId !== 'string') {
@@ -13,7 +13,6 @@ export default class AccountLogin extends EasCommand {
description: 'Login with SSO',
char: 's',
default: false,
hidden: true,
}),
};
@@ -74,14 +74,20 @@ export default class BuildConfigure extends EasCommand {
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
await syncAndroidUpdatesConfigurationAsync({ projectDir, exp, workflow, env: undefined });
}
}
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(vcsClient, projectDir, exp);
await syncIosUpdatesConfigurationAsync({
vcsClient,
projectDir,
exp,
workflow,
env: undefined,
});
}
}
}
@@ -1,5 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import figures from 'figures';
@@ -33,6 +34,9 @@ interface RawBuildFlags {
'auto-submit-with-profile'?: string;
'resource-class'?: ResourceClass;
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
repack: boolean;
}
export default class Build extends EasCommand {
@@ -98,6 +102,19 @@ export default class Build extends EasCommand {
char: 'm',
description: 'A short message describing the build',
}),
'build-logger-level': Flags.enum({
description: 'The level of logs to output during the build process. Defaults to "info".',
options: Object.values(LoggerLevel),
}),
'freeze-credentials': Flags.boolean({
default: false,
description: 'Prevent the build from updating credentials in non-interactive mode',
}),
repack: Flags.boolean({
default: false,
hidden: true,
description: 'Use the golden dev client build repack flow as it works for onboarding',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -210,6 +227,9 @@ export default class Build extends EasCommand {
submitProfile: flags['auto-submit-with-profile'] ?? profile,
resourceClass: flags['resource-class'],
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
repack: flags.repack,
};
}
@@ -105,6 +105,7 @@ export default class BuildInspect extends EasCommand {
projectDir,
{
nonInteractive: false,
freezeCredentials: false,
wait: true,
clearCache: false,
json: false,
@@ -119,6 +120,7 @@ export default class BuildInspect extends EasCommand {
workingdir: tmpWorkingdir,
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
},
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync
@@ -6,7 +6,6 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import { RequestedPlatform } from '../../platform';
import { enableJsonOutput } from '../../utils/json';
import { setVcsClient } from '../../vcs';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
/**
@@ -30,6 +29,17 @@ export default class BuildInternal extends EasCommand {
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
'auto-submit': Flags.boolean({
default: false,
description:
'Submit on build complete using the submit profile with the same name as the build profile',
exclusive: ['auto-submit-with-profile'],
}),
'auto-submit-with-profile': Flags.string({
description: 'Submit on build complete using the submit profile with provided name',
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
};
static override contextDefinition = {
@@ -53,10 +63,9 @@ export default class BuildInternal extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
vcsClientOverride: new GitNoCommitClient(),
});
setVcsClient(new GitNoCommitClient());
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(
@@ -68,13 +77,16 @@ export default class BuildInternal extends EasCommand {
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
freezeCredentials: false,
wait: false,
clearCache: false,
json: true,
autoSubmit: false,
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
localBuildOptions: {
localBuildMode: LocalBuildMode.INTERNAL,
},
submitProfile: flags['auto-submit-with-profile'] ?? flags.profile,
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync
@@ -88,6 +88,7 @@ export default class BuildVersionGetView extends EasCommand {
buildProfile: profile,
platform,
vcsClient,
nonInteractive: flags['non-interactive'],
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -77,6 +77,7 @@ export default class BuildVersionSetView extends EasCommand {
buildProfile: profile,
platform,
vcsClient,
nonInteractive: false,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -102,6 +102,7 @@ export default class BuildVersionSyncView extends EasCommand {
buildProfile: profileInfo.profile,
platform: profileInfo.platform,
vcsClient,
nonInteractive: false,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -125,6 +125,7 @@ export default class ChannelRollout extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.Vcs,
...this.ContextOptions.LoggedIn,
};
@@ -133,6 +134,7 @@ export default class ChannelRollout extends EasCommand {
const argsAndFlags = this.sanitizeArgsAndFlags({ ...flags, ...args });
const {
privateProjectConfig: { exp, projectId, projectDir },
vcsClient,
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelRollout, {
nonInteractive: argsAndFlags.nonInteractive,
@@ -146,6 +148,7 @@ export default class ChannelRollout extends EasCommand {
nonInteractive: argsAndFlags.nonInteractive,
graphqlClient,
app,
vcsClient,
};
if (argsAndFlags.nonInteractive) {
await new NonInteractiveRollout(argsAndFlags).runAsync(ctx);
@@ -0,0 +1,65 @@
import { Platform } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { SelectBuildProfileFromEasJson } from '../../credentials/manager/SelectBuildProfileFromEasJson';
import { SetUpBuildCredentialsCommandAction } from '../../credentials/manager/SetUpBuildCredentialsCommandAction';
import { selectPlatformAsync } from '../../platform';
export default class InitializeBuildCredentials extends EasCommand {
static override description = 'Set up credentials for building your project.';
static override flags = {
platform: Flags.enum({
char: 'p',
options: [Platform.ANDROID, Platform.IOS],
}),
profile: Flags.string({
char: 'e',
description: 'The name of the build profile in eas.json.',
helpValue: 'PROFILE_NAME',
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(InitializeBuildCredentials);
const {
loggedIn: { actor, graphqlClient },
privateProjectConfig,
getDynamicPrivateProjectConfigAsync,
analytics,
vcsClient,
} = await this.getContextAsync(InitializeBuildCredentials, {
nonInteractive: false,
});
const platform = await selectPlatformAsync(flags.platform);
const buildProfile =
flags.profile ??
(await new SelectBuildProfileFromEasJson(
privateProjectConfig.projectDir,
Platform.IOS
).getProfileNameFromEasConfigAsync());
await new SetUpBuildCredentialsCommandAction(
actor,
graphqlClient,
vcsClient,
analytics,
privateProjectConfig ?? null,
getDynamicPrivateProjectConfigAsync,
platform,
buildProfile,
process.cwd()
).runAsync();
}
}
@@ -1,7 +1,7 @@
import { Flags } from '@oclif/core';
import EasCommand from '../commandUtils/EasCommand';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
import EasCommand from '../../commandUtils/EasCommand';
import { SelectPlatform } from '../../credentials/manager/SelectPlatform';
export default class Credentials extends EasCommand {
static override description = 'manage credentials';
+6 -2
View File
@@ -1,8 +1,10 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { ora } from '../ora';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
@@ -20,6 +22,7 @@ export default class Diagnostics extends EasCommand {
nonInteractive: true,
});
const spinner = ora().start(`Gathering diagnostic information...`);
const info = await envinfo.run(
{
System: ['OS', 'Shell'],
@@ -40,9 +43,10 @@ export default class Diagnostics extends EasCommand {
npmGlobalPackages: ['eas-cli', 'expo-cli'],
},
{
title: `EAS CLI ${easCliVersion} environment info`,
title: chalk.bold(`EAS CLI ${easCliVersion} environment info`),
}
);
spinner.succeed('All needed information gathered!');
Log.log(info.trimEnd());
await this.printWorkflowAsync(projectDir, vcsClient);
@@ -56,7 +60,7 @@ export default class Diagnostics extends EasCommand {
if (androidWorkflow === iosWorkflow) {
Log.log(` Project workflow: ${androidWorkflow}`);
} else {
Log.log(` Project Workflow:`);
Log.log(` Project workflow:`);
Log.log(` Android: ${androidWorkflow}`);
Log.log(` iOS: ${iosWorkflow}`);
}
@@ -110,6 +110,7 @@ describe(ProjectInit.name, () => {
it('is no-op if already configured for id', async () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
name: 'testing-123',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
@@ -121,6 +122,7 @@ describe(ProjectInit.name, () => {
it('prompts to overwrite when different', async () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
name: 'testing-123',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
@@ -135,6 +137,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -152,6 +155,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -186,6 +190,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -198,6 +203,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -223,6 +229,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -247,6 +254,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -273,6 +281,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -288,6 +297,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -302,6 +312,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -329,6 +340,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -355,6 +367,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -384,6 +397,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -398,6 +412,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-124',
name: 'testing-123',
fullName: '@jester2/testing-124',
ownerAccount: jester2.accounts[0],
});
@@ -437,6 +452,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -464,6 +480,7 @@ describe(ProjectInit.name, () => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '0129',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -0,0 +1,475 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform } from '@expo/eas-build-job';
import { EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { getEASUpdateURL, getExpoWebsiteBaseUrl } from '../../api';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { validateOrSetProjectIdAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import { CredentialsContextProjectInfo } from '../../credentials/context';
import { SetUpBuildCredentialsCommandAction } from '../../credentials/manager/SetUpBuildCredentialsCommandAction';
import {
AppFragment,
AppPlatform,
OnboardingDeviceType,
OnboardingEnvironment,
} from '../../graphql/generated';
import { UserPreferencesMutation } from '../../graphql/mutations/UserPreferencesMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { learnMore, link } from '../../log';
import { runGitCloneAsync, runGitPushAsync } from '../../onboarding/git';
import { installDependenciesAsync } from '../../onboarding/installDependencies';
import { runCommandAsync } from '../../onboarding/runCommand';
import { RequestedPlatform } from '../../platform';
import { ExpoConfigOptions, getPrivateExpoConfig } from '../../project/expoConfig';
import { promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { easCliVersion } from '../../utils/easCli';
import GitClient from '../../vcs/clients/git';
export default class Onboarding extends EasCommand {
static override hidden = true;
static override aliases = ['init:onboarding', 'onboarding'];
static override description = 'continue onboarding process started on the expo.dev website';
static override flags = {};
static override args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
const {
args: { TARGET_PROJECT_DIRECTORY: targetProjectDirInput },
} = await this.parse(Onboarding);
const {
loggedIn: { actor, graphqlClient },
analytics,
} = await this.getContextAsync(Onboarding, {
nonInteractive: false,
});
if (actor.__typename === 'Robot') {
throw new Error(
'This command is not available for robot users. Make sure you are not using a robot token and try again.'
);
}
if (!actor.preferences.onboarding) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. Visit https://expo.new to start a new project.'
);
}
if (!actor.preferences.onboarding.platform) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected platform). Continue the onboarding process on the Expo website.'
);
}
if (!actor.preferences.onboarding.environment) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected environment). Continue the onboarding process on the Expo website.'
);
}
if (!actor.preferences.onboarding.deviceType) {
throw new Error(
'This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected device type). Continue the onboarding process on the Expo website.'
);
}
if (
new Date(actor.preferences.onboarding.lastUsed) < new Date(Date.now() - 1000 * 60 * 60 * 24)
) {
Log.warn(
'It seems like you started an onboarding process, but it has been a while since you last used it. If you want to start a new onboarding process, visit https://expo.new.'
);
Log.log();
}
const platform =
actor.preferences.onboarding.platform === AppPlatform.Android
? Platform.ANDROID
: Platform.IOS;
const app = await AppQuery.byIdAsync(graphqlClient, actor.preferences.onboarding.appId);
const githubUsername = app.githubRepository
? app.githubRepository.metadata.githubRepoOwnerName
: 'expo';
const githubRepositoryName = app.githubRepository
? app.githubRepository.metadata.githubRepoName
: 'expo-template-default';
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
Log.log();
Log.log('✨ We will continue your onboarding process in EAS CLI');
Log.log();
Log.log(
`🚚 Let's start by cloning ${
app.githubRepository
? `your project (${githubUsername}/${githubRepositoryName})`
: `default Expo template project (${githubUsername}/${githubRepositoryName})`
} from GitHub and installing dependencies.`
);
Log.log();
let initialTargetProjectDirectory: string;
if (targetProjectDirInput) {
initialTargetProjectDirectory = targetProjectDirInput;
Log.log(`📂 Cloning the project to ${initialTargetProjectDirectory}`);
} else {
const { selectedTargetProjectDirectory } = await promptAsync({
type: 'text',
name: 'selectedTargetProjectDirectory',
message: app.githubRepository
? '📂 Where would you like to clone the project to?'
: '📂 Where would you like to create your new project directory?',
initial: app.githubRepository
? path.join(process.cwd(), githubRepositoryName)
: path.join(process.cwd(), `${actor.username}-first-project`),
});
initialTargetProjectDirectory = selectedTargetProjectDirectory;
}
Log.log();
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
githubUsername,
githubRepositoryName,
targetProjectDir: initialTargetProjectDirectory,
});
const vcsClient = new GitClient(finalTargetProjectDirectory);
if (!app.githubRepository) {
await fs.remove(path.join(finalTargetProjectDirectory, '.git'));
await runCommandAsync({
cwd: finalTargetProjectDirectory,
command: 'git',
args: ['init'],
});
Log.log();
await configureProjectFromBareDefaultExpoTemplateAsync({
app,
vcsClient,
targetDir: finalTargetProjectDirectory,
});
}
await installDependenciesAsync({
projectDir: finalTargetProjectDirectory,
});
const exp = await getPrivateExpoConfigWithProjectIdAsync({
projectDir: finalTargetProjectDirectory,
graphqlClient,
actor,
});
const getDynamicProjectConfigFn = getDynamicPrivateProjectConfigGetter({
projectDir: finalTargetProjectDirectory,
graphqlClient,
actor,
});
if (!app.githubRepository) {
await runCommandAsync({
cwd: finalTargetProjectDirectory,
command: 'npx',
args: ['expo', 'install', 'expo-updates'],
});
Log.log();
await runCommandAsync({
cwd: finalTargetProjectDirectory,
command: 'npx',
args: ['expo', 'install', 'expo-insights'],
});
Log.log();
await runCommandAsync({
cwd: finalTargetProjectDirectory,
command: 'npx',
args: ['expo', 'install', 'expo-dev-client'],
});
Log.log();
}
await vcsClient.trackFileAsync('package-lock.json');
const shouldSetupCredentials =
((platform === Platform.IOS &&
actor.preferences.onboarding.deviceType === OnboardingDeviceType.Device) ||
platform === Platform.ANDROID) &&
actor.preferences.onboarding.environment === OnboardingEnvironment.DevBuild;
if (shouldSetupCredentials) {
Log.log('🔑 Now we need to set up build credentials for your project:');
await new SetUpBuildCredentialsCommandAction(
actor,
graphqlClient,
vcsClient,
analytics,
exp,
getDynamicProjectConfigFn,
platform,
actor.preferences.onboarding.deviceType === OnboardingDeviceType.Simulator
? 'development-simulator'
: 'development',
finalTargetProjectDirectory
).runAsync();
}
if (app.githubRepository && (await vcsClient.hasUncommittedChangesAsync())) {
Log.log(
'📦 We will now commit the changes made by the configuration process and push them to GitHub:'
);
Log.log();
Log.log('🔍 Checking for changes in the repository...');
await vcsClient.showChangedFilesAsync();
await reviewAndCommitChangesAsync(
vcsClient,
`[eas-onboarding] Install dependencies${
shouldSetupCredentials ? ' and set up build credentials' : ''
}`,
{ nonInteractive: false }
);
Log.log('📤 Pushing changes to GitHub...');
await runGitPushAsync({
targetProjectDir: finalTargetProjectDirectory,
});
} else if (!app.githubRepository) {
await runCommandAsync({
cwd: finalTargetProjectDirectory,
command: 'git',
args: ['add', '.'],
});
Log.log();
await runCommandAsync({
cwd: finalTargetProjectDirectory,
command: 'git',
args: ['commit', '-m', 'Initial commit'],
});
Log.log();
}
Log.log();
Log.log('🎉 We finished configuring your project.');
Log.log();
if (
app.githubRepository ||
actor.preferences.onboarding.environment === OnboardingEnvironment.ExpoGo
) {
Log.log('🚀 You can now go back to the website to continue:');
const url = new URL(
`/onboarding/develop/set-up-project-on-your-machine?project=${app.slug}&accountId=${app.ownerAccount.id}`,
getExpoWebsiteBaseUrl()
).toString();
Log.log(`👉 ${link(url)}`);
} else {
Log.log('🚀 Now we are going to trigger your first build');
Log.log();
const { buildIds } = await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
finalTargetProjectDirectory,
{
nonInteractive: true,
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
profile:
actor.preferences.onboarding.deviceType === OnboardingDeviceType.Simulator
? 'development-simulator'
: 'development',
wait: false,
clearCache: false,
json: false,
autoSubmit: false,
localBuildOptions: {},
freezeCredentials: false,
repack: true,
},
actor,
getDynamicProjectConfigFn
);
const buildId = buildIds[0];
Log.log();
Log.log('🚀 You can now go back to the website to continue:');
const url = new URL(
`/onboarding/develop/set-up-project-on-your-machine?project=${app.slug}&accountId=${app.ownerAccount.id}&buildId=${buildId}`,
getExpoWebsiteBaseUrl()
).toString();
Log.log(`👉 ${link(url)}`);
}
const { __typename, ...previousPreferences } = actor.preferences.onboarding;
await UserPreferencesMutation.markCliDoneInOnboardingUserPreferencesAsync(graphqlClient, {
...previousPreferences,
appId: app.id,
});
}
}
// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
async function getPrivateExpoConfigWithProjectIdAsync({
projectDir,
graphqlClient,
actor,
options,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options?: ExpoConfigOptions;
}): Promise<CredentialsContextProjectInfo> {
const expBefore = getPrivateExpoConfig(projectDir, options);
const projectId = await validateOrSetProjectIdAsync({
exp: expBefore,
graphqlClient,
actor,
options: {
nonInteractive: false,
},
});
const exp = getPrivateExpoConfig(projectDir, options);
return {
exp,
projectId,
};
}
// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
function getDynamicPrivateProjectConfigGetter({
projectDir,
graphqlClient,
actor,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
}): DynamicConfigContextFn {
return async (options?: ExpoConfigOptions) => {
return {
...(await getPrivateExpoConfigWithProjectIdAsync({
projectDir,
graphqlClient,
actor,
options,
})),
projectDir,
};
};
}
async function configureProjectFromBareDefaultExpoTemplateAsync({
app,
vcsClient,
targetDir,
}: {
app: AppFragment;
vcsClient: GitClient;
targetDir: string;
}): Promise<void> {
// Android package name requires each component to start with a lowercase letter.
const isUsernameValidSegment = /^[^a-z]/.test(app.ownerAccount.name);
const userPrefix = isUsernameValidSegment ? 'user' : '';
const isSlugValidSegment = /^[^a-z]/.test(app.slug);
const slugPrefix = isSlugValidSegment ? 'app' : '';
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(
app.ownerAccount.name
)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
const updateUrl = getEASUpdateURL(app.id);
const easBuildGitHubConfig = {
android: {
image: 'latest',
},
ios: {
image: 'latest',
},
};
const easJson: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
channel: 'development',
...easBuildGitHubConfig,
},
'development-simulator': {
extends: 'development',
ios: {
simulator: true,
},
},
preview: {
distribution: 'internal',
channel: 'preview',
...easBuildGitHubConfig,
},
production: {
channel: 'production',
...easBuildGitHubConfig,
},
},
submit: {
production: {},
},
};
const easJsonPath = path.join(targetDir, 'eas.json');
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcsClient.trackFileAsync(easJsonPath);
Log.log(
`✅ Generated ${chalk.bold('eas.json')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
)}`
);
Log.log();
const expoConfig: ExpoConfig = {
name: app.name ?? app.slug,
slug: app.slug,
extra: {
eas: {
projectId: app.id,
},
},
owner: app.ownerAccount.name,
updates: {
url: updateUrl,
},
runtimeVersion: {
policy: 'appVersion',
},
ios: {
bundleIdentifier,
},
android: {
package: bundleIdentifier,
},
};
const appJsonPath = path.join(targetDir, 'app.json');
await fs.writeFile(appJsonPath, `${JSON.stringify({ expo: expoConfig }, null, 2)}\n`);
await vcsClient.trackFileAsync(appJsonPath);
Log.log(
`✅ Generated ${chalk.bold('app.json')}. ${learnMore(
'https://docs.expo.dev/versions/latest/config/app/'
)}`
);
Log.log();
}
function stripInvalidCharactersForBundleIdentifier(string: string): string {
return string.replaceAll(/[^A-Za-z0-9]/g, '');
}
+9
View File
@@ -33,6 +33,7 @@ interface RawCommandFlags {
verbose: boolean;
wait: boolean;
'non-interactive': boolean;
'verbose-fastlane': boolean;
}
interface CommandFlags {
@@ -42,6 +43,7 @@ interface CommandFlags {
verbose: boolean;
wait: boolean;
nonInteractive: boolean;
isVerboseFastlaneEnabled: boolean;
}
export default class Submit extends EasCommand {
@@ -83,6 +85,10 @@ export default class Submit extends EasCommand {
default: true,
allowNo: true,
}),
'verbose-fastlane': Flags.boolean({
default: false,
description: 'Enable verbose logging for the submission process',
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
@@ -132,6 +138,7 @@ export default class Submit extends EasCommand {
profile: submissionProfile.profile,
archiveFlags: flagsWithPlatform.archiveFlags,
nonInteractive: flagsWithPlatform.nonInteractive,
isVerboseFastlaneEnabled: flagsWithPlatform.isVerboseFastlaneEnabled,
actor,
graphqlClient,
analytics,
@@ -175,6 +182,7 @@ export default class Submit extends EasCommand {
wait,
profile,
'non-interactive': nonInteractive,
'verbose-fastlane': isVerboseFastlaneEnabled,
...archiveFlags
} = flags;
@@ -195,6 +203,7 @@ export default class Submit extends EasCommand {
wait,
profile,
nonInteractive,
isVerboseFastlaneEnabled,
};
}
@@ -12,6 +12,7 @@ import {
DynamicPublicProjectConfigContextField,
} from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import VcsClientContextField from '../../../commandUtils/context/VcsClientContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
@@ -21,6 +22,7 @@ import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { resolveVcsClient } from '../../../vcs';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
@@ -265,10 +267,14 @@ function mockTestProject({
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest
.spyOn(VcsClientContextField.prototype, 'getValueAsync')
.mockResolvedValue(resolveVcsClient());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -367,6 +367,7 @@ function mockTestProject({
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -11,6 +11,7 @@ import {
DynamicPublicProjectConfigContextField,
} from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import VcsClientContextField from '../../../commandUtils/context/VcsClientContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
@@ -19,6 +20,7 @@ import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { resolveVcsClient } from '../../../vcs';
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
const projectRoot = '/test-project';
@@ -250,10 +252,14 @@ function mockTestProject({
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest
.spyOn(VcsClientContextField.prototype, 'getValueAsync')
.mockResolvedValue(resolveVcsClient());
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
name: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
@@ -52,6 +52,7 @@ export default class UpdateConfigure extends EasCommand {
projectDir,
platform,
vcsClient,
env: undefined,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
+12 -15
View File
@@ -1,4 +1,5 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -6,7 +7,6 @@ import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
@@ -40,6 +40,7 @@ import {
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
@@ -199,6 +200,7 @@ export default class UpdatePublish extends EasCommand {
projectDir,
projectId,
vcsClient,
env: undefined,
});
const { exp } = await getDynamicPublicProjectConfigAsync();
@@ -351,7 +353,7 @@ export default class UpdatePublish extends EasCommand {
Log.withInfo(
`${platformString} (maximum: ${assetLimitPerUpdateGroup} total per update). ${learnMore(
'https://expo.fyi/eas-update-asset-limits',
{ learnMoreMessage: 'Learn more about asset limits.' }
{ learnMoreMessage: 'Learn more about asset limits' }
)}`
);
} catch (e) {
@@ -359,29 +361,24 @@ export default class UpdatePublish extends EasCommand {
throw e;
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersions = await getRuntimeVersionObjectAsync({
exp,
platforms: realizedPlatforms,
projectDir,
vcsClient,
workflows: {
...workflows,
web: Workflow.UNKNOWN,
},
env: undefined,
});
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
Log.withTick(
`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`
);
}
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
@@ -517,7 +514,7 @@ export default class UpdatePublish extends EasCommand {
},
]
: []),
{ label: 'Website link', value: updateGroupLink },
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
@@ -1,11 +1,10 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
@@ -33,6 +32,7 @@ import {
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
@@ -154,6 +154,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
projectDir,
projectId,
vcsClient,
env: undefined,
});
// check that the expo-updates package version supports roll back to embedded
@@ -184,28 +185,24 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const realizedPlatforms: PublishPlatform[] =
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersions = await getRuntimeVersionObjectAsync({
exp,
platforms: realizedPlatforms,
projectDir,
vcsClient,
workflows: {
...workflows,
web: Workflow.UNKNOWN,
},
env: undefined,
});
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
@@ -279,7 +276,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
},
]
: []),
{ label: 'Website link', value: updateGroupLink },
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
@@ -55,6 +55,7 @@ export const testPKCS12EmptyPasswordKeystore: Keystore = {
export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
name: 'testapp',
slug: 'testapp',
ownerAccount: {
id: 'test-account-id',
@@ -1,5 +1,5 @@
import AppStoreApi from '../ios/appstore/AppStoreApi';
import { AuthCtx } from '../ios/appstore/authenticateTypes';
import { AuthCtx, AuthenticationMode } from '../ios/appstore/authenticateTypes';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
@@ -9,6 +9,7 @@ export const testAuthCtx: AuthCtx = {
export function getAppstoreMock(): AppStoreApi {
return {
defaultAuthenticationMode: AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(),
ensureBundleIdExistsAsync: jest.fn(),
listDistributionCertificatesAsync: jest.fn(),
@@ -1,3 +1,5 @@
import assert from 'node:assert';
import { AppFragment, Role } from '../../graphql/generated';
import { Actor } from '../../user/User';
@@ -24,6 +26,7 @@ export const jester: Actor = {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};
export const jester2 = {
@@ -41,6 +44,7 @@ export const jester2 = {
featureGates: {},
};
assert(jester.__typename === 'User');
export const testUsername = jester.username;
export const testSlug = 'testApp';
export const testProjectId = '7ef93448-3bc7-4b57-be32-99326dcf24f0';
@@ -76,6 +80,7 @@ export const testAppJsonWithDifferentOwner = {
export const testAppQueryByIdResponse: AppFragment = {
id: testProjectId,
slug: testSlug,
name: 'testing 123',
fullName: testExperienceName,
ownerAccount: jester.accounts[0],
};
@@ -55,6 +55,7 @@ export const testAppleAppIdentifierFragment: AppleAppIdentifierFragment = {
export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
name: 'testapp',
slug: 'testapp',
ownerAccount: {
id: 'test-account-id',
@@ -4,27 +4,76 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
GoogleServiceAccountKeyByAccountQuery,
GoogleServiceAccountKeyFragment,
GoogleServiceAccountKeysPaginatedByAccountQuery,
} from '../../../../../graphql/generated';
import { GoogleServiceAccountKeyFragmentNode } from '../../../../../graphql/types/credentials/GoogleServiceAccountKey';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const GoogleServiceAccountKeyQuery = {
async getAllForAccountAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<GoogleServiceAccountKeyFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<GoogleServiceAccountKeyFragment>> => {
return await GoogleServiceAccountKeyQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching Google Service Account Keys...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<
GoogleServiceAccountKeysPaginatedByAccountQuery['account']['byName']['googleServiceAccountKeysPaginated']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GoogleServiceAccountKeyByAccountQuery>(
.query<GoogleServiceAccountKeysPaginatedByAccountQuery>(
gql`
query GoogleServiceAccountKeyByAccountQuery($accountName: String!) {
query GoogleServiceAccountKeysPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
account {
byName(accountName: $accountName) {
id
googleServiceAccountKeys {
id
...GoogleServiceAccountKeyFragment
googleServiceAccountKeysPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...GoogleServiceAccountKeyFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
@@ -33,6 +82,10 @@ export const GoogleServiceAccountKeyQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['GoogleServiceAccountKey'],
@@ -40,6 +93,6 @@ export const GoogleServiceAccountKeyQuery = {
)
.toPromise()
);
return data.account.byName.googleServiceAccountKeys;
return data.account.byName.googleServiceAccountKeysPaginated;
},
};
@@ -7,8 +7,10 @@ import * as AndroidGraphqlClient from './android/api/GraphqlClient';
import * as IosGraphqlClient from './ios/api/GraphqlClient';
import AppStoreApi from './ios/appstore/AppStoreApi';
import { AuthenticationMode } from './ios/appstore/authenticateTypes';
import { MinimalAscApiKey } from './ios/credentials';
import { Analytics } from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppStoreConnectApiKeyFragment } from '../graphql/generated';
import Log from '../log';
import { getPrivateExpoConfig } from '../project/expoConfig';
import { confirmAsync } from '../prompts';
@@ -25,6 +27,7 @@ export class CredentialsContext {
public readonly appStore = new AppStoreApi();
public readonly ios = IosGraphqlClient;
public readonly nonInteractive: boolean;
public readonly freezeCredentials: boolean = false;
public readonly projectDir: string;
public readonly user: Actor;
public readonly graphqlClient: ExpoGraphqlClient;
@@ -47,6 +50,7 @@ export class CredentialsContext {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
freezeCredentials?: boolean;
env?: Env;
}
) {
@@ -58,6 +62,7 @@ export class CredentialsContext {
this.vcsClient = options.vcsClient;
this.nonInteractive = options.nonInteractive ?? false;
this.projectInfo = options.projectInfo;
this.freezeCredentials = options.freezeCredentials ?? false;
}
get hasProjectContext(): boolean {
@@ -82,6 +87,28 @@ export class CredentialsContext {
getPrivateExpoConfig(this.options.projectDir);
}
public configureAppStoreAuth(ascApiKey: AppStoreConnectApiKeyFragment): void {
/* keyP8: string;
keyId: string;
issuerId: string;
teamId?: string;
teamName?: string;
roles?: UserRole[];
name?: string; */
this.appStore.authCtx = {
ascApiKey: {
keyP8: ascApiKey.keyP8,
keyId: ascApiKey.keyIdentifier,
issuerId: ascApiKey.issuerIdentifier,
},
appleTeam: {
teamIdentifier: ascApiKey.appleTeam?.appleTeamIdentifier ?? '',
teamName: ascApiKey.appleTeam?.appleTeamName,
teamType: ascApiKey.appleTeam,
},
};
}
async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
if (this.appStore.authCtx || !this.shouldAskAuthenticateAppStore) {
// skip prompts if already have apple ctx or already asked about it
@@ -1,9 +1,31 @@
import { learnMore } from '../log';
export class MissingCredentialsNonInteractiveError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up. Run this command again in interactive mode.');
}
}
export class InsufficientAuthenticationNonInteractiveError extends Error {
constructor(message?: string) {
super(
message ??
`Authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
}
export class ForbidCredentialModificationError extends Error {
constructor(message?: string) {
super(
message ??
'Credentials cannot be modified. Run this command again without the --freeze-credentials flag.'
);
}
}
export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
@@ -14,6 +14,7 @@ import { isAdHocProfile, isEnterpriseUniversalProfile } from './utils/provisioni
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
import { findApplicationTarget } from '../../project/ios/target';
import { isExpoNotificationsInstalled } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
@@ -105,6 +106,11 @@ export default class IosCredentialsProvider {
if (ctx.easJsonCliConfig?.promptToConfigurePushNotifications === false) {
return null;
} else if (
ctx.easJsonCliConfig?.promptToConfigurePushNotifications === undefined &&
!(await isExpoNotificationsInstalled(ctx.projectDir))
) {
return null;
}
const setupOption = await selectAsync(
@@ -1,3 +1,4 @@
import { UserRole } from '@expo/apple-utils';
import chalk from 'chalk';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
@@ -24,10 +25,19 @@ import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey
export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit',
BUILD_SERVICE = 'EAS Build',
}
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
export const PURPOSE_TO_ROLES: Record<AppStoreApiKeyPurpose, UserRole[]> = {
[AppStoreApiKeyPurpose.SUBMISSION_SERVICE]: [UserRole.ADMIN],
[AppStoreApiKeyPurpose.BUILD_SERVICE]: [UserRole.ADMIN],
};
export async function promptForAscApiKeyPathAsync(
ctx: CredentialsContext,
roles: UserRole[]
): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync(roles);
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
if (bestEffortIssuerId) {
@@ -60,15 +70,15 @@ export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<Mi
export async function provideOrGenerateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
roles: UserRole[]
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
const userProvided = await promptForAscApiKeyAsync(ctx, roles);
if (!userProvided) {
return await generateAscApiKeyAsync(ctx, purpose);
return await generateAscApiKeyAsync(ctx, roles);
}
if (!ctx.appStore.authCtx) {
@@ -86,39 +96,49 @@ export async function provideOrGenerateAscApiKeyAsync(
if (useUserProvided) {
return userProvided;
}
return await provideOrGenerateAscApiKeyAsync(ctx, purpose);
return await provideOrGenerateAscApiKeyAsync(ctx, roles);
}
async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
roles: UserRole[]
): Promise<MinimalAscApiKey> {
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, {
nickname: getAscApiKeyName(purpose),
nickname: getAscApiKeyName(),
roles,
});
return await getMinimalAscApiKeyAsync(ascApiKey);
}
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
const nameParts = ['[Expo]', purpose, nanoid(10)];
export function getAscApiKeyName(): string {
const nameParts = ['[Expo]', nanoid(10)];
return nameParts.join(' ');
}
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> {
async function promptForAscApiKeyAsync(
ctx: CredentialsContext,
roles: UserRole[]
): Promise<MinimalAscApiKey | null> {
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
if (shouldAutoGenerateCredentials) {
return null;
}
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx, roles);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
}
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
async function promptForKeyP8AndIdAsync(
roles: UserRole[]
): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
const rolesStringified = roles.map(r => r.toString()).join(', ');
const isPlural = roles.length > 1;
Log.log(
chalk.bold(
'An App Store Connect Api key is required to upload your app to the Apple App Store Connect'
`An App Store Connect Api key with the ${rolesStringified} role${
isPlural ? 's' : ''
} is required to upload your app to the Apple App Store Connect`
)
);
Log.log(
@@ -241,13 +261,16 @@ export function sortAscApiKeysByUpdatedAtDesc(
}
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
const { keyIdentifier, appleTeam, roles, name, updatedAt } = ascApiKey;
let line: string = '';
line += `Key ID: ${keyIdentifier}`;
if (name) {
line += chalk.gray(`\n Name: ${name}`);
}
if (roles && roles.length > 0) {
line += chalk.gray(`\n Roles: ${roles.join(', ')}`);
}
if (appleTeam) {
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
}
@@ -1,7 +1,9 @@
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
import { ChooseAssociatedAppleTeamId } from './ChooseAssociatedAppleTeamId';
import { UpdateAppleTeamInfo } from './UpdateAppleTeamInfo';
import {
AppStoreConnectApiKeyFragment,
AppleTeamFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
@@ -16,12 +18,15 @@ export class AssignAscApiKey {
ascApiKey: AppStoreConnectApiKeyFragment,
purpose: AppStoreApiKeyPurpose
): Promise<CommonIosAppCredentialsFragment> {
const appleTeam =
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
const [ascApiKeyWithAppleTeam, appleTeam] = await this.ensureKeyHasAppleTeamInfoAsync(
ctx,
ascApiKey
);
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
ctx.graphqlClient,
this.app,
{ appleTeam: appleTeam ?? undefined }
{ appleTeam }
);
let updatedAppCredentials;
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
@@ -29,7 +34,15 @@ export class AssignAscApiKey {
ctx.graphqlClient,
appCredentials,
{
ascApiKeyIdForSubmissions: ascApiKey.id,
ascApiKeyIdForSubmissions: ascApiKeyWithAppleTeam.id,
}
);
} else if (purpose === AppStoreApiKeyPurpose.BUILD_SERVICE) {
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(
ctx.graphqlClient,
appCredentials,
{
ascApiKeyIdForBuilds: ascApiKeyWithAppleTeam.id,
}
);
} else {
@@ -40,4 +53,28 @@ export class AssignAscApiKey {
);
return updatedAppCredentials;
}
private async ensureKeyHasAppleTeamInfoAsync(
ctx: CredentialsContext,
ascApiKey: AppStoreConnectApiKeyFragment
): Promise<[AppStoreConnectApiKeyFragment, AppleTeamFragment]> {
let appleTeamIdentifier = ascApiKey.appleTeam?.appleTeamIdentifier;
if (!appleTeamIdentifier) {
const chooseAssociatedAppleTeamIdAction = new ChooseAssociatedAppleTeamId(
'App Store Connect API Key'
);
appleTeamIdentifier = await chooseAssociatedAppleTeamIdAction.runAsync(ctx);
}
const updateAppleTeamAction = new UpdateAppleTeamInfo(this.app.account, appleTeamIdentifier);
const appleTeam = await updateAppleTeamAction.runAsync(ctx);
if (!ascApiKey.appleTeam) {
const updatedAscApiKey = await ctx.ios.updateAscApiKeyAsync(
ctx.graphqlClient,
ascApiKey,
appleTeam
);
return [updatedAscApiKey, appleTeam];
}
return [ascApiKey, appleTeam];
}
}
@@ -0,0 +1,48 @@
import { Teams } from '@expo/apple-utils';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
export class ChooseAssociatedAppleTeamId {
constructor(private associatedCredential: string) {}
public async runAsync(ctx: CredentialsContext): Promise<string> {
if (!ctx.appStore.authCtx) {
return await this.promptForAppleTeamIdAsync();
}
const teams = await Teams.getTeamsAsync();
const chosenTeamId = await this.promptForAppleTeamIdFromDeveloperPortalAsync(
teams.map(team => ({ prettyName: `${team.name} (${team.type})`, teamId: team.teamId }))
);
return chosenTeamId ?? (await this.promptForAppleTeamIdAsync());
}
private async promptForAppleTeamIdFromDeveloperPortalAsync(
teamChoices: { prettyName: string; teamId: string }[]
): Promise<string | null> {
if (teamChoices.length === 0) {
return null;
}
const { appleTeamId } = await promptAsync({
type: 'select',
message: `Select the Apple Team associated with your ${this.associatedCredential}:`,
name: 'appleTeamId',
choices: [
...teamChoices.map(({ prettyName, teamId }) => ({ title: prettyName, value: teamId })),
{ title: 'None of the above', value: null },
],
});
return appleTeamId;
}
private async promptForAppleTeamIdAsync(): Promise<string> {
const { appleTeamId } = await promptAsync({
type: 'text',
name: 'appleTeamId',
message: `Input the Apple Team ID associated with your ${this.associatedCredential}:`,
validate: (val: string) => val !== '',
});
return appleTeamId;
}
}
@@ -5,15 +5,18 @@ import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticateTypes';
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
import { Target } from '../types';
export class ConfigureProvisioningProfile {
@@ -27,9 +30,18 @@ export class ConfigureProvisioningProfile {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleProvisioningProfileMutationResult | null> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Configuring Provisioning Profiles is only supported in interactive mode.'
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Remove the --freeze-credentials flag to configure a Provisioning Profile.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
const { developerPortalIdentifier, provisioningProfile } = this.originalProvisioningProfile;
@@ -46,7 +58,7 @@ export class ConfigureProvisioningProfile {
return null;
}
const applePlatform = await getApplePlatformFromTarget(this.target);
const applePlatform = getApplePlatformFromTarget(this.target);
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier,
applePlatform
@@ -1,6 +1,17 @@
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import {
AppStoreApiKeyPurpose,
PURPOSE_TO_ROLES,
provideOrGenerateAscApiKeyAsync,
} from './AscApiKeyUtils';
import { ChooseAssociatedAppleTeamId } from './ChooseAssociatedAppleTeamId';
import { UpdateAppleTeamInfo } from './UpdateAppleTeamInfo';
import {
AccountFragment,
AppStoreConnectApiKeyFragment,
AppleTeamFragment,
} from '../../../graphql/generated';
import { CredentialsContext } from '../../context';
import { MinimalAscApiKey } from '../credentials';
export class CreateAscApiKey {
constructor(private account: AccountFragment) {}
@@ -12,8 +23,32 @@ export class CreateAscApiKey {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
}
const roles = PURPOSE_TO_ROLES[purpose];
if (!roles) {
throw new Error(`Unsupported purpose: ${purpose}`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
return await ctx.ios.createAscApiKeyAsync(ctx.graphqlClient, this.account, ascApiKey);
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, roles);
const appleTeam = await this.ensureKeyHasAppleTeamInfoAsync(ctx, ascApiKey);
return await ctx.ios.createAscApiKeyAsync(ctx.graphqlClient, this.account, {
...ascApiKey,
teamId: appleTeam.appleTeamIdentifier,
});
}
private async ensureKeyHasAppleTeamInfoAsync(
ctx: CredentialsContext,
ascApiKey: MinimalAscApiKey
): Promise<AppleTeamFragment> {
let appleTeamIdentifier = ascApiKey.teamId;
if (!appleTeamIdentifier) {
const chooseAssociatedAppleTeamIdAction = new ChooseAssociatedAppleTeamId(
'App Store Connect API Key'
);
appleTeamIdentifier = await chooseAssociatedAppleTeamIdAction.runAsync(ctx);
}
const updateAppleTeamAction = new UpdateAppleTeamInfo(this.account, appleTeamIdentifier);
return await updateAppleTeamAction.runAsync(ctx);
}
}
@@ -4,14 +4,17 @@ import nullthrows from 'nullthrows';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { generateProvisioningProfileAsync } from './ProvisioningProfileUtils';
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfile } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticateTypes';
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
import { provisioningProfileSchema } from '../credentials';
import { Target } from '../types';
@@ -23,9 +26,18 @@ export class CreateProvisioningProfile {
) {}
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Creating Provisioning Profiles is only supported in interactive mode.'
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Run this command again without the --freeze-credentials flag in order to generate a new Provisioning Profile.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to generate a new Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
const appleAuthCtx = await ctx.appStore.ensureAuthenticatedAsync();
@@ -49,16 +61,31 @@ export class CreateProvisioningProfile {
return provisioningProfileMutationResult;
}
private async provideOrGenerateAsync(
ctx: CredentialsContext,
appleAuthCtx: AuthCtx
): Promise<ProvisioningProfile> {
private async maybeGetUserProvidedAsync(
ctx: CredentialsContext
): Promise<ProvisioningProfile | null> {
if (ctx.nonInteractive) {
return null;
}
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
if (userProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
Log.warn('Provisioning profile: Unable to validate specified profile.');
return userProvided;
}
return null;
}
private async provideOrGenerateAsync(
ctx: CredentialsContext,
appleAuthCtx: AuthCtx
): Promise<ProvisioningProfile> {
const maybeUserProvided = await this.maybeGetUserProvidedAsync(ctx);
if (maybeUserProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
Log.warn('Provisioning profile: Unable to validate specified profile.');
return maybeUserProvided;
}
assert(
this.distributionCertificate.certificateP12,
'Distribution Certificate must have p12 certificate'
@@ -48,8 +48,13 @@ export function formatDistributionCertificate(
buildCredentials => buildCredentials.iosAppCredentials.app
);
if (apps.length) {
const appFullNames = apps.map(app => app.fullName).join(',');
line += chalk.gray(`\n 📲 Used by: ${appFullNames}`);
// iosAppBuildCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
line += chalk.gray(`\n 📲 Used by: ${appFullNames}${andMaybeMore}`);
}
if (validSerialNumbers?.includes(serialNumber)) {
@@ -254,6 +259,10 @@ function formatDistributionCertificateFromApple(
const { name, status, id, expires, created, ownerName, serialNumber } = appleInfo;
const expiresDate = new Date(expires * 1000).toDateString();
const createdDate = new Date(created * 1000).toDateString();
return `${name} (${status}) - Cert ID: ${id}, Serial number: ${serialNumber}, Team ID: ${appleInfo.ownerId}, Team name: ${ownerName}
expires: ${expiresDate}, created: ${createdDate}`;
return [
`🍏 ${chalk.bold(name)} (${status})`,
`${chalk.bold('ID:')} ${id} ${chalk.bold('Serial Number:')} ${serialNumber}`,
`${chalk.bold('Apple Team:')} ${appleInfo.ownerId} (${ownerName})`,
`${chalk.bold('Expires:')} ${expiresDate} ${chalk.bold('Created:')} ${createdDate}`,
].join('\n\t');
}
@@ -0,0 +1,39 @@
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
export class GetAscApiKey {
constructor(
private app: AppLookupParams,
private purpose: AppStoreApiKeyPurpose
) {}
public async runAsync(ctx: CredentialsContext): Promise<AppStoreConnectApiKeyFragment | null> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(
ctx.graphqlClient,
this.app
);
if (this.purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
return appCredentials?.appStoreConnectApiKeyForSubmissions ?? null;
} else if (this.purpose === AppStoreApiKeyPurpose.BUILD_SERVICE) {
const ascApiKey = appCredentials?.appStoreConnectApiKeyForBuilds;
if (!ascApiKey) {
return null;
}
const appleTeam = ascApiKey.appleTeam;
const hasAppleTeamInfo =
!!appleTeam && !!appleTeam.appleTeamIdentifier && !!appleTeam.appleTeamType;
if (!hasAppleTeamInfo) {
Log.warn(
`App Store Connect API Key for ${this.purpose} is missing Apple Team information. Run "eas credentials" and select "Use an existing API Key" to update the missing information.`
);
return null;
}
return ascApiKey;
} else {
throw new Error(`App Store Connect API Keys are not yet supported for ${this.purpose}.`);
}
}
}
@@ -185,8 +185,13 @@ export function formatPushKey(
const apps = pushKey.iosAppCredentialsList.map(appCredentials => appCredentials.app);
if (apps.length) {
const appFullNames = apps.map(app => app.fullName).join(',');
line += chalk.gray(`\n 📲 Used by: ${appFullNames}`);
// iosAppCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
line += chalk.gray(`\n 📲 Used by: ${appFullNames}${andMaybeMore}`);
}
if (validPushKeyIdentifiers?.includes(keyIdentifier)) {
@@ -34,14 +34,20 @@ export class RemoveDistributionCertificate {
buildCredentials => buildCredentials.iosAppCredentials.app
);
if (apps.length !== 0) {
const appFullNames = apps.map(app => app.fullName).join(',');
// iosAppBuildCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
if (ctx.nonInteractive) {
throw new Error(
`Certificate is currently used by ${appFullNames} and cannot be deleted in non-interactive mode.`
`Certificate is currently used by ${appFullNames}${andMaybeMore} and cannot be deleted in non-interactive mode.`
);
}
const confirm = await confirmAsync({
message: `You are removing certificate used by ${appFullNames}. Do you want to continue?`,
message: `You are removing certificate used by ${appFullNames}${andMaybeMore}. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
@@ -27,9 +27,14 @@ export class RemovePushKey {
const apps = this.pushKey.iosAppCredentialsList.map(appCredentials => appCredentials.app);
if (apps.length !== 0) {
const appFullNames = apps.map(app => app.fullName).join(',');
// iosAppCredentialsList is capped at 20 on www
const appFullNames = apps
.map(app => app.fullName)
.slice(0, 19)
.join(',');
const andMaybeMore = apps.length > 19 ? ' (and more)' : '';
const confirm = await confirmAsync({
message: `Removing this push key will disable push notifications for ${appFullNames}. Do you want to continue?`,
message: `Removing this push key will disable push notifications for ${appFullNames}${andMaybeMore}. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
@@ -110,10 +110,13 @@ export class SetUpAscApiKey {
ctx.graphqlClient,
this.app
);
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
} else if (purpose === AppStoreApiKeyPurpose.BUILD_SERVICE) {
return !!appCredentials?.appStoreConnectApiKeyForBuilds;
} else {
throw new Error(`App Store Connect API Key setup is not yet supported for ${purpose}.`);
}
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
}
private async processChoicesAsync(
@@ -15,12 +15,17 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../graphql/generated';
import { learnMore } from '../../../log';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthenticationMode } from '../appstore/authenticateTypes';
import { Target } from '../types';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
@@ -102,9 +107,18 @@ export class SetUpProvisioningProfile {
if (areBuildCredentialsSetup) {
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, this.distributionType));
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Provisioning profile is not configured correctly. Run this command again in interactive mode.'
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Provisioning profile is not configured correctly. Remove the --freeze-credentials flag to configure it.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
@@ -127,11 +141,18 @@ export class SetUpProvisioningProfile {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
const confirm = await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
const isNonInteractiveOrUserDidConfirmAsync = async (): Promise<boolean> => {
if (ctx.nonInteractive) {
return true;
}
return await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
};
const confirm = await isNonInteractiveOrUserDidConfirmAsync();
if (!confirm) {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
@@ -0,0 +1,88 @@
import { Teams } from '@expo/apple-utils';
import { AccountFragment, AppleTeamFragment, AppleTeamType } from '../../../graphql/generated';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppleTeamQuery } from '../api/graphql/queries/AppleTeamQuery';
/* *
* Update the Apple Team for the app.
* If the user is logged in, we sync the Team info from the Developer Portal
* Else we ask the user to choose their Team Type (Company/Organization, Individual, In-House)
*/
export class UpdateAppleTeamInfo {
constructor(
private account: AccountFragment,
private appleTeamIdentifier: string
) {}
public async runAsync(ctx: CredentialsContext): Promise<AppleTeamFragment> {
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
ctx.graphqlClient,
this.account.id,
this.appleTeamIdentifier
);
const hasAppleTeamInfo = !!appleTeam && !!appleTeam.appleTeamType;
if (hasAppleTeamInfo) {
return appleTeam;
}
if (!ctx.appStore.authCtx) {
return await this.promptAndUpdateAppleTeamTypeAsync(ctx);
}
const teams = await Teams.getTeamsAsync();
const team = teams.find(t => t.teamId === this.appleTeamIdentifier);
if (!team) {
return await this.promptAndUpdateAppleTeamTypeAsync(ctx);
}
const { teamId, type, name } = team;
const teamType = appStoreTeamTypeToEnum(type);
return await ctx.ios.createOrUpdateExistingAppleTeamAsync(ctx.graphqlClient, this.account, {
appleTeamIdentifier: teamId,
appleTeamName: name,
appleTeamType: teamType,
});
}
private async promptAndUpdateAppleTeamTypeAsync(
ctx: CredentialsContext
): Promise<AppleTeamFragment> {
if (ctx.nonInteractive) {
throw new Error(
'Unable to get Apple Team information in non-interactive mode. Please re-run with interactive mode.'
);
}
const teamType = await promptForAppleTeamTypeAsync();
return await ctx.ios.createOrUpdateExistingAppleTeamAsync(ctx.graphqlClient, this.account, {
appleTeamIdentifier: this.appleTeamIdentifier,
appleTeamType: teamType,
});
}
}
function appStoreTeamTypeToEnum(
teamType: 'In-House' | 'Company/Organization' | 'Individual' | string
): AppleTeamType | null {
if (teamType === 'In-House') {
return AppleTeamType.InHouse;
} else if (teamType === 'Company/Organization') {
return AppleTeamType.CompanyOrOrganization;
} else if (teamType === 'Individual') {
return AppleTeamType.Individual;
}
return null;
}
async function promptForAppleTeamTypeAsync(): Promise<AppleTeamType> {
const { appleTeamType } = await promptAsync({
type: 'select',
message: 'Select your Apple Team Type:',
name: 'appleTeamType',
choices: [
{ title: 'Enterprise', value: AppleTeamType.InHouse },
{ title: 'Company/Organization', value: AppleTeamType.CompanyOrOrganization },
{ title: 'Individual', value: AppleTeamType.Individual },
],
});
return appleTeamType;
}
@@ -1,21 +1,15 @@
import { UserRole } from '@expo/apple-utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
import {
AppStoreApiKeyPurpose,
getAscApiKeyName,
promptForAscApiKeyPathAsync,
} from '../AscApiKeyUtils';
import { getAscApiKeyName, promptForAscApiKeyPathAsync } from '../AscApiKeyUtils';
jest.mock('../../../../prompts');
jest.mock('../../../utils/promptForCredentials');
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
return Object.keys(obj) as K[];
}
afterEach(() => {
jest.mocked(promptAsync).mockClear();
jest.mocked(getCredentialsFromUserAsync).mockClear();
@@ -24,11 +18,8 @@ afterEach(() => {
describe(getAscApiKeyName, () => {
// Apple enforces a 30 char limit on this name
it('produces a name under 30 chars', async () => {
for (const value of enumKeys(AppStoreApiKeyPurpose)) {
const purpose = AppStoreApiKeyPurpose[value];
const name = getAscApiKeyName(purpose);
expect(name.length < 30).toBe(true);
}
const name = getAscApiKeyName();
expect(name.length < 30).toBe(true);
});
});
@@ -51,7 +42,7 @@ describe(promptForAscApiKeyPathAsync, () => {
authCtx: null,
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx, [UserRole.ADMIN]);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
@@ -76,7 +67,7 @@ describe(promptForAscApiKeyPathAsync, () => {
getAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx, [UserRole.ADMIN]);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id-from-apple',
@@ -11,7 +11,11 @@ import {
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../../errors';
import { AuthenticationMode } from '../../appstore/authenticateTypes';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
@@ -24,15 +28,18 @@ describe('ConfigureProvisioningProfile', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('configures a Provisioning Profile in Interactive Mode', async () => {
const testCases = ['NON_INTERACTIVE', 'INTERACTIVE'];
test.each(testCases)('configures a Provisioning Profile in %s Mode', async mode => {
const mockProvisioningProfileFromApple = {
provisioningProfileId: testProvisioningProfileFragment.developerPortalIdentifier,
provisioningProfile: testProvisioningProfileFragment.provisioningProfile,
};
const ctx = createCtxMock({
nonInteractive: false,
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
@@ -57,7 +64,30 @@ describe('ConfigureProvisioningProfile', () => {
// expect provisioning profile not to be updated on apple portal
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
it('errors with --freeze-credentials flag', async () => {
const ctx = createCtxMock({
freezeCredentials: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const provProfConfigurator = new ConfigureProvisioningProfile(
appLookupParams,
testTarget,
testDistCertFragmentNoDependencies,
testProvisioningProfileFragment
);
await expect(provProfConfigurator.runAsync(ctx)).rejects.toThrowError(
ForbidCredentialModificationError
);
// expect provisioning profile not to be updated on expo servers
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('errors with wrong authentication type in nonInteractive mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -72,7 +102,7 @@ describe('ConfigureProvisioningProfile', () => {
testProvisioningProfileFragment
);
await expect(provProfConfigurator.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
InsufficientAuthenticationNonInteractiveError
);
// expect provisioning profile not to be updated on expo servers
@@ -10,7 +10,11 @@ import {
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../../errors';
import { AuthenticationMode } from '../../appstore/authenticateTypes';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
@@ -22,11 +26,15 @@ describe('CreateProvisioningProfile', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('creates a Provisioning Profile in Interactive Mode', async () => {
const testCases = ['NON_INTERACTIVE', 'INTERACTIVE'];
test.each(testCases)('creates a Provisioning Profile in in %s Mode', async mode => {
const ctx = createCtxMock({
nonInteractive: false,
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
@@ -48,7 +56,29 @@ describe('CreateProvisioningProfile', () => {
// expect provisioning profile to be created on apple portal
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
it('errors with --freeze-credentials flag', async () => {
const ctx = createCtxMock({
freezeCredentials: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const createProvProfAction = new CreateProvisioningProfile(
appLookupParams,
testTarget,
testDistCertFragmentNoDependencies
);
await expect(createProvProfAction.runAsync(ctx)).rejects.toThrowError(
ForbidCredentialModificationError
);
// expect provisioning profile not to be created on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be created on apple portal
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('errors with wrong authentication type in nonInteractive mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -62,7 +92,7 @@ describe('CreateProvisioningProfile', () => {
testDistCertFragmentNoDependencies
);
await expect(createProvProfAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
InsufficientAuthenticationNonInteractiveError
);
// expect provisioning profile not to be created on expo servers
@@ -15,7 +15,11 @@ import {
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../../errors';
import { AuthenticationMode } from '../../appstore/authenticateTypes';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
@@ -32,32 +36,185 @@ describe('SetUpProvisioningProfile', () => {
beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue(testAppQueryByIdResponse);
});
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
const testCases = ['NON_INTERACTIVE', 'INTERACTIVE'];
test.each(testCases)(
'repairs existing Provisioning Profile with bad build credentials in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => [
{
provisioningProfileId: nullthrows(
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0]
.provisioningProfile
).developerPortalIdentifier,
},
]),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
() => testIosAppBuildCredentialsFragment
),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
}
);
test.each(testCases)(
'sets up a new Provisioning Profile with bad build credentials in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
}
);
test.each(testCases)(
'skips setting up a Provisioning Profile with prior build credentials configured properly in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
}
);
test.each(testCases)(
'sets up a Provisioning Profile with no prior build credentials configured in %s Mode',
async mode => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: mode === 'NON_INTERACTIVE',
appStore: {
...getAppstoreMock(),
defaultAuthenticationMode:
mode === 'NON_INTERACTIVE' ? AuthenticationMode.API_KEY : AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
}
);
it('errors with --freeze-credentials flag', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => [
{
provisioningProfileId: nullthrows(
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0].provisioningProfile
).developerPortalIdentifier,
},
]),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
() => testIosAppBuildCredentialsFragment
),
},
freezeCredentials: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
@@ -68,79 +225,9 @@ describe('SetUpProvisioningProfile', () => {
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
await expect(setupProvisioningProfileAction.runAsync(ctx)).rejects.toThrowError(
ForbidCredentialModificationError
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
//listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
@@ -149,39 +236,8 @@ describe('SetUpProvisioningProfile', () => {
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
it('errors with wrong authentication type in nonInteractive mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
@@ -196,7 +252,7 @@ describe('SetUpProvisioningProfile', () => {
IosDistributionType.AppStore
);
await expect(setupProvisioningProfileAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
InsufficientAuthenticationNonInteractiveError
);
// expect build credentials not to be created or updated on expo servers
@@ -38,6 +38,7 @@ import {
AppleDistributionCertificateFragment,
ApplePushKeyFragment,
AppleTeamFragment,
AppleTeamInput,
CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
@@ -189,28 +190,26 @@ export async function updateIosAppCredentialsAsync(
{
applePushKeyId,
ascApiKeyIdForSubmissions,
ascApiKeyIdForBuilds,
}: {
applePushKeyId?: string;
ascApiKeyIdForSubmissions?: string;
ascApiKeyIdForBuilds?: string;
}
): Promise<CommonIosAppCredentialsFragment> {
let updatedAppCredentials = appCredentials;
if (applePushKeyId) {
updatedAppCredentials = await IosAppCredentialsMutation.setPushKeyAsync(
graphqlClient,
updatedAppCredentials.id,
applePushKeyId
);
const updatedAppCredentials = appCredentials;
if (!applePushKeyId && !ascApiKeyIdForSubmissions && !ascApiKeyIdForBuilds) {
return updatedAppCredentials;
}
if (ascApiKeyIdForSubmissions) {
updatedAppCredentials =
await IosAppCredentialsMutation.setAppStoreConnectApiKeyForSubmissionsAsync(
graphqlClient,
updatedAppCredentials.id,
ascApiKeyIdForSubmissions
);
}
return updatedAppCredentials;
return await IosAppCredentialsMutation.updateIosAppCredentialsAsync(
graphqlClient,
appCredentials.id,
{
pushKeyId: applePushKeyId,
appStoreConnectApiKeyForSubmissionsId: ascApiKeyIdForSubmissions,
appStoreConnectApiKeyForBuildsId: ascApiKeyIdForBuilds,
}
);
}
async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync(
@@ -263,7 +262,42 @@ async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync(
export async function createOrGetExistingAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
account: AccountFragment,
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName?: string }
{ appleTeamIdentifier, appleTeamName, appleTeamType }: AppleTeamInput
): Promise<AppleTeamFragment> {
return await createOrManageExistingAppleTeamAsync(
graphqlClient,
account,
{
appleTeamIdentifier,
appleTeamName,
appleTeamType,
},
{ updateMissingFields: false }
);
}
export async function createOrUpdateExistingAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
account: AccountFragment,
{ appleTeamIdentifier, appleTeamName, appleTeamType }: AppleTeamInput
): Promise<AppleTeamFragment> {
return await createOrManageExistingAppleTeamAsync(
graphqlClient,
account,
{
appleTeamIdentifier,
appleTeamName,
appleTeamType,
},
{ updateMissingFields: true }
);
}
async function createOrManageExistingAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
account: AccountFragment,
{ appleTeamIdentifier, appleTeamName, appleTeamType }: AppleTeamInput,
{ updateMissingFields }: { updateMissingFields: boolean }
): Promise<AppleTeamFragment> {
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
graphqlClient,
@@ -271,11 +305,24 @@ export async function createOrGetExistingAppleTeamAsync(
appleTeamIdentifier
);
if (appleTeam) {
const didTeamNameChange = appleTeamName && appleTeam.appleTeamName !== appleTeamName;
const didTeamTypeChange = appleTeamType && appleTeam.appleTeamType !== appleTeamType;
const didFieldsChange = didTeamNameChange || didTeamTypeChange;
if (updateMissingFields && didFieldsChange) {
return await AppleTeamMutation.updateAppleTeamAsync(
graphqlClient,
{
appleTeamName: didTeamNameChange ? appleTeamName : undefined,
appleTeamType: didTeamTypeChange ? appleTeamType : undefined,
},
appleTeam.id
);
}
return appleTeam;
} else {
return await AppleTeamMutation.createAppleTeamAsync(
graphqlClient,
{ appleTeamIdentifier, appleTeamName },
{ appleTeamIdentifier, appleTeamName, appleTeamType },
account.id
);
}
@@ -326,7 +373,7 @@ export async function getDevicesForAppleTeamAsync(
): Promise<AppleDeviceFragment[]> {
return await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(
graphqlClient,
account.id,
account.name,
appleTeamIdentifier,
{
useCache,
@@ -521,6 +568,20 @@ export async function createAscApiKeyAsync(
);
}
export async function updateAscApiKeyAsync(
graphqlClient: ExpoGraphqlClient,
ascApiKey: AppStoreConnectApiKeyFragment,
appleTeam: AppleTeamFragment
): Promise<AppStoreConnectApiKeyFragment> {
return await AppStoreConnectApiKeyMutation.updateAppStoreConnectApiKeyAsync(
graphqlClient,
{
appleTeamId: appleTeam.id,
},
ascApiKey.id
);
}
export async function getAscApiKeysForAccountAsync(
graphqlClient: ExpoGraphqlClient,
account: AccountFragment
@@ -7,8 +7,10 @@ import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyFragment,
AppStoreConnectApiKeyInput,
AppStoreConnectApiKeyUpdateInput,
CreateAppStoreConnectApiKeyMutation,
DeleteAppStoreConnectApiKeyMutation,
UpdateAppStoreConnectApiKeyMutation,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
@@ -51,6 +53,44 @@ export const AppStoreConnectApiKeyMutation = {
);
return data.appStoreConnectApiKey.createAppStoreConnectApiKey;
},
async updateAppStoreConnectApiKeyAsync(
graphqlClient: ExpoGraphqlClient,
appStoreConnectApiKeyUpdateInput: AppStoreConnectApiKeyUpdateInput,
appStoreConnectApiKeyId: string
): Promise<AppStoreConnectApiKeyFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateAppStoreConnectApiKeyMutation>(
gql`
mutation UpdateAppStoreConnectApiKeyMutation(
$appStoreConnectApiKeyUpdateInput: AppStoreConnectApiKeyUpdateInput!
$id: ID!
) {
appStoreConnectApiKey {
updateAppStoreConnectApiKey(
appStoreConnectApiKeyUpdateInput: $appStoreConnectApiKeyUpdateInput
id: $id
) {
id
...AppStoreConnectApiKeyFragment
}
}
}
${print(AppStoreConnectApiKeyFragmentNode)}
`,
{
appStoreConnectApiKeyUpdateInput,
appStoreConnectApiKeyId,
}
)
.toPromise()
);
assert(
data.appStoreConnectApiKey.updateAppStoreConnectApiKey,
'GraphQL: `updateAppStoreConnectApiKey` not defined in server response'
);
return data.appStoreConnectApiKey.updateAppStoreConnectApiKey;
},
async deleteAppStoreConnectApiKeyAsync(
graphqlClient: ExpoGraphqlClient,
appStoreConnectApiKeyId: string
@@ -7,7 +7,9 @@ import {
AccountFragment,
AppleTeamFragment,
AppleTeamInput,
AppleTeamUpdateInput,
CreateAppleTeamMutation,
UpdateAppleTeamMutation,
} from '../../../../../graphql/generated';
import { AccountFragmentNode } from '../../../../../graphql/types/Account';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
@@ -50,4 +52,40 @@ export const AppleTeamMutation = {
);
return data.appleTeam.createAppleTeam;
},
async updateAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
appleTeamUpdateInput: AppleTeamUpdateInput,
appleTeamId: string
): Promise<AppleTeamMutationResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateAppleTeamMutation>(
gql`
mutation UpdateAppleTeamMutation(
$appleTeamUpdateInput: AppleTeamUpdateInput!
$appleTeamId: ID!
) {
appleTeam {
updateAppleTeam(appleTeamUpdateInput: $appleTeamUpdateInput, id: $appleTeamId) {
id
...AppleTeamFragment
account {
id
...AccountFragment
}
}
}
}
${print(AppleTeamFragmentNode)}
${print(AccountFragmentNode)}
`,
{
appleTeamUpdateInput,
appleTeamId,
}
)
.toPromise()
);
return data.appleTeam.updateAppleTeam;
},
};
@@ -10,6 +10,7 @@ import {
IosAppCredentialsInput,
SetAppStoreConnectApiKeyForSubmissionsMutation,
SetPushKeyMutation,
UpdateIosAppCredentialsMutation,
} from '../../../../../graphql/generated';
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
@@ -56,6 +57,44 @@ export const IosAppCredentialsMutation = {
);
return data.iosAppCredentials.createIosAppCredentials;
},
async updateIosAppCredentialsAsync(
graphqlClient: ExpoGraphqlClient,
iosAppCredentialsId: string,
iosAppCredentialsInput: IosAppCredentialsInput
): Promise<CommonIosAppCredentialsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateIosAppCredentialsMutation>(
gql`
mutation UpdateIosAppCredentialsMutation(
$iosAppCredentialsInput: IosAppCredentialsInput!
$iosAppCredentialsId: ID!
) {
iosAppCredentials {
updateIosAppCredentials(
id: $iosAppCredentialsId
iosAppCredentialsInput: $iosAppCredentialsInput
) {
id
...CommonIosAppCredentialsFragment
}
}
}
${print(CommonIosAppCredentialsFragmentNode)}
`,
{
iosAppCredentialsInput,
iosAppCredentialsId,
}
)
.toPromise()
);
assert(
data.iosAppCredentials.updateIosAppCredentials,
'GraphQL: `updateIosAppCredentials` not defined in server response'
);
return data.iosAppCredentials.updateIosAppCredentials;
},
async setPushKeyAsync(
graphqlClient: ExpoGraphqlClient,
iosAppCredentialsId: string,
@@ -4,27 +4,76 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyByAccountQuery,
AppStoreConnectApiKeyFragment,
AppStoreConnectApiKeysPaginatedByAccountQuery,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const AppStoreConnectApiKeyQuery = {
async getAllForAccountAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<AppStoreConnectApiKeyFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppStoreConnectApiKeyFragment>> => {
return await AppStoreConnectApiKeyQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching ASC Keys...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<
AppStoreConnectApiKeysPaginatedByAccountQuery['account']['byName']['appStoreConnectApiKeysPaginated']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppStoreConnectApiKeyByAccountQuery>(
.query<AppStoreConnectApiKeysPaginatedByAccountQuery>(
gql`
query AppStoreConnectApiKeyByAccountQuery($accountName: String!) {
query AppStoreConnectApiKeysPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
account {
byName(accountName: $accountName) {
id
appStoreConnectApiKeys {
id
...AppStoreConnectApiKeyFragment
appStoreConnectApiKeysPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...AppStoreConnectApiKeyFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
@@ -33,6 +82,10 @@ export const AppStoreConnectApiKeyQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['AppStoreConnectApiKey'],
@@ -40,6 +93,6 @@ export const AppStoreConnectApiKeyQuery = {
)
.toPromise()
);
return data.account.byName.appStoreConnectApiKeys;
return data.account.byName.appStoreConnectApiKeysPaginated;
},
};
@@ -1,4 +1,3 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
@@ -7,15 +6,16 @@ import { DeviceNotFoundError } from '../../../../../devices/utils/errors';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppleDevice,
AppleDeviceFilterInput,
AppleDeviceFragment,
AppleDevicesByAppleTeamQuery,
AppleDevicesByIdentifierQuery,
AppleDevicesByTeamIdentifierQuery,
AppleDevicesByTeamIdentifierQueryVariables,
AppleDevicesPaginatedByAccountQuery,
AppleTeamFragment,
} from '../../../../../graphql/generated';
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
import { formatAppleTeam } from '../../../actions/AppleTeamFormatting';
export type AppleDeviceFragmentWithAppleTeam = AppleDeviceFragment & {
@@ -38,51 +38,25 @@ export type AppleDevicesByIdentifierQueryResult = AppleDeviceQueryResult & {
export const AppleDeviceQuery = {
async getAllByAppleTeamIdentifierAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string,
accountName: string,
appleTeamIdentifier: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<AppleDeviceFragmentWithAppleTeam[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesByAppleTeamQuery>(
gql`
query AppleDevicesByAppleTeamQuery($accountId: ID!, $appleTeamIdentifier: String!) {
appleTeam {
byAppleTeamIdentifier(accountId: $accountId, identifier: $appleTeamIdentifier) {
id
...AppleTeamFragment
appleDevices {
id
...AppleDeviceFragment
appleTeam {
id
...AppleTeamFragment
}
}
}
}
}
${print(AppleTeamFragmentNode)}
${print(AppleDeviceFragmentNode)}
`,
{
accountId,
appleTeamIdentifier,
},
{
additionalTypenames: ['AppleDevice'],
requestPolicy: useCache ? 'cache-first' : 'network-only',
}
)
.toPromise()
);
assert(
data.appleTeam.byAppleTeamIdentifier,
'byAppleTeamIdentifier should be defined in this context - enforced by GraphQL'
);
const { appleDevices } = data.appleTeam.byAppleTeamIdentifier;
assert(appleDevices, 'Apple Devices should be defined in this context - enforced by GraphQL');
return appleDevices;
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppleDeviceFragmentWithAppleTeam>> => {
return await AppleDeviceQuery.getAllForAccountPaginatedAsync(graphqlClient, accountName, {
...relayArgs,
filter: {
appleTeamIdentifier,
},
useCache,
});
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'Fetching Apple devices...',
});
},
async getAllForAppleTeamAsync(
@@ -113,6 +87,7 @@ export const AppleDeviceQuery = {
deviceClass
enabled
model
createdAt
}
}
}
@@ -138,41 +113,21 @@ export const AppleDeviceQuery = {
graphqlClient: ExpoGraphqlClient,
accountName: string,
identifier: string
): Promise<AppleDevicesByIdentifierQueryResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesByIdentifierQuery>(
gql`
query AppleDevicesByIdentifier($accountName: String!, $identifier: String!) {
account {
byName(accountName: $accountName) {
id
appleDevices(identifier: $identifier) {
id
model
identifier
name
deviceClass
enabled
appleTeam {
id
appleTeamIdentifier
appleTeamName
}
}
}
}
}
`,
{ accountName, identifier },
{
additionalTypenames: ['AppleDevice', 'AppleTeam'],
}
)
.toPromise()
);
const device = data.account.byName.appleDevices[0];
): Promise<AppleDeviceFragmentWithAppleTeam> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppleDeviceFragmentWithAppleTeam>> => {
return await AppleDeviceQuery.getAllForAccountPaginatedAsync(graphqlClient, accountName, {
...relayArgs,
filter: {
identifier,
},
});
};
const devices = await fetchEntireDatasetAsync({
paginatedGetterAsync,
});
const device = devices[0];
if (!device) {
throw new DeviceNotFoundError(
`Device with id ${identifier} was not found on account ${accountName}.`
@@ -180,4 +135,86 @@ export const AppleDeviceQuery = {
}
return device;
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
filter,
useCache = true,
}: {
after?: string;
first?: number;
before?: string;
last?: number;
filter?: AppleDeviceFilterInput;
useCache?: boolean;
}
): Promise<AppleDevicesPaginatedByAccountQuery['account']['byName']['appleDevicesPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDevicesPaginatedByAccountQuery>(
gql`
query AppleDevicesPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
$filter: AppleDeviceFilterInput
) {
account {
byName(accountName: $accountName) {
id
appleDevicesPaginated(
after: $after
first: $first
before: $before
last: $last
filter: $filter
) {
edges {
cursor
node {
id
...AppleDeviceFragment
appleTeam {
id
...AppleTeamFragment
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
}
${print(AppleTeamFragmentNode)}
${print(AppleDeviceFragmentNode)}
`,
{
accountName,
after,
first,
before,
last,
filter,
},
{
additionalTypenames: ['AppleDevice'],
requestPolicy: useCache ? 'cache-first' : 'network-only',
}
)
.toPromise()
);
return data.account.byName.appleDevicesPaginated;
},
};
@@ -5,13 +5,14 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppleDistributionCertificateByAccountQuery,
AppleDistributionCertificateByAppQuery,
AppleDistributionCertificateFragment,
AppleDistributionCertificatesPaginatedByAccountQuery,
IosDistributionType,
} from '../../../../../graphql/generated';
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const AppleDistributionCertificateQuery = {
async getForAppAsync(
@@ -81,17 +82,65 @@ export const AppleDistributionCertificateQuery = {
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<AppleDistributionCertificateFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<AppleDistributionCertificateFragment>> => {
return await AppleDistributionCertificateQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching Apple Distribution Certificates...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<
AppleDistributionCertificatesPaginatedByAccountQuery['account']['byName']['appleDistributionCertificatesPaginated']
> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDistributionCertificateByAccountQuery>(
.query<AppleDistributionCertificatesPaginatedByAccountQuery>(
gql`
query AppleDistributionCertificateByAccountQuery($accountName: String!) {
query AppleDistributionCertificatesPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
account {
byName(accountName: $accountName) {
id
appleDistributionCertificates {
id
...AppleDistributionCertificateFragment
appleDistributionCertificatesPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...AppleDistributionCertificateFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
@@ -100,6 +149,10 @@ export const AppleDistributionCertificateQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['AppleDistributionCertificate'],
@@ -107,6 +160,6 @@ export const AppleDistributionCertificateQuery = {
)
.toPromise()
);
return data.account.byName.appleDistributionCertificates;
return data.account.byName.appleDistributionCertificatesPaginated;
},
};
@@ -3,25 +3,75 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import { ApplePushKeyByAccountQuery, ApplePushKeyFragment } from '../../../../../graphql/generated';
import {
ApplePushKeyFragment,
ApplePushKeysPaginatedByAccountQuery,
} from '../../../../../graphql/generated';
import { ApplePushKeyFragmentNode } from '../../../../../graphql/types/credentials/ApplePushKey';
import { Connection, QueryParams, fetchEntireDatasetAsync } from '../../../../../utils/relay';
export const ApplePushKeyQuery = {
async getAllForAccountAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string
): Promise<ApplePushKeyFragment[]> {
const paginatedGetterAsync = async (
relayArgs: QueryParams
): Promise<Connection<ApplePushKeyFragment>> => {
return await ApplePushKeyQuery.getAllForAccountPaginatedAsync(
graphqlClient,
accountName,
relayArgs
);
};
return await fetchEntireDatasetAsync({
paginatedGetterAsync,
progressBarLabel: 'fetching Apple Push Keys...',
});
},
async getAllForAccountPaginatedAsync(
graphqlClient: ExpoGraphqlClient,
accountName: string,
{
after,
first,
before,
last,
}: { after?: string; first?: number; before?: string; last?: number }
): Promise<ApplePushKeysPaginatedByAccountQuery['account']['byName']['applePushKeysPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ApplePushKeyByAccountQuery>(
.query<ApplePushKeysPaginatedByAccountQuery>(
gql`
query ApplePushKeyByAccountQuery($accountName: String!) {
query ApplePushKeysPaginatedByAccountQuery(
$accountName: String!
$after: String
$first: Int
$before: String
$last: Int
) {
account {
byName(accountName: $accountName) {
id
applePushKeys {
id
...ApplePushKeyFragment
applePushKeysPaginated(
after: $after
first: $first
before: $before
last: $last
) {
edges {
cursor
node {
id
...ApplePushKeyFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
@@ -30,6 +80,10 @@ export const ApplePushKeyQuery = {
`,
{
accountName,
after,
first,
before,
last,
},
{
additionalTypenames: ['ApplePushKey'],
@@ -37,6 +91,6 @@ export const ApplePushKeyQuery = {
)
.toPromise()
);
return data.account.byName.applePushKeys;
return data.account.byName.applePushKeysPaginated;
},
};
@@ -1,4 +1,4 @@
import { ProfileType } from '@expo/app-store';
import { ProfileType, UserRole } from '@expo/app-store';
import {
AscApiKey,
@@ -45,16 +45,23 @@ import { createOrReuseAdhocProvisioningProfileAsync } from './provisioningProfil
import { createPushKeyAsync, listPushKeysAsync, revokePushKeyAsync } from './pushKey';
import { hasAscEnvVars } from './resolveCredentials';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { AppleTeamType } from '../../../graphql/generated';
import Log from '../../../log';
import { MinimalAscApiKey } from '../credentials';
type AscApiKeyAuth = {
ascApiKey: MinimalAscApiKey;
appleTeam: { teamIdentifier: string; teamName?: string; teamType: AppleTeamType };
};
export default class AppStoreApi {
public authCtx?: AuthCtx;
public defaultAuthenticationMode: AuthenticationMode;
public readonly ascApiKeyAuth?: AscApiKeyAuth;
constructor() {
this.defaultAuthenticationMode = hasAscEnvVars()
? AuthenticationMode.API_KEY
: AuthenticationMode.USER;
constructor(options: { ascApiKeyAuth?: AscApiKeyAuth } = {}) {
this.ascApiKeyAuth = options.ascApiKeyAuth;
this.defaultAuthenticationMode =
hasAscEnvVars() || this.ascApiKeyAuth ? AuthenticationMode.API_KEY : AuthenticationMode.USER;
}
public async ensureUserAuthenticatedAsync(options?: AuthenticateOptions): Promise<UserAuthCtx> {
@@ -195,10 +202,10 @@ export default class AppStoreApi {
public async createAscApiKeyAsync(
analytics: Analytics,
{ nickname }: { nickname: string }
{ nickname, roles }: { nickname: string; roles: UserRole[] }
): Promise<AscApiKey> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await createAscApiKeyAsync(analytics, userCtx, { nickname });
return await createAscApiKeyAsync(analytics, userCtx, { nickname, roles });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
@@ -9,6 +9,7 @@ export enum AuthenticationMode {
USER,
}
// TODO: move this to graphql types
export enum AppleTeamType {
IN_HOUSE = 'IN_HOUSE',
COMPANY_OR_ORGANIZATION = 'COMPANY_OR_ORGANIZATION',
@@ -1,54 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`print credentials prints the IosAppCredentials map 1`] = `
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
Push Key
Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Connect API Key
Developer Portal ID NYN468XNN3
Name Expo Submissions pl94k8_lnS
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
Roles ADMIN
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
"
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
Push Key
Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE
Updated 6 months ago
EAS Submit App Store Connect API Key
Developer Portal ID NYN468XNN3
Name Expo Submissions pl94k8_lnS
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
Roles ADMIN
Apple Team 77KQ969CHE
Updated 6 months ago
App Store Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
Ad Hoc Configuration
Distribution Certificate
Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 6 months ago
Provisioning Profile
Developer Portal ID test-profile-id
Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
"
`;
@@ -12,6 +12,7 @@ import { APPLE_DEVICE_CLASS_LABELS } from '../../../graphql/types/credentials/Ap
import Log from '../../../log';
import { fromNow } from '../../../utils/date';
import formatFields from '../../../utils/formatFields';
import { AppStoreApiKeyPurpose } from '../actions/AscApiKeyUtils';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { App, IosAppBuildCredentialsMap, IosAppCredentialsMap, Target } from '../types';
@@ -92,7 +93,12 @@ export function displayIosCredentials(
continue;
}
const { appleTeam, pushKey, appStoreConnectApiKeyForSubmissions } = targetAppCredentials;
const {
appleTeam,
pushKey,
appStoreConnectApiKeyForSubmissions,
appStoreConnectApiKeyForBuilds,
} = targetAppCredentials;
if (appleTeam) {
const { appleTeamIdentifier, appleTeamName } = appleTeam;
fields.push({
@@ -107,7 +113,15 @@ export function displayIosCredentials(
}
if (appStoreConnectApiKeyForSubmissions) {
displayAscApiKey(appStoreConnectApiKeyForSubmissions, fields);
displayAscApiKey(
AppStoreApiKeyPurpose.SUBMISSION_SERVICE,
appStoreConnectApiKeyForSubmissions,
fields
);
}
if (appStoreConnectApiKeyForBuilds) {
displayAscApiKey(AppStoreApiKeyPurpose.BUILD_SERVICE, appStoreConnectApiKeyForBuilds, fields);
}
const sortedIosAppBuildCredentialsList = sortBuildCredentialsByDistributionType(
@@ -236,10 +250,11 @@ function displayApplePushKey(
}
function displayAscApiKey(
purpose: AppStoreApiKeyPurpose,
maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
fields: { label: string; value: string }[]
): void {
fields.push({ label: 'App Store Connect API Key', value: '' });
fields.push({ label: `${purpose.toString()} App Store Connect API Key`, value: '' });
if (maybeAscApiKey) {
const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey;
fields.push({ label: 'Developer Portal ID', value: keyIdentifier });
@@ -29,12 +29,12 @@ export enum AndroidActionType {
UseExistingGsaKeyForSubmissions,
RemoveGsaKey,
SetUpGsaKeyForSubmissions,
CreateGsaKeyForFcmV1,
UseExistingGsaKeyForFcmV1,
RemoveGsaKeyForFcmV1,
SetUpGsaKeyForFcmV1,
UpdateCredentialsJson,
SetUpBuildCredentialsFromCredentialsJson,
SetUpBuildCredentials,
}
export enum IosActionType {
@@ -55,8 +55,8 @@ export enum IosActionType {
CreatePushKey,
UseExistingPushKey,
RemovePushKey,
SetUpAscApiKeyForSubmissions,
UseExistingAscApiKeyForSubmissions,
CreateAscApiKeyForSubmissions,
SetUpAscApiKeyForService,
CreateAscApiKeyForService,
UseExistingAscApiKeyForService,
RemoveAscApiKey,
}
@@ -0,0 +1,46 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import Log from '../../log';
export class CheckBuildProfileFlagAgainstEasJson<T extends Platform> {
private easJsonAccessor: EasJsonAccessor;
constructor(
projectDir: string,
private platform: T,
private profileNameFromFlag: string
) {
this.easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
}
async runAsync(): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync();
const easConfig = await EasJsonUtils.getBuildProfileAsync<T>(
this.easJsonAccessor,
this.platform,
profileName
);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
async getProfileNameFromEasConfigAsync(): Promise<string> {
const buildProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(this.easJsonAccessor);
if (buildProfileNames.length === 0) {
throw new Error(
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details'
);
} else if (buildProfileNames.length === 1) {
return buildProfileNames[0];
}
if (buildProfileNames.includes(this.profileNameFromFlag)) {
return this.profileNameFromFlag;
} else {
throw new Error(
`Build profile ${this.profileNameFromFlag} does not exist in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details`
);
}
}
}
@@ -77,22 +77,22 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
];
}
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
export function getAscApiKeyActions(): ActionInfo[] {
return [
{
value: IosActionType.SetUpAscApiKeyForSubmissions,
title: 'Set up your project to use an API Key for EAS Submit',
value: IosActionType.SetUpAscApiKeyForService,
title: `Set up your project to use an API Key`,
scope: Scope.Project,
},
{
value: IosActionType.UseExistingAscApiKeyForSubmissions,
title: 'Use an existing API Key for EAS Submit',
value: IosActionType.UseExistingAscApiKeyForService,
title: `Use an existing API Key in your project`,
scope: Scope.Project,
},
{
value: IosActionType.CreateAscApiKeyForSubmissions,
title: 'Add a new API Key For EAS Submit',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
value: IosActionType.CreateAscApiKeyForService,
title: `Add a new API Key to use in your project`,
scope: Scope.Project,
},
{
value: IosActionType.RemoveAscApiKey,
@@ -34,6 +34,7 @@ import { DownloadKeystore } from '../android/actions/DownloadKeystore';
import { RemoveFcm } from '../android/actions/RemoveFcm';
import { SelectAndRemoveGoogleServiceAccountKey } from '../android/actions/RemoveGoogleServiceAccountKey';
import { RemoveKeystore } from '../android/actions/RemoveKeystore';
import { SetUpBuildCredentials } from '../android/actions/SetUpBuildCredentials';
import { SetUpBuildCredentialsFromCredentialsJson } from '../android/actions/SetUpBuildCredentialsFromCredentialsJson';
import { SetUpGoogleServiceAccountKeyForFcmV1 } from '../android/actions/SetUpGoogleServiceAccountKeyForFcmV1';
import { SetUpGoogleServiceAccountKeyForSubmissions } from '../android/actions/SetUpGoogleServiceAccountKeyForSubmissions';
@@ -48,8 +49,8 @@ import { AndroidPackageNotDefinedError } from '../errors';
export class ManageAndroid {
constructor(
private callingAction: Action,
private projectDir: string
protected callingAction: Action,
protected projectDir: string
) {}
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
@@ -164,7 +165,7 @@ export class ManageAndroid {
}
}
private async createProjectContextAsync(
protected async createProjectContextAsync(
ctx: CredentialsContext,
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<GradleBuildContext | undefined> {
@@ -172,7 +173,7 @@ export class ManageAndroid {
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile, ctx.vcsClient);
}
private async runProjectSpecificActionAsync(
protected async runProjectSpecificActionAsync(
ctx: CredentialsContext,
action: AndroidActionType,
gradleContext?: GradleBuildContext
@@ -239,6 +240,8 @@ export class ManageAndroid {
}
} else if (action === AndroidActionType.SetUpBuildCredentialsFromCredentialsJson) {
await new SetUpBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.SetUpBuildCredentials) {
await new SetUpBuildCredentials({ app: appLookupParams }).runAsync(ctx);
}
}
}
@@ -56,8 +56,8 @@ import { displayIosCredentials } from '../ios/utils/printCredentials';
export class ManageIos {
constructor(
private callingAction: Action,
private projectDir: string
protected callingAction: Action,
protected projectDir: string
) {}
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
@@ -85,7 +85,7 @@ export class ManageIos {
});
const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions();
await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -182,7 +182,7 @@ export class ManageIos {
}
}
private async createProjectContextAsync(
protected async createProjectContextAsync(
ctx: CredentialsContext,
account: AccountFragment,
buildProfile: BuildProfile<Platform.IOS>
@@ -215,7 +215,7 @@ export class ManageIos {
};
}
private async runAccountSpecificActionAsync(
protected async runAccountSpecificActionAsync(
ctx: CredentialsContext,
account: AccountFragment,
action: IosActionType
@@ -226,8 +226,6 @@ export class ManageIos {
await new CreateDistributionCertificate(account).runAsync(ctx);
} else if (action === IosActionType.CreatePushKey) {
await new CreatePushKey(account).runAsync(ctx);
} else if (action === IosActionType.CreateAscApiKeyForSubmissions) {
await new CreateAscApiKey(account).runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
} else if (action === IosActionType.RemovePushKey) {
await new SelectAndRemovePushKey(account).runAsync(ctx);
} else if (action === IosActionType.RemoveAscApiKey) {
@@ -235,7 +233,7 @@ export class ManageIos {
}
}
private async runProjectSpecificActionAsync(
protected async runProjectSpecificActionAsync(
ctx: CredentialsContext,
app: App,
targets: Target[],
@@ -354,40 +352,29 @@ export class ManageIos {
}
return;
}
case IosActionType.SetUpAscApiKeyForSubmissions: {
await new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
).runAsync(ctx);
case IosActionType.SetUpAscApiKeyForService: {
const purpose = await this.selectAscApiKeyPurposeAsync();
await new SetUpAscApiKey(appLookupParams, purpose).runAsync(ctx);
return;
}
case IosActionType.UseExistingAscApiKeyForSubmissions: {
case IosActionType.UseExistingAscApiKeyForService: {
const purpose = await this.selectAscApiKeyPurposeAsync();
const ascApiKey = await selectAscApiKeysFromAccountAsync(ctx, appLookupParams.account, {
filterDifferentAppleTeam: true,
});
if (ascApiKey) {
await new AssignAscApiKey(appLookupParams).runAsync(
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await new AssignAscApiKey(appLookupParams).runAsync(ctx, ascApiKey, purpose);
}
return;
}
case IosActionType.CreateAscApiKeyForSubmissions: {
const ascApiKey = await new CreateAscApiKey(appLookupParams.account).runAsync(
ctx,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
case IosActionType.CreateAscApiKeyForService: {
const purpose = await this.selectAscApiKeyPurposeAsync();
const ascApiKey = await new CreateAscApiKey(appLookupParams.account).runAsync(ctx, purpose);
const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new API Key?`,
});
if (confirm) {
await new AssignAscApiKey(appLookupParams).runAsync(
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await new AssignAscApiKey(appLookupParams).runAsync(ctx, ascApiKey, purpose);
}
return;
}
@@ -396,7 +383,7 @@ export class ManageIos {
}
}
private async setupProvisioningProfileWithSpecificDistCertAsync(
protected async setupProvisioningProfileWithSpecificDistCertAsync(
ctx: CredentialsContext,
target: Target,
appLookupParams: AppLookupParams,
@@ -419,7 +406,7 @@ export class ManageIos {
}
}
private async selectTargetAsync(targets: Target[]): Promise<Target> {
protected async selectTargetAsync(targets: Target[]): Promise<Target> {
if (targets.length === 1) {
return targets[0];
}
@@ -431,4 +418,17 @@ export class ManageIos {
}))
);
}
protected async selectAscApiKeyPurposeAsync(): Promise<AppStoreApiKeyPurpose> {
const { keyPurpose } = await promptAsync({
type: 'select',
message: `Select the service you'd like to use your ASC Api Key for:`,
name: 'keyPurpose',
choices: [
{ title: 'EAS Submit', value: AppStoreApiKeyPurpose.SUBMISSION_SERVICE },
{ title: 'EAS Build', value: AppStoreApiKeyPurpose.BUILD_SERVICE },
],
});
return keyPurpose;
}
}
Loaded 100 of 166 files, more files were not shown because too many files have changed in this diff. Show more