Compare commits

...
Author SHA1 Message Date
kbrandwijk dd02e8be4a chore: initial setup for ESM 2022-04-24 20:43:37 +02:00
Expo CI 89dda20149 update CHANGELOG.md 2022-04-19 10:40:33 +00:00
Dominik Sokal b591698c31 v0.51.0 2022-04-19 12:32:52 +02:00
Cedric van Putten 66543b658b [eas-cli] Enable update bundle output when debugging (#1070)
* [eas-cli] Enable update bundle output when debugging

* update CHANGELOG.md
2022-04-19 03:15:36 -07:00
Evan Bacon 4769048578 Bump @expo/[email protected] (#1064)
* Bump @expo/[email protected]

* Update CHANGELOG.md
2022-04-14 11:56:15 -05:00
Will Schurman 64c1e47674 [eas-cli] Add non-interactive flag to eas update command (#1066) 2022-04-14 09:39:42 -07:00
Dominik Sokal be3a86abda submit - add custom error message for expired apple certs problem (#1068)
* submit - add custom error message for expired apple certs problem

* PR feedback

* update CHANGELOG.md
2022-04-14 11:49:07 +02:00
Dominik Sokal eb2ca117fc [ENG-4615] unify google service account prompts (#1063)
* unify google service account prompt

* update CHANGELOG.md

* fix tests
2022-04-14 09:50:39 +02:00
Dominik Sokal 5043385077 fix expo-cli reference 2022-04-12 11:54:56 +02:00
Will Schurman bb4c08a52a [eas-cli] Require private-key-path to be specified for updating when code signing configured (#1059) 2022-04-11 10:42:31 -07:00
Expo CI 8b75057fa1 update CHANGELOG.md 2022-04-11 08:27:15 +00:00
Dominik Sokal 80cc849137 v0.50.0 2022-04-11 10:20:16 +02:00
Dominik SokalandBrent Vatne 2d407ea9d4 [ENG-4566] refactor status updates for multi-platform builds (#1058)
* refactor status updates for multi-platform builds

* update CHANGELOG

* Update packages/eas-cli/src/build/build.ts

Co-authored-by: Brent Vatne <[email protected]>

Co-authored-by: Brent Vatne <[email protected]>
2022-04-11 10:07:20 +02:00
Quinlan Jung 91456d1c65 [eas-cli] require userAuthCtx for methods that don't support jwt (#1055)
* [eas-cli] require userAuthCtx for methods that don't support jwt

* changelog
2022-04-08 10:49:19 -07:00
Quinlan Jung 757a0ccad7 fix changelog 2022-04-07 16:38:48 -07:00
4b7cac0392 [ENG-4419][eas-cli] use asc api token to authenticate for EAS build (#1051)
* [eas-cli] enable authentication with asc api key

* Update packages/eas-cli/src/build/createContext.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-cli/src/credentials/ios/appstore/authenticate.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-cli/src/credentials/ios/appstore/authenticate.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-cli/src/credentials/ios/appstore/authenticate.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-cli/src/credentials/ios/appstore/authenticate.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-json/src/build/types.ts

Co-authored-by: Evan Bacon <[email protected]>

* Update packages/eas-json/src/build/types.ts

Co-authored-by: Evan Bacon <[email protected]>

* pr feedback

* Update packages/eas-cli/src/credentials/ios/appstore/ensureAppExists.ts

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/credentials/ios/appstore/authenticate.ts

Co-authored-by: Dominik Sokal <[email protected]>

* pr feedback

* changelog

* return instantiation back to original state

Co-authored-by: Evan Bacon <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2022-04-07 15:54:43 -07:00
Will Schurman 8ff8022b3f [eas-cli] Use @expo/multipart-body-parser library (#1054) 2022-04-07 12:19:15 -07:00
Wojciech Kozyra a2acea9fe9 do not delete assigned issues 2022-04-07 16:00:25 +02:00
Wojciech KozyraandKim Brandwijk 9abdfcfdbe autoclose stale issues (#1053)
* autoclose stale issues

* Update .github/workflows/stale-issues.yml

Co-authored-by: Kim Brandwijk <[email protected]>

Co-authored-by: Kim Brandwijk <[email protected]>
2022-04-07 15:51:09 +02:00
Dominik Sokal 696f9dde13 [ENG-3864] provide queue position and estimated wait time (#1049)
* update graphql

* show queue position progress + estimated wait time

* refactor

* update CHANGELOG.md

* hide estimated wait time when build is in progress

* add priority queue info
2022-04-06 13:46:06 +02:00
Expo CI 1e73c8c96f update CHANGELOG.md 2022-04-05 10:48:52 +00:00
Wojciech Kozyra 1b02a67d88 v0.49.0 2022-04-05 12:25:49 +02:00
Wojciech Kozyra d0f158a546 upgrade eas-cli-local-build-plugin to 0.0.74 2022-04-05 12:25:02 +02:00
Evan Bacon 16a2dc681b [eas-cli] Added experimental ASC provisioning profile regeneration (#1038)
* Added experimental provisioning profile regeneration

* Update CHANGELOG.md

* use utility method
2022-04-04 14:36:05 -05:00
Wojciech Kozyra 73800252ae [eas-cli] generate credentials for iOS app extensions in managed projects (#1039)
* [eas-cli] generate credentials for iOS app extensions

* update CHANGELOG.md
2022-04-04 14:40:02 +02:00
Wojciech Kozyra e7c4ba1f54 [eas-cli] make deprecation message more generic (#1047)
* [eas-cli] make deprecation message more generic

* update CHANGELOG.md
2022-04-01 14:43:28 +02:00
Dominik Sokal 84cddaf0e1 upgrade dependencies (#1043)
* upgrade dependencies

* update CHANGELOG.md
2022-04-01 12:49:20 +02:00
Wojciech Kozyra 896f7f0385 [eas-cli] fix proxy support (#1032)
* [eas-cli] fix proxy support

* update CHANGELOG.md
2022-03-25 14:47:02 +01:00
Kim Brandwijk 7f31ea6a2d chore: update bug report issue template label (#1028) 2022-03-24 22:03:13 +01:00
Evan Bacon 3ef397f96c chore: add annotations about functions that do not support App Store Connect API (#1029)
* chore: add annotations about functions that do not support App Store Connect API

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update capabilityIdentifiers.ts
2022-03-24 12:09:37 -05:00
Expo CI 680a684a8e update CHANGELOG.md 2022-03-21 11:27:13 +00:00
Wojciech Kozyra 1eae0c75ee v0.48.2 2022-03-21 12:19:03 +01:00
Wojciech Kozyra 483e0f2dca upgrade eas-cli-local-build-plugin to 0.0.67 2022-03-21 12:18:51 +01:00
Wojciech Kozyra 6ee843e41c [eas-cli] proxy support (#1005)
* [eas-cli] proxy support

* update CHANGELOG.md

* review feedback
2022-03-21 12:14:15 +01:00
Wojciech Kozyra 5402ebdc5c [eas-cli] do not retry build requests (#1024)
* do not retry build requests

* update CHANGELOG.md
2022-03-18 13:03:25 +01:00
Giau. Tran Minh 1b5119d818 fix: Add project slug to the BuildFragment (#1021)
* fix: Add project slug to the BuildFragment

* chore: update CHANGELOG

* fix: format URL using URL object
2022-03-16 11:47:35 +01:00
Expo CI 8a82365680 update CHANGELOG.md 2022-03-15 22:30:11 +00:00
kbrandwijk 9c55621719 v0.48.1 2022-03-15 23:23:27 +01:00
Wojciech Kozyra 1afc3093ec [eas-cli] Improve apple login prompt for internal distribution builds. (#1016)
* [eas-cli] improve messaging about apple login requirement

* update CHANGELOG.md
2022-03-15 10:48:55 +01:00
Will Schurman 790c3d776e [eas-cli] Fix code signing error when not yet configured (#1018) 2022-03-14 20:28:14 -07:00
Expo CI be84ffd62a update CHANGELOG.md 2022-03-14 13:32:52 +00:00
Dominik Sokal fa812f448f v0.48.0 2022-03-14 14:25:56 +01:00
Dominik Sokal d09161b745 upgrade eas-cli-local-build-plugin to 0.0.66 2022-03-14 14:25:41 +01:00
Dominik Sokal 653f26257b upgrade dependencies (#1015)
* upgrade dependencies

* [eas-cli] regenerate GraphQL code

* update CHANGELOG.md
2022-03-14 13:26:06 +01:00
Wojciech Kozyra b1e874966c [eas-cli] Fix build status when build is canceled (#1012)
* [eas-cli] Fix build status when build is canceled

* update CHANGELOG.md
2022-03-14 11:04:54 +01:00
Jérémy Barbet 315abf0a03 docs: both platforms can be used (#1013) 2022-03-14 11:04:41 +01:00
Wojciech Kozyra 89a9255929 [eas-cli] Fix login request + update CHANGELOG (#1004) 2022-03-09 14:51:17 +01:00
Wojciech Kozyra cae0ba21eb [eas-cli] Fix code signing error on unconfigured eas project (#1002) 2022-03-08 16:32:40 +01:00
Wojciech KozyraandKim Brandwijk ed52e291cb [eas-cli] replace got with node-fetch (#1000)
* [eas-cli] replace got with node-fetch

* Update packages/eas-cli/src/api.ts

Co-authored-by: Kim Brandwijk <[email protected]>

* review feedback

* convert ApiV2 class into plain object

Co-authored-by: Kim Brandwijk <[email protected]>
2022-03-08 15:24:03 +01:00
dependabot[bot] 6db7ac617b Bump plist from 3.0.1 to 3.0.4 (#996)
Bumps [plist](https://github.com/TooTallNate/node-plist) from 3.0.1 to 3.0.4.
- [Release notes](https://github.com/TooTallNate/node-plist/releases)
- [Changelog](https://github.com/TooTallNate/plist.js/blob/master/History.md)
- [Commits](https://github.com/TooTallNate/node-plist/commits)

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

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-04 15:41:49 +01:00
Wojciech KozyraandDominik Sokal 1dd9614dbe [eas-cli] remove unused flag (#995)
* [eas-cli] remove unused flag

* update CHANGELOG.md

* Update packages/eas-cli/src/commands/build/index.ts

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2022-03-01 11:54:10 +01:00
Kim Brandwijk 67c58a75d9 [chore] update issue template for expo-env-info (#986) 2022-02-27 10:19:44 +01:00
jkh 5b3e4cb3e1 [eas-cli] make eas update:configure work with native files (#978) 2022-02-21 14:50:57 -05:00
dependabot[bot] caf257ce9c Bump follow-redirects from 1.14.7 to 1.14.8 (#976)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
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>
2022-02-21 12:08:28 +01:00
Kim Brandwijk 31122623fa docs: changelog typo 2022-02-21 10:07:14 +01:00
kbrandwijk eb36dd0fef chore: linting fix 2022-02-17 21:14:09 +01:00
Kim Brandwijk bb0723354f [update] make runtime version warning more descriptive (#979) 2022-02-17 21:09:52 +01:00
jkh 1b9feed040 [eas-cli] install expo-updates when running update:configure (#977) 2022-02-15 16:15:55 -05:00
Will Schurman 0220ca17a6 [eas-cli] Update @expo/config-types and related packages (#974) 2022-02-11 13:54:49 -08:00
Will Schurman 74cec12c23 [eas-cli] Add code signing (#964) 2022-02-11 12:48:33 -08:00
Wojciech Kozyra e807fc105d [eas-cli] check if package.json is inside git repo (#971)
* [eas-cli] check if package.json is inside git repo

* update CHANGELOG.md

* fix try catch
2022-02-11 14:50:51 +01:00
jkh ae30c4c7db [eas-cli] add tsconfig unused var rules (#961) 2022-02-09 15:50:25 -05:00
jkh 35b9fda122 [eas-cli] import getRuntimeVersionNullabe from config-plugins (#962) 2022-02-09 08:38:00 -05:00
jkh 8d010ad7e8 [eas-cli] remove unnecessary casting (#963) 2022-02-09 08:37:51 -05:00
Expo CI d7181478c8 update CHANGELOG.md 2022-02-08 11:15:55 +00:00
Dominik Sokal 70ed1a482a v0.47.0 2022-02-08 12:05:54 +01:00
Dominik Sokal 920e5ee8a2 upgrade deps (#946)
* upgrade deps

* update CHANGELOG.md
2022-02-08 11:42:25 +01:00
Dominik Sokal f334f2c019 update CHANGELOG.md 2022-02-08 11:31:33 +01:00
jkh b145a7d98c [eas-cli] assert expo-updates is installed before a publish (#953) 2022-02-07 10:06:01 -05:00
jkh 3c750b60c0 [eas-cli] update error message to new publish command (#952) 2022-02-03 10:44:09 -05:00
jkh b3a9f1af70 [eas-cli] return an informative error if rollout channel dne (#930) 2022-01-28 11:18:34 -05:00
jkh 39d8ab65d1 [eas-cli] fix get branchByNameAsync typing (#929) 2022-01-26 11:23:26 -05:00
Expo CI 1ff2274d8d update CHANGELOG.md 2022-01-26 09:43:42 +00:00
Dominik Sokal 5ed4b65133 v0.46.0 2022-01-26 10:33:41 +01:00
Dominik Sokal 10aca5208f fix url placeholder for iOS submits (#941)
* fix url placeholder for iOS submits

* update CHANGELOG.md
2022-01-25 15:26:20 +01:00
dependabot[bot] b2da80d5ea Bump node-fetch from 2.6.1 to 2.6.7 (#940)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-25 15:21:45 +01:00
Dominik Sokal b9457d71f9 upgrade deps (#939)
* upgrade deps

* update CHANGELOG.md
2022-01-25 14:48:02 +01:00
Dominik Sokalandjkh 5fd3149a95 unify command descriptions (#925)
* unify command descriptions

* fix lint warnings

* Apply suggestions from code review

Co-authored-by: jkh <[email protected]>

* address PR feedback

* update CHANGELOG.md

Co-authored-by: jkh <[email protected]>
2022-01-21 12:13:40 +01:00
dependabot[bot] 5f83724833 Bump trim-off-newlines from 1.0.1 to 1.0.3 (#923)
Bumps [trim-off-newlines](https://github.com/stevemao/trim-off-newlines) from 1.0.1 to 1.0.3.
- [Release notes](https://github.com/stevemao/trim-off-newlines/releases)
- [Commits](https://github.com/stevemao/trim-off-newlines/compare/v1.0.1...v1.0.3)

---
updated-dependencies:
- dependency-name: trim-off-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-20 09:56:09 +01:00
Expo CI 7df558d713 update CHANGELOG.md 2022-01-19 15:50:30 +00:00
Wojciech Kozyra 543e8ce26d v0.45.1 2022-01-19 16:31:12 +01:00
Kim Brandwijk 355fada383 add json output for started builds (#921) 2022-01-19 13:48:37 +01:00
Kim Brandwijk b6625a4cba add support for prebuildCommand in eas.json (#919) 2022-01-19 13:27:31 +01:00
Wojciech Kozyra c1ef20afab [eas-cli] add Monterey image (#922)
* [eas-cli] add Monterey image

* update CHANGELOG.md
2022-01-19 12:45:19 +01:00
Expo CI d8321e1f06 update CHANGELOG.md 2022-01-18 12:28:19 +00:00
kbrandwijk d8906e085e v0.45.0 2022-01-18 13:21:25 +01:00
Dominik Sokal 3b823865d3 upgrade dependencies (#872)
* upgrade dependencies

* fix TS error

* regenerate graphql code

* update CHANGELOG.md
2022-01-18 10:33:50 +01:00
dependabot[bot] a29ed19653 Bump follow-redirects from 1.14.3 to 1.14.7 (#912)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.3 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.3...v1.14.7)

---
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>
2022-01-17 10:15:58 +01:00
Dominik Sokal e70ecaa9b5 upgrade axios to 0.24.0 2022-01-17 10:14:13 +01:00
dependabot[bot] 9886fcb3ef Bump shelljs from 0.8.4 to 0.8.5 (#911)
Bumps [shelljs](https://github.com/shelljs/shelljs) from 0.8.4 to 0.8.5.
- [Release notes](https://github.com/shelljs/shelljs/releases)
- [Changelog](https://github.com/shelljs/shelljs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/shelljs/shelljs/compare/v0.8.4...v0.8.5)

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

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-17 10:09:38 +01:00
kbrandwijk 4f5739b2dc docs: update readme 2022-01-13 14:59:48 +01:00
Kim Brandwijk c7a40b06a5 delete device functionality (#890)
* feat: device delete command
2022-01-12 12:31:10 +01:00
Dominik Sokal 3f88dc8690 fix webhook:update description 2022-01-12 11:43:19 +01:00
Expo CI 874db1c125 update CHANGELOG.md 2022-01-11 20:34:32 +00:00
Brent Vatne 91a147b40b v0.44.1 2022-01-11 12:28:58 -08:00
Brent Vatne cc4553b318 [eas-cli] Only configure updates in generic projects (#904) 2022-01-11 11:52:20 -08:00
Dominik Sokal dd2303dbb2 don't use forked @oclif/plugin-autocomplete (#903)
* upgrade oclif deps

* update CHANGELOG.md
2022-01-11 14:28:49 +01:00
dependabot[bot]andDominik Sokal 967b5d9985 Bump node-forge from 0.10.0 to 1.0.0 (#902)
* Bump node-forge from 0.10.0 to 1.0.0

Bumps [node-forge](https://github.com/digitalbazaar/forge) from 0.10.0 to 1.0.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/0.10.0...v1.0.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>

* upgrade @types/node-forge

* update CHANGELOG.md

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dominik Sokal <[email protected]>
2022-01-11 14:27:00 +01:00
Expo CI 8439cd2141 update CHANGELOG.md 2022-01-11 11:45:09 +00:00
Dominik Sokal f46063cca5 v0.44.0 2022-01-11 12:39:39 +01:00
Dominik Sokal c0cfa93b69 upgrade eas-cli-local-build-plugin to 0.0.58 2022-01-11 12:39:30 +01:00
Dominik Sokal 7d6f620f91 Update CHANGELOG.md 2022-01-11 12:38:53 +01:00
Dominik Sokal c973a1f603 refactor build configuration + don't configure Android projects locally (#888)
* remove 'eas build:configure' and refactor project config synchronization

* simplify updates configuration

* remove --skip-project-configuration flag

* add build:configure back

* better ux

* run Android cleanup in build:configure

* address PR feedback

* update CHANGELOG.md
2022-01-11 12:30:39 +01:00
Evan Bacon 1b8f60fad4 Remove APNS capability by default (#797)
* Remove APNS capability by default

* Update bundleIdCapabilities-test.ts

* Update bundleIdCapabilities-test.ts

* Update CHANGELOG.md
2022-01-06 12:45:15 -07:00
Wojciech Kozyra 7a30de9ad7 [eas-cli] do not follow symlinks with fast-glob (#891)
* [eas-cli] do not follow symlinks with fast-glob

* update CHANGELOG.md
2022-01-05 17:50:36 +01:00
jkh 4247b65e09 [eas-cli] add update:list alias (#884) 2022-01-05 10:05:55 -05:00
Dominik Sokal 8c388d5173 Update CHANGELOG.md 2022-01-05 11:12:34 +01:00
Evan Bacon fce945679f Bump @expo/[email protected] (#889)
* Bump @expo/[email protected]

* Update CHANGELOG.md
2022-01-04 15:07:40 -07:00
Wojciech Kozyra 7772e199b4 [eas-cli] Add --output flag to build command (#885)
* [eas-cli] Add --output flag to build command

* update CHANGELOG.md
2022-01-04 10:56:25 +01:00
Dominik Sokal 755e105252 fix README.md 2022-01-03 11:47:59 +01:00
Expo CI 611d0c98ee update CHANGELOG.md 2022-01-03 10:42:36 +00:00
165 changed files with 33123 additions and 29865 deletions

No files matched your search

-4
View File
@@ -26,10 +26,6 @@ module.exports = {
name: 'lodash',
message: "Don't use lodash, it's heavy!",
},
{
name: 'node-fetch',
message: 'Use got instead, node-fetch is used only for the GraphQL client.',
},
],
},
],
+2 -2
View File
@@ -1,5 +1,5 @@
name: "\U0001F41B Bug Report in EAS CLI"
labels: 'bug'
labels: 'needs review'
description: 'You want to report a reproducible bug or regression in EAS CLI.'
body:
- type: markdown
@@ -37,7 +37,7 @@ body:
required: true
attributes:
label: Environment
description: Run `eas diagnostics` and paste the output here.
description: Run `npx expo-env-info` and paste the output here.
- type: textarea
validations:
required: false
+1 -1
View File
@@ -1,5 +1,5 @@
name: "\U0001F64B Enhancement"
labels: 'enhancement'
labels: 'enhancement,needs review'
description: 'Want us to add something to EAS CLI?'
body:
- type: markdown
+24
View File
@@ -0,0 +1,24 @@
name: Close inactive issues
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
ascending: false
operations-per-run: 300
days-before-issue-stale: 30
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem."
days-before-pr-stale: -1
days-before-pr-close: -1
enable-statistics: true
exempt-issue-labels: "needs review"
exempt-all-assignees: true
+170 -2
View File
@@ -8,6 +8,176 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
### 🧹 Chores
## [0.51.0](https://github.com/expo/eas-cli/releases/tag/v0.51.0) - 2022-04-19
### 🎉 New features
- Add non-interactive flag to eas update command. ([#1066](https://github.com/expo/eas-cli/pull/1066) by [@wschurman](https://github.com/wschurman))
- Handle new EAS Submit common error - expired Apple's certificates. ([#1068](https://github.com/expo/eas-cli/pull/1068) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Enable full bundling logging for eas update when debugging. ([#1070](https://github.com/expo/eas-cli/pull/1070) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Upgrade `@expo/[email protected]` (now with license). ([#1064](https://github.com/expo/eas-cli/pull/1064) by [@EvanBacon](https://github.com/EvanBacon))
- Unify Google Service Account key prompts. ([#1063](https://github.com/expo/eas-cli/pull/1063) by [@dsokal](https://github.com/dsokal))
## [0.50.0](https://github.com/expo/eas-cli/releases/tag/v0.50.0) - 2022-04-11
### 🎉 New features
- Provide queue position progress and estimated wait time. ([#1049](https://github.com/expo/eas-cli/pull/1049), [#1058](https://github.com/expo/eas-cli/pull/1058) by [@dsokal](https://github.com/dsokal))
- Enable App Store authentication for ASC Api Keys with environment variables. ([#1051](https://github.com/expo/eas-cli/pull/1051) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Prompt for user/password authentication where required. ([#1055](https://github.com/expo/eas-cli/pull/1055) by [@quinlanj](https://github.com/quinlanj))
- Require private-key-path to be specified for updating when code signing configured. ([#1059](https://github.com/expo/eas-cli/pull/1059) by [@wschurman](https://github.com/wschurman))
## [0.49.0](https://github.com/expo/eas-cli/releases/tag/v0.49.0) - 2022-04-05
### 🎉 New features
- Added experimental App Store Connect API provisioning profile regeneration. ([#1038](https://github.com/expo/eas-cli/pull/1038) by [@EvanBacon](https://github.com/EvanBacon))
- Experimental support for iOS App Extensions in managed projects. ([#1039](https://github.com/expo/eas-cli/pull/1039) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix proxy support. ([#1032](https://github.com/expo/eas-cli/pull/1032) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add annotations regarding App Store Connect Token session support. ([#1029](https://github.com/expo/eas-cli/pull/1029) by [@EvanBacon](https://github.com/EvanBacon))
- Upgrade dependencies. ([#1043](https://github.com/expo/eas-cli/pull/1043) by [@dsokal](https://github.com/dsokal))
- Make default deprecation message more generic. ([#1047](https://github.com/expo/eas-cli/pull/1047) by [@wkozyra95](https://github.com/wkozyra95))
## [0.48.2](https://github.com/expo/eas-cli/releases/tag/v0.48.2) - 2022-03-21
### 🎉 New features
- Add proxy support. ([#1005](https://github.com/expo/eas-cli/pull/1005) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Do not retry build requests. ([#1024](https://github.com/expo/eas-cli/pull/1024) by [@wkozyra95](https://github.com/wkozyra95))
## [0.48.1](https://github.com/expo/eas-cli/releases/tag/v0.48.1) - 2022-03-15
### 🐛 Bug fixes
- Fix code signing error when not yet configured. ([#1018](https://github.com/expo/eas-cli/pull/1018) by [@wschurman](https://github.com/wschurman))
- Fix link to build details page to use `project.slug`. ([#1021](https://github.com/expo/eas-cli/pull/1021) by [@giautm](https://github.com/giautm))
### 🧹 Chores
- Improve apple login prompt for internal distribution builds. ([#1016](https://github.com/expo/eas-cli/pull/1016) by [@wkozyra95](https://github.com/wkozyra95))
## [0.48.0](https://github.com/expo/eas-cli/releases/tag/v0.48.0) - 2022-03-14
### 🎉 New features
- Add code signing. ([#964](https://github.com/expo/eas-cli/pull/964) by [@wschurman](https://github.com/wschurman))
- Install expo-updates when running update:configure. ([#977](https://github.com/expo/eas-cli/pull/977) by [@jkhales](https://github.com/jkhales))
- Make `update:configure` work on native files. ([#978](https://github.com/expo/eas-cli/pull/978) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Fix `"Unknown status"` error when a build is canceled. ([#1012](https://github.com/expo/eas-cli/pull/1012) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add error message when package.json is outside git repository. ([#971](https://github.com/expo/eas-cli/pull/971) by [@wkozyra95](https://github.com/wkozyra95))
- Make runtime version policy warning on the update command more descriptive. ([#979](https://github.com/expo/eas-cli/pull/979) by [@kbrandwijk](https://github.com/kbrandwijk))
- Remove unused flag. ([#995](https://github.com/expo/eas-cli/pull/995) by [@wkozyra95](https://github.com/wkozyra95))
- Replace `got` with `node-fetch`. ([#1000](https://github.com/expo/eas-cli/pull/1000) by [@wkozyra95](https://github.com/wkozyra95))
- Upgrade dependencies. ([#1015](https://github.com/expo/eas-cli/pull/1015) by [@dsokal](https://github.com/dsokal))
## [0.47.0](https://github.com/expo/eas-cli/releases/tag/v0.47.0) - 2022-02-08
### 🐛 Bug fixes
- Return informative error when running `eas channel:rollout` with a channel that does not exist. ([#930](https://github.com/expo/eas-cli/pull/930) by [@jkhales](https://github.com/jkhales))
- Check if `expo-updates` is installed before publish. ([#953](https://github.com/expo/eas-cli/pull/953) by [@jkhales](https://github.com/jkhales))
### 🧹 Chores
- Upgrade dependencies. ([#946](https://github.com/expo/eas-cli/pull/946) by [@dsokal](https://github.com/dsokal))
## [0.46.0](https://github.com/expo/eas-cli/releases/tag/v0.46.0) - 2022-01-26
### 🛠 Breaking changes
- Drop support for building iOS builds on macOS Catalina. ([#939](https://github.com/expo/eas-cli/pull/939) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix URL placeholder for iOS submits. ([#941](https://github.com/expo/eas-cli/pull/941) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Unify EAS command descriptions style. ([#925](https://github.com/expo/eas-cli/pull/925) by [@dsokal](https://github.com/dsokal))
- Upgrade dependencies. ([#939](https://github.com/expo/eas-cli/pull/939) by [@dsokal](https://github.com/dsokal))
## [0.45.1](https://github.com/expo/eas-cli/releases/tag/v0.45.1) - 2022-01-19
### 🎉 New features
- Add json output for started builds when using `--no-wait`. ([#921](https://github.com/expo/eas-cli/pull/921) by [@kbrandwijk](https://github.com/kbrandwijk))
- Add support to `eas build` for specifying a custom `prebuildCommand` in `eas.json`. ([#919](https://github.com/expo/eas-cli/pull/919) by [@kbrandwijk](https://github.com/kbrandwijk))
### 🧹 Chores
- Add macOS Monterey image. ([#922](https://github.com/expo/eas-cli/pull/922) by [@wkozyra95](https://github.com/wkozyra95))
## [0.45.0](https://github.com/expo/eas-cli/releases/tag/v0.45.0) - 2022-01-18
### 🎉 New features
- Add `eas device:delete`. ([#890](https://github.com/expo/eas-cli/pull/890) by [@kbrandwijk](https://github.com/kbrandwijk))
### 🧹 Chores
- Upgrade dependencies. ([#872](https://github.com/expo/eas-cli/pull/872) by [@dsokal](https://github.com/dsokal))
## [0.44.1](https://github.com/expo/eas-cli/releases/tag/v0.44.1) - 2022-01-11
### 🐛 Bug fixes
- Fix build:configure expo-updates configuration to only run on generic projects. ([#904](https://github.com/expo/eas-cli/pull/904) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
### 🛠 Breaking changes
- No longer enable APNS (iOS Push Notifications) capability by default anymore. ([#797](https://github.com/expo/eas-cli/pull/797) by [@EvanBacon](https://github.com/EvanBacon))
- Don't configure Android projects locally. Clean up old Gradle signing config. Streamline `expo-updates` configuration. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add `update:list` command. ([#884](https://github.com/expo/eas-cli/pull/884) by [@jkhales](https://github.com/jkhales))
- Improve error message for outdated Apple PLA error. ([#889](https://github.com/expo/eas-cli/pull/889) by [@EvanBacon](https://github.com/EvanBacon))
- Add `--output` flag to the build command. ([#885](https://github.com/expo/eas-cli/pull/885) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix creating project archive with symlink cycle. ([#891](https://github.com/expo/eas-cli/pull/891) by [@wkozyra95](https://github.com/wkozyra95))
- Sign Android debug builds with the correct keystore. Previously, all debug builds would be signed with a default debug keystore. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
## [0.43.0](https://github.com/expo/eas-cli/releases/tag/v0.43.0) - 2022-01-03
### 🎉 New features
- Add `eas channel:delete`. ([#846](https://github.com/expo/eas-cli/pull/846), [#869](https://github.com/expo/eas-cli/pull/869) by [@jkhales](https://github.com/jkhales))
- Add `eas autocomplete`. ([#870](https://github.com/expo/eas-cli/pull/870) by [@dsokal](https://github.com/dsokal))
@@ -15,8 +185,6 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix saving `build:inspect` results to project subdirectory. ([#863](https://github.com/expo/eas-cli/pull/863) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
## [0.42.4](https://github.com/expo/eas-cli/releases/tag/v0.42.4) - 2021-12-21
### 🐛 Bug fixes
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = {
projects: [
require('./packages/eas-cli/jest.config.js'),
require('./packages/eas-json/jest.config.js'),
],
import easCliConfig from './packages/eas-cli/jest.config.js';
import easJsonConfig from './packages/eas-json/jest.config.js';
export default {
projects: [easCliConfig, easJsonConfig],
testPathIgnorePatterns: ['.*'],
};
+2 -1
View File
@@ -1,7 +1,8 @@
module.exports = {
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
extensionsToTreatAsEsm: ['.ts'],
};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.43.0"
"version": "0.51.0"
}
+10 -9
View File
@@ -1,5 +1,6 @@
{
"private": true,
"type":"module",
"workspaces": [
"packages/*"
],
@@ -17,19 +18,19 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "27.0.1",
"@types/jest": "27.4.1",
"@types/node": "16.7.8",
"eslint": "7.32.0",
"eslint-config-universe": "8.0.0",
"eslint": "8.12.0",
"eslint-config-universe": "10.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"lerna": "4.0.0",
"oclif": "2.0.1",
"prettier": "2.4.1",
"ts-jest": "27.0.5",
"typescript": "4.5.2"
"oclif": "3.0.1",
"prettier": "2.6.1",
"ts-jest": "27.1.4",
"typescript": "4.6.3"
},
"volta": {
"node": "16.10.0",
+940 -2
View File
@@ -33,8 +33,120 @@ eas --help COMMAND
# Commands
<!-- commands -->
* [`eas account:login`](#eas-accountlogin)
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
* [`eas branch:list`](#eas-branchlist)
* [`eas branch:rename`](#eas-branchrename)
* [`eas branch:view [NAME]`](#eas-branchview-name)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:submit`](#eas-buildsubmit)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
* [`eas channel:list`](#eas-channellist)
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:delete`](#eas-devicedelete)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
* [`eas update:configure`](#eas-updateconfigure)
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
* [`eas update:list`](#eas-updatelist)
* [`eas update:view GROUPID`](#eas-updateview-groupid)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
## `eas account:login`
log in with your Expo account
```
USAGE
$ eas account:login
DESCRIPTION
log in with your Expo account
ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
log out
```
USAGE
$ eas account:logout
DESCRIPTION
log out
ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
show the username you are logged in as
```
USAGE
$ eas account:view
DESCRIPTION
show the username you are logged in as
ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
display or change analytics settings
```
USAGE
$ eas analytics [STATUS]
DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -63,7 +175,463 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@expo/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.0.2/src/commands/autocomplete/index.ts)_
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.2.0/src/commands/autocomplete/index.ts)_
## `eas branch:create [NAME]`
create a branch
```
USAGE
$ eas branch:create [NAME] [--json]
ARGUMENTS
NAME Name of the branch to create
FLAGS
--json return a json with the new branch ID and name.
DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
delete a branch
```
USAGE
$ eas branch:delete [NAME] [--json]
ARGUMENTS
NAME Name of the branch to delete
FLAGS
--json return JSON with the edited branch's ID and name.
DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
list all branches
```
USAGE
$ eas branch:list [--json]
FLAGS
--json return output as JSON
DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
rename a branch
```
USAGE
$ eas branch:rename [--from <value>] [--to <value>] [--json]
FLAGS
--from=<value> current name of the branch.
--json return a json with the edited branch's ID and name.
--to=<value> new name of the branch.
DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
view a branch
```
USAGE
$ eas branch:view [NAME] [--json]
ARGUMENTS
NAME Name of the branch to view
FLAGS
--json return a json with the branch's ID name and recent update groups.
DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
start a build
```
USAGE
$ eas build [-p android|ios|all] [--json] [--profile <value>] [--non-interactive] [--local] [--output
<value>] [--wait] [--clear-cache] [--auto-submit | --auto-submit-with-profile <value>]
FLAGS
-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 command in non-interactive mode
--output=<value> Output path for local build
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
in eas.json.
--[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/v0.51.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
cancel a build
```
USAGE
$ eas build:cancel [BUILD_ID]
DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
configure the project to support EAS Build
```
USAGE
$ eas build:configure [-p android|ios|all]
FLAGS
-p, --platform=(android|ios|all) Platform to configure
DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
inspect the state of the project at specific build stages, useful for troubleshooting
```
USAGE
$ eas build:inspect -p android|ios -s archive|pre-build|post-build -o <value> [--profile <value>] [--force] [-v]
FLAGS
-o, --output=OUTPUT_DIRECTORY
(required) Output directory.
-p, --platform=(android|ios)
(required)
-s, --stage=(archive|pre-build|post-build)
(required) Stage of the build you want to inspect.
archive - builds the project archive that would be uploaded to EAS when building
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
post-build - builds the native project and leaves the output directory for inspection
-v, --verbose
--force
Delete OUTPUT_DIRECTORY if it already exists.
--profile=PROFILE_NAME
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
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/v0.51.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
list all builds for your project
```
USAGE
$ eas build:list [--platform all|android|ios] [--json] [--status
new|in-queue|in-progress|errored|finished|canceled] [--distribution store|internal|simulator] [--channel <value>]
[--appVersion <value>] [--appBuildVersion <value>] [--sdkVersion <value>] [--runtimeVersion <value>]
[--appIdentifier <value>] [--buildProfile <value>] [--gitCommitHash <value>] [--limit <value>]
FLAGS
--appBuildVersion=<value>
--appIdentifier=<value>
--appVersion=<value>
--buildProfile=<value>
--channel=<value>
--distribution=(store|internal|simulator)
--gitCommitHash=<value>
--json Enable JSON output, non-JSON messages will be printed
to stderr
--limit=<value>
--platform=(all|android|ios)
--runtimeVersion=<value>
--sdkVersion=<value>
--status=(new|in-queue|in-progress|errored|finished|canceled)
DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
submit app binary to App Store and/or Play Store
```
USAGE
$ eas build:submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
<value>] [--verbose] [--wait] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=<value> Path to the .apk/.aab/.ipa file
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=<value> App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
DESCRIPTION
submit app binary to App Store and/or Play Store
ALIASES
$ eas build:submit
```
## `eas build:view [BUILD_ID]`
view a build for your project
```
USAGE
$ eas build:view [BUILD_ID] [--json]
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr
DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
create a channel
```
USAGE
$ eas channel:create [NAME] [--json]
ARGUMENTS
NAME Name of the channel to create
FLAGS
--json print output as a JSON object with the new channel ID, name and branch mapping.
DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
point a channel at a new branch
```
USAGE
$ eas channel:edit [NAME] [--branch <value>] [--json]
ARGUMENTS
NAME Name of the channel to edit
FLAGS
--branch=<value> Name of the branch to point to
--json Print output as a JSON object with the channel ID, name and branch mapping
DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
list all channels
```
USAGE
$ eas channel:list [--json]
FLAGS
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
view a channel
```
USAGE
$ eas channel:view [NAME] [--json]
ARGUMENTS
NAME Name of the channel to view
FLAGS
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
display project configuration (app.json + eas.json)
```
USAGE
$ eas config [-p android|ios] [--profile <value>]
FLAGS
-p, --platform=(android|ios)
--profile=<value>
DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
manage credentials
```
USAGE
$ eas credentials
DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
register new Apple Devices to use for internal distribution
```
USAGE
$ eas device:create
DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
remove a registered device from your account
```
USAGE
$ eas device:delete [--apple-team-id <value>] [--udid <value>]
FLAGS
--apple-team-id=<value>
--udid=<value>...
DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
list all registered devices for your account
```
USAGE
$ eas device:list [--apple-team-id <value>]
FLAGS
--apple-team-id=<value>
DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
view a device for your project
```
USAGE
$ eas device:view [UDID]
DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
display environment info
```
USAGE
$ eas diagnostics
DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -83,5 +651,375 @@ DESCRIPTION
Display help for eas.
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.10/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_
## `eas init`
create or link an EAS project
```
USAGE
$ eas init
DESCRIPTION
create or link an EAS project
ALIASES
$ eas init
```
## `eas login`
log in with your Expo account
```
USAGE
$ eas login
DESCRIPTION
log in with your Expo account
ALIASES
$ eas login
```
## `eas logout`
log out
```
USAGE
$ eas logout
DESCRIPTION
log out
ALIASES
$ eas logout
```
## `eas project:info`
information about the current project
```
USAGE
$ eas project:info
DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
create or link an EAS project
```
USAGE
$ eas project:init
DESCRIPTION
create or link an EAS project
ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
create an environment secret on the current project or owner account
```
USAGE
$ eas secret:create [--scope account|project] [--name <value>] [--value <value>] [--force]
FLAGS
--force Delete and recreate existing secrets
--name=<value> Name of the secret
--scope=(account|project) [default: project] Scope for the secret
--value=<value> Value of the secret
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/v0.51.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
delete an environment secret by ID
```
USAGE
$ eas secret:delete [--id <value>]
FLAGS
--id=<value> ID of the secret to delete
DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
list environment secrets available for your current app
```
USAGE
$ eas secret:list
DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
submit app binary to App Store and/or Play Store
```
USAGE
$ eas submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
<value>] [--verbose] [--wait] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=<value> Path to the .apk/.aab/.ipa file
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
--url=<value> App archive url
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
DESCRIPTION
submit app binary to App Store and/or Play Store
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
publish an update group
```
USAGE
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and update message
--branch=<value> Branch to publish the update group on
--group=<value> Update group to republish
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr
--message=<value> A short message describing the update
--non-interactive Run command in non-interactive mode
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory.
--republish Republish an update group
--skip-bundler Skip running Expo CLI to bundle the app before publishing
DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
configure the project to support EAS Update
```
USAGE
$ eas update:configure [-p android|ios|all]
FLAGS
-p, --platform=(android|ios|all) [default: all] Platform to configure
DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
delete all the updates in an update group
```
USAGE
$ eas update:delete [GROUPID] [--json]
ARGUMENTS
GROUPID The ID of an update group to delete.
FLAGS
--json Return a json with the group ID of the deleted updates.
DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
view the recent updates for a branch
```
USAGE
$ eas update:list [--branch <value> | --all] [--json]
FLAGS
--all List all updates associated with this project
--branch=<value> List all updates on this branch
--json Return a json with all of the recent update groups.
DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
update group details
```
USAGE
$ eas update:view [GROUPID] [--json]
ARGUMENTS
GROUPID The ID of an update group.
FLAGS
--json Return a json with the updates belonging to the group.
DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
create a webhook
```
USAGE
$ eas webhook:create [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=<value> Webhook URL
DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
delete a webhook
```
USAGE
$ eas webhook:delete [ID]
ARGUMENTS
ID ID of the webhook to delete
DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
list webhooks
```
USAGE
$ eas webhook:list [--event BUILD|SUBMIT]
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
update a webhook
```
USAGE
$ eas webhook:update --id <value> [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--id=<value> (required) Webhook ID
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=<value> Webhook URL
DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
view a webhook
```
USAGE
$ eas webhook:view [ID]
ARGUMENTS
ID ID of the webhook to view
DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
show the username you are logged in as
```
USAGE
$ eas whoami
DESCRIPTION
show the username you are logged in as
ALIASES
$ eas whoami
```
<!-- commandsstop -->
File renamed without changes.
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env node
require('@oclif/core')
.run()
.then(require('@oclif/core/flush'))
.catch(require('@oclif/core/handle'));
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env node
import { Errors, flush, run } from '@oclif/core';
run(undefined, import.meta.url)
.then(flush)
.catch(Errors.handle);
+21515 -20485
View File
File diff suppressed because it is too large. Load diff
+11 -2
View File
@@ -1,5 +1,14 @@
module.exports = {
...require('../../jest/jest.config.js'),
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import baseConfig from '../../jest/jest.config.js';
const filename = fileURLToPath(import.meta.url);
// eslint-disable-next-line no-underscore-dangle
const __dirname = dirname(filename);
export default {
...baseConfig,
rootDir: __dirname,
roots: ['src', '__mocks__'],
globalSetup: './jest/global-setup.ts',
+67 -57
View File
@@ -1,61 +1,67 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.43.0",
"version": "0.51.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
"eas": "./bin/run.js"
},
"type": "module",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.11",
"@expo/config-plugins": "4.0.11",
"@expo/eas-build-job": "0.2.61",
"@expo/eas-json": "0.43.0",
"@expo/apple-utils": "0.0.0-alpha.31",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.19",
"@expo/config-plugins": "4.1.0",
"@expo/config-types": "44.0.0",
"@expo/eas-build-job": "0.2.68",
"@expo/eas-json": "0.51.0",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.6",
"@expo/plist": "0.0.16",
"@expo/plugin-autocomplete": "1.0.2",
"@expo/plugin-warn-if-update-available": "2.0.1",
"@expo/prebuild-config": "3.0.11",
"@expo/multipart-body-parser": "1.1.0",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.1.0",
"@expo/prebuild-config": "3.1.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.5.0",
"@expo/spawn-async": "1.6.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.0.10",
"@oclif/plugin-help": "5.1.10",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"@oclif/core": "1.6.4",
"@oclif/plugin-autocomplete": "1.2.0",
"@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2",
"chalk": "4.1.2",
"cli-table3": "0.6.0",
"dateformat": "4.5.1",
"cli-progress": "3.10.0",
"cli-table3": "0.6.1",
"dateformat": "4.6.3",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-glob": "3.2.7",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.2.11",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "10.0.0",
"form-data": "4.0.0",
"fs-extra": "10.0.1",
"getenv": "1.0.0",
"got": "11.8.2",
"gradle-to-js": "2.0.0",
"graphql": "15.5.1",
"graphql-tag": "2.12.5",
"ignore": "5.1.8",
"graphql": "16.3.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.0",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.1",
"joi": "17.4.2",
"jks-js": "1.0.2",
"joi": "17.6.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "2.5.2",
"minimatch": "3.0.4",
"nanoid": "3.1.25",
"node-fetch": "2.6.1",
"node-forge": "0.10.0",
"mime": "3.0.0",
"minimatch": "3.0.5",
"nanoid": "3.3.2",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"prompts": "2.3.2",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
@@ -70,35 +76,36 @@
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.19.2",
"@graphql-codegen/introspection": "^1.18.0",
"@graphql-codegen/typescript": "^1.17.11",
"@graphql-codegen/typescript-operations": "^1.17.10",
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5",
"@types/cli-progress": "3.9.2",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/form-data": "^2.5.0",
"@types/fs-extra": "9.0.12",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "^0.9.5",
"@types/prompts": "^2.0.9",
"@types/semver": "7.3.8",
"@types/tar": "4.0.5",
"@types/node-forge": "1.0.1",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.1",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.21.4",
"axios": "0.26.1",
"eslint-plugin-graphql": "4.0.0",
"memfs": "3.2.2",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"form-data": "4.0.0",
"memfs": "3.4.1",
"mockdate": "3.0.5",
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "1.0.8",
"typescript": "4.5.2"
"ts-deepmerge": "2.0.1",
"typescript": "4.6.3"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.16"
},
"files": [
"/bin",
@@ -116,13 +123,16 @@
"bin": "eas",
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@oclif/plugin-help",
"@expo/plugin-warn-if-update-available",
"@expo/plugin-autocomplete"
"@expo/plugin-warn-if-update-available"
],
"topics": {
"account": {
"description": "manage your account"
"description": "manage account"
},
"branch": {
"description": "manage update branches"
},
"build": {
"description": "build app binaries"
@@ -131,10 +141,10 @@
"description": "manage update channels"
},
"device": {
"description": "manage your Apple devices for internal distribution"
"description": "manage Apple devices for Internal Distribution"
},
"project": {
"description": "manage your project"
"description": "manage project"
},
"release": {
"description": "manage update releases"
-27
View File
@@ -1,27 +0,0 @@
import { JSONValue } from '@expo/json-file';
import { HTTPError, RequestError } from 'got/dist/source';
export default class ApiV2Error extends RequestError {
readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(
originalError: HTTPError,
response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}
) {
super(response.message, originalError, originalError.request);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
+4 -6
View File
@@ -1,9 +1,7 @@
import assert from 'assert';
import { RequestError } from 'got/dist/source';
import nock from 'nock';
import ApiV2Error from '../ApiV2Error';
import { apiClient, getExpoApiBaseUrl } from '../api';
import { ApiV2Error, api, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
@@ -23,7 +21,7 @@ describe('apiClient', () => {
let error: Error | null = null;
try {
await apiClient.post('test');
await api.postAsync('test', { body: {} });
} catch (e: any) {
error = e;
}
@@ -43,11 +41,11 @@ describe('apiClient', () => {
let error: Error | null = null;
try {
await apiClient.post('test');
await api.postAsync('test', { body: {} });
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(RequestError);
expect(error).not.toBeNull();
expect(error).not.toBeInstanceOf(ApiV2Error);
});
});
+87 -35
View File
@@ -1,42 +1,94 @@
import got, { HTTPError, NormalizedOptions, RequestError } from 'got';
import { JSONValue } from '@expo/json-file';
import ApiV2Error from './ApiV2Error';
import fetch, { RequestError, RequestInit } from './fetch';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export const apiClient = got.extend({
prefixUrl: getExpoApiBaseUrl() + '/v2/',
hooks: {
beforeRequest: [
(options: NormalizedOptions) => {
const token = getAccessToken();
if (token) {
options.headers.authorization = `Bearer ${token}`;
return;
}
const sessionSecret = getSessionSecret();
if (sessionSecret) {
options.headers['expo-session'] = sessionSecret;
}
},
],
beforeError: [
(error: RequestError): RequestError => {
if (error instanceof HTTPError) {
let result: { [key: string]: any };
try {
result = JSON.parse(error.response.body as string);
} catch (e2) {
return error;
}
if (result.errors?.length) {
return new ApiV2Error(error, result.errors[0]);
}
}
return error;
},
],
export class ApiV2Error extends Error {
readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
interface RequestOptions {
body: JSONValue;
}
export const api = {
async putAsync(path: string, options: RequestOptions): Promise<any> {
return await requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
},
});
async postAsync(path: string, options: RequestOptions): Promise<any> {
return await requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
},
async deleteAsync(path: string): Promise<any> {
return await requestAsync(path, { method: 'DELETE' });
},
async getAsync(path: string): Promise<any> {
return await requestAsync(path, { method: 'GET' });
},
};
async function requestAsync(path: string, options: RequestInit): Promise<any> {
try {
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
});
return await response.json();
} catch (err) {
await handleApiErrorAsync(err);
}
}
async function handleApiErrorAsync(err: any): Promise<void> {
if (err instanceof RequestError) {
let result: { [key: string]: any };
try {
result = (await err.response.json()) as { [key: string]: any };
} catch {
throw new Error(`Malformed api response: ${await err.response.text()}`);
}
if (result.errors?.length) {
throw new ApiV2Error(result.errors[0]);
}
} else {
throw err;
}
}
function getAuthHeaders(): Record<string, string> {
const token = getAccessToken();
if (token) {
return { authorization: `Bearer ${token}` };
}
const sessionSecret = getSessionSecret();
if (sessionSecret) {
return { 'expo-session': sessionSecret };
}
return {};
}
export function getExpoApiBaseUrl(): string {
if (process.env.EXPO_STAGING) {
@@ -1,97 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
}
}
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
let androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, androidManifest);
}
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const androidManifest = await getAndroidManifestAsync(projectDir);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
throw new Error('Missing values in AndroidManifest.xml');
}
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifest = await getAndroidManifestAsync(projectDir);
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidConfig.Updates.Config.RELEASE_CHANNEL
);
} catch (err) {
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const androidManifest = await getAndroidManifestAsync(projectDir);
const stringifiedRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
androidManifest,
AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
);
if (!stringifiedRequestHeaders) {
return null;
}
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
} catch (err) {
return null;
}
}
async function getAndroidManifestAsync(projectDir: string): Promise<AndroidManifest> {
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
if (!androidManifestPath) {
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
}
return AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
}
+3 -3
View File
@@ -24,9 +24,9 @@ import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
@@ -71,8 +71,8 @@ export async function prepareAndroidBuildAsync(
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
return await ensureAndroidCredentialsAsync(ctx);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
@@ -18,6 +18,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
experimental: job.experimental,
};
}
@@ -75,6 +75,9 @@ export async function prepareJobAsync(
artifactPath: buildProfile.artifactPath,
buildType,
username,
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
};
return sanitizeJob(job);
@@ -2,39 +2,17 @@ import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
} from '../../project/android/applicationId';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { getVcsClient } from '../../vcs';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasAndroidNativeProject) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
await getVcsClient().trackFileAsync(easGradlePath);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
Log.withTick('Android project configured');
}
export async function validateAndSyncProjectConfigurationAsync({
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
@@ -48,11 +26,7 @@ export async function validateAndSyncProjectConfigurationAsync({
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
throw new Error(
'Project is not configured. Please run "eas build:configure" to configure the project.'
);
}
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
@@ -73,3 +47,22 @@ function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement):
return BumpStrategy.APP_VERSION;
}
}
// TODO: remove this after a few months
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
if (await fs.pathExists(easBuildGradlePath)) {
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
await fs.remove(easBuildGradlePath);
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
const buildGradleContentsWithoutApply = buildGradleContents.replace(
/apply from: ["'].\/eas-build.gradle["']\n/,
''
);
if (buildGradleContentsWithoutApply !== buildGradleContents) {
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
}
}
}
+287 -137
View File
@@ -1,16 +1,29 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { getExpoWebsiteBaseUrl } from '../api';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { requestedPlatformDisplayNames } from '../platform';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
@@ -38,7 +51,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
@@ -60,14 +73,13 @@ export async function prepareBuildRequestForPlatformAsync<
trackingCtx: ctx.trackingCtx,
}
);
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
}
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
trackingCtx: ctx.trackingCtx,
});
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
@@ -101,43 +113,43 @@ export async function prepareBuildRequestForPlatformAsync<
try {
return await sendBuildRequestAsync(builder, job, metadata);
} catch (error: any) {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED'
) {
Log.error(
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
`You have already reached the maximum number of pending ${
requestedPlatformDisplayNames[job.platform]
} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
);
}
throw error;
handleBuildRequestError(error, job.platform);
}
}
};
}
function handleBuildRequestError(error: any, platform: Platform): never {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
Log.error(
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
);
}
throw error;
}
async function uploadProjectAsync<TPlatform extends Platform>(
ctx: BuildContext<TPlatform>
): Promise<string> {
@@ -209,104 +221,242 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
);
}
type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log(
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
);
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
}
})
);
if (builds.length === 1) {
const [build] = builds;
if (build !== null) {
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status');
throw new Error(`Unknown build status: ${build.status} - aborting!`);
}
} else {
if (!spinner.text) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
}
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknown = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
]
.filter(i => i)
.join('\t');
}
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{
// 2 hours (max build time limit) + 10 minutes (possible queue time)
timeoutSec = 2 * 60 * 60 + 10 * 60,
intervalSec = 10,
} = {}
): Promise<MaybeBuildFragment[]> {
const b = `build${buildIds.length > 1 ? 's' : ''}`;
Log.log(`Waiting for ${b} to complete. You can press Ctrl+C to exit.`);
const spinner = ora(`Waiting for ${b} to complete.`).start();
const endTime = new Date().getTime() + timeoutSec * 1000;
while (new Date().getTime() <= endTime) {
const builds = await getBuildsSafelyAsync(buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
: await handleMultipleBuildsProgressAsync({ builds }, { spinner });
if (!refetch) {
return builds;
}
time = new Date().getTime();
await sleepAsync(intervalSec * 1000);
}
spinner.warn('Timed out');
spinner.fail('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
const promises = buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
}
});
return await Promise.all(promises);
}
interface BuildProgressResult {
refetch: boolean;
}
let queueProgressBarStarted = false;
const queueProgressBar = new cliProgress.SingleBar(
{ format: '|{bar}| {estimatedWaitTime}' },
cliProgress.Presets.rect
);
async function handleSingleBuildProgressAsync(
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
if (build === null) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
return { refetch: true };
}
if (queueProgressBarStarted && build?.status && build.status !== BuildStatus.InQueue) {
if (build.status === BuildStatus.InProgress) {
queueProgressBar.update(queueProgressBar.getTotal(), {
estimatedWaitTime: '',
});
}
queueProgressBar.stop();
Log.newLine();
queueProgressBarStarted = false;
spinner.start('Build is about to start');
}
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
formatAccountSubscriptionsUrl(accountName)
)}.`;
break;
case BuildStatus.InQueue: {
spinner.text = 'Build queued...';
const progressBarPayload =
typeof build.estimatedWaitTimeLeftSeconds === 'number'
? { estimatedWaitTime: formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds) }
: { estimatedWaitTime: '' };
if (
!queueProgressBarStarted &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number'
) {
spinner.stopAndPersist();
if (build.priority !== BuildPriority.High) {
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${chalk.underline(
formatAccountSubscriptionsUrl(accountName)
)}`
);
}
Log.newLine();
Log.log('Waiting in queue');
queueProgressBar.start(
build.initialQueuePosition + 1,
build.initialQueuePosition - build.queuePosition + 1,
progressBarPayload
);
queueProgressBarStarted = true;
}
if (typeof build.queuePosition === 'number') {
queueProgressBar.update(build.queuePosition, progressBarPayload);
}
break;
}
case BuildStatus.Canceled:
spinner.fail('Build canceled');
return { refetch: false };
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return { refetch: false };
} else {
throw new Error('Standalone build failed!');
}
default:
spinner.warn('Unknown status');
throw new Error(`Unknown build status: ${build.status} - aborting!`);
}
return { refetch: true };
}
const statusToDisplayName: Record<BuildStatus, string> = {
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
[BuildStatus.InQueue]: 'in queue',
[BuildStatus.InProgress]: 'in progress',
[BuildStatus.Canceled]: 'canceled',
[BuildStatus.Finished]: 'finished',
[BuildStatus.Errored]: 'failed',
};
const platforms = [AppPlatform.Android, AppPlatform.Ios];
async function handleMultipleBuildsProgressAsync(
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
const allFinished =
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
const allSettled =
builds.filter(build =>
[BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(build.status)
).length === buildCount;
if (allSettled) {
if (allFinished) {
spinner.succeed(formatSettledBuildsText(builds));
} else {
spinner.fail(formatSettledBuildsText(builds));
}
return { refetch: false };
} else {
spinner.text = formatPendingBuildsText(builds);
return { refetch: true };
}
}
function formatSettledBuildsText(builds: BuildFragment[]): string {
return platforms
.map(platform => {
const build = nullthrows(
builds.find(build => build.platform === platform),
`Build for platform ${platform} must be defined in this context`
);
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(statusToDisplayName[build.status])}`;
})
.join('\n ');
}
function formatPendingBuildsText(builds: BuildFragment[]): string {
return platforms
.map(platform => {
const build = builds.find(build => build.platform === platform);
const status = build ? statusToDisplayName[build.status] : 'unknown';
let extraInfo = '';
if (
build?.status === BuildStatus.InQueue &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number'
) {
const percent = Math.floor(
((build.initialQueuePosition - build.queuePosition + 1) /
(build.initialQueuePosition + 1)) *
100
);
const estimatedWaitTime =
typeof build.estimatedWaitTimeLeftSeconds === 'number'
? ` - ${formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds)}`
: '';
extraInfo = ` - queue progress: ${chalk.bold(`${percent}%`)}${estimatedWaitTime}`;
}
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${extraInfo}`;
})
.join('\n ');
}
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
return maybeBuild !== null;
}
function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
if (estimatedWaitTimeLeftSeconds < 5 * 60) {
return 'starting soon...';
} else {
const n = Math.floor(estimatedWaitTimeLeftSeconds / (10 * 60)) + 1;
return `starting in about ${n}0 minutes...`;
}
}
function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(
`/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
).toString();
}
+36 -125
View File
@@ -1,97 +1,47 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log, { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import Log from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
const configureCommitMessage = {
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
[RequestedPlatform.All]: 'Configure EAS Build',
};
export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
const message = 'This project is not configured to build with EAS. Set it up now?';
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: requestedPlatform,
});
if (await getVcsClient().isCommitRequiredAsync()) {
Errors.error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
Errors.error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.dev/build/eas-json'
)})`,
{ exit: 1 }
);
}
interface ConfigureParams {
projectDir: string;
nonInteractive: boolean;
}
export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
/**
* Creates eas.json if it does not exist.
*
* Returns:
* - false - if eas.json already exists
* - true - if eas.json was created by the function
*/
export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams
): Promise<boolean> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
return false;
}
await configureAsync(configureParams);
return true;
}
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
const ctx: ConfigureContext = {
user: await ensureLoggedInAsync(),
projectDir: options.projectDir,
exp,
requestedPlatform: options.platform,
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
options.platform
),
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
hasAndroidNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
hasIosNativeProject:
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
};
Log.newLine();
await ensureEasJsonExistsAsync(ctx);
if (ctx.shouldConfigureAndroid) {
await configureAndroidAsync(ctx);
}
if (ctx.shouldConfigureIos) {
await configureIosAsync(ctx);
}
await createEasJsonAsync(projectDir);
if (await getVcsClient().isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
await reviewAndCommitChangesAsync('Configure EAS Build', {
nonInteractive,
});
}
}
@@ -138,58 +88,19 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAsync();
Log.withTick('Validated eas.json');
return;
}
async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
hasAndroidNativeProject || hasIosNativeProject
? EAS_JSON_BARE_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await getVcsClient().trackFileAsync(easJsonPath);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
Skip,
}
async function reviewAndCommitChangesAsync(
initialCommitMessage: string,
askedFirstTime: boolean = true
): Promise<void> {
const { selected } = await promptAsync({
type: 'select',
name: 'selected',
message: 'Can we commit these changes to git for you?',
choices: [
{ title: 'Yes', value: ShouldCommitChanges.Yes },
...(askedFirstTime
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
: []),
{
title: 'Skip committing changes, I will do it later on my own',
value: ShouldCommitChanges.Skip,
},
],
});
if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
}
-13
View File
@@ -6,23 +6,11 @@ import type { XCBuildConfiguration } from 'xcode';
import { TrackingContext } from '../analytics/common';
import { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types';
import { RequestedPlatform } from '../platform';
import { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User';
import { LocalBuildOptions } from './local';
export interface ConfigureContext {
user: Actor;
projectDir: string;
exp: ExpoConfig;
requestedPlatform: RequestedPlatform;
shouldConfigureAndroid: boolean;
shouldConfigureIos: boolean;
hasAndroidNativeProject: boolean;
hasIosNativeProject: boolean;
}
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
export interface AndroidBuildContext {
@@ -51,7 +39,6 @@ export interface BuildContext<T extends Platform> {
projectDir: string;
projectId: string;
projectName: string;
skipProjectConfiguration: boolean;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
@@ -25,7 +25,6 @@ export async function createBuildContextAsync<T extends Platform>({
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -34,7 +33,6 @@ export async function createBuildContextAsync<T extends Platform>({
nonInteractive: boolean;
platform: T;
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
@@ -81,7 +79,6 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
@@ -1,91 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { getVcsClient } from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
expoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
await writeExpoPlistAsync(projectDir, expoPlist);
}
// TODO: ensure ExpoPlist in pbxproj
}
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
try {
await ensureUpdatesConfiguredAsync(projectDir);
} catch (error) {
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
}
let expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistVersionConfigurationSynced(exp, expoPlist)) {
expoPlist = IOSConfig.Updates.setVersionsConfig(exp, expoPlist);
await writeExpoPlistAsync(projectDir, expoPlist);
}
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
const expoPlist = await readExpoPlistAsync(projectDir);
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
throw new Error('Missing values in Expo.plist');
}
}
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
projectDir: string,
expoPlist: IOSConfig.ExpoPlist
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
await writePlistAsync(expoPlistPath, expoPlist);
await getVcsClient().trackFileAsync(expoPlistPath);
}
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist = await readExpoPlistAsync(projectDir);
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
} catch (err) {
return null;
}
}
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
return updatesRequestHeaders['expo-channel-name'] ?? null;
} catch (err) {
return null;
}
}
+3 -3
View File
@@ -11,10 +11,10 @@ import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from
import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
@@ -63,8 +63,8 @@ export async function prepareIosBuildAsync(
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
@@ -20,6 +20,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
username: job.username,
developmentClient: job.developmentClient,
simulator: job.simulator,
experimental: job.experimental,
};
}
@@ -66,6 +66,9 @@ export async function prepareJobAsync(
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
username,
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
};
return sanitizeJob(job);
}
@@ -3,32 +3,12 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
} from '../../project/ios/bundleIdentifier';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
return;
}
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
Log.withTick('iOS project configured');
}
export async function validateAndSyncProjectConfigurationAsync({
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
+3 -1
View File
@@ -6,13 +6,14 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.57';
const PLUGIN_PACKAGE_VERSION = '0.0.74';
export interface LocalBuildOptions {
enable: boolean;
skipCleanup?: boolean;
skipNativeBuild?: boolean;
artifactsDir?: string;
artifactPath?: string;
workingdir?: string;
verbose?: boolean;
}
@@ -41,6 +42,7 @@ export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions):
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
},
});
childProcess = spawnPromise.child;
+9 -22
View File
@@ -5,34 +5,21 @@ import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
import Log from '../log';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
} from './android/UpdatesModule';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
} from '../update/android/UpdatesModule';
import {
readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from './ios/UpdatesModule';
} from '../update/ios/UpdatesModule';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
function getRuntimeVersionNullable(
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
): string | null {
try {
return Updates.getRuntimeVersion(config, platform);
} catch (e) {
Log.debug(e);
return null;
}
}
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
@@ -49,7 +36,7 @@ export async function collectMetadataAsync<T extends Platform>(
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx),
...channelOrReleaseChannel,
distribution,
@@ -20,6 +20,7 @@ import {
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
@@ -38,7 +39,6 @@ let sdkVersionChecked = false;
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile?: string;
nonInteractive: boolean;
wait: boolean;
@@ -53,7 +53,10 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, flags.requestedPlatform);
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: flags.nonInteractive,
});
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
@@ -128,10 +131,17 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
}
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
}
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
const { accountName } = Object.values(buildCtxByPlatform)[0];
const builds = await waitForBuildEndAsync({
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
});
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -163,7 +173,6 @@ async function prepareAndStartBuildAsync({
nonInteractive: flags.nonInteractive,
platform: buildProfile.platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
localBuildOptions: flags.localBuildOptions,
});
@@ -103,10 +103,7 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
Log.warn("Changes won't affect your project config.");
Log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn(
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
);
Log.warn('This command is using API that soon will be deprecated.');
Log.warn(deprecationInfo.message);
} else {
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
@@ -1,14 +0,0 @@
import { ExpoConfig, getPackageJson } from '@expo/config';
export function isExpoUpdatesInstalled(projectDir: string): boolean {
const packageJson = getPackageJson(projectDir);
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
}
export function ensureValidVersions(exp: ExpoConfig): void {
if (!exp.runtimeVersion && !exp.sdkVersion) {
throw new Error(
"Couldn't find either 'runtimeVersion' or 'sdkVersion' to configure 'expo-updates'. Please specify at least one of these properties under the 'expo' key in 'app.json'"
);
}
}
+11 -7
View File
@@ -4,20 +4,24 @@ import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
return new URL(
`/accounts/${accountName}/projects/${projectName}`,
getExpoWebsiteBaseUrl()
).toString();
}
export function getBuildLogsUrl(build: BuildFragment): string {
const { project } = build;
if (project.__typename === 'App') {
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
} else {
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
}
const url =
project.__typename !== 'App'
? `/builds/${build.id}`
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
return new URL(url, getExpoWebsiteBaseUrl()).toString();
}
export function getArtifactUrl(artifactId: string): string {
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
return new URL(`/artifacts/${artifactId}`, getExpoWebsiteBaseUrl()).toString();
}
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
+1 -1
View File
@@ -3,7 +3,7 @@ import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends EasCommand {
static description = 'view or change analytics settings';
static description = 'display or change analytics settings';
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
@@ -52,7 +52,7 @@ export async function createUpdateBranchOnAppAsync({
}
export default class BranchCreate extends EasCommand {
static description = 'Create a branch on the current project.';
static description = 'create a branch';
static args = [
{
@@ -77,7 +77,7 @@ async function deleteBranchOnAppAsync({
}
export default class BranchDelete extends EasCommand {
static description = 'Delete a branch on the current project';
static description = 'delete a branch';
static args = [
{
+1 -1
View File
@@ -55,7 +55,7 @@ export async function listBranchesAsync({
}
export default class BranchList extends EasCommand {
static description = 'List all branches on this project.';
static description = 'list all branches';
static flags = {
json: Flags.boolean({
@@ -52,7 +52,7 @@ async function renameUpdateBranchOnAppAsync({
}
export default class BranchRename extends EasCommand {
static description = 'Rename a branch.';
static description = 'rename a branch';
static flags = {
from: Flags.string({
+3 -57
View File
@@ -2,11 +2,9 @@ import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { ViewBranchQuery, ViewBranchQueryVariables } from '../../graphql/generated';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -15,60 +13,8 @@ import groupBy from '../../utils/expodash/groupBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
const PAGE_LIMIT = 10_000;
export async function viewUpdateBranchAsync({
appId,
name,
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<ViewBranchQuery> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ViewBranchQuery, ViewBranchQueryVariables>(
gql`
query ViewBranch($appId: String!, $name: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateBranchByName(name: $name) {
id
name
updates(offset: 0, limit: $limit) {
id
group
message
createdAt
runtimeVersion
platform
manifestFragment
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
`,
{
appId,
name,
limit: PAGE_LIMIT,
},
{ additionalTypenames: ['UpdateBranch', 'Update'] }
)
.toPromise()
);
return data;
}
export default class BranchView extends EasCommand {
static description = 'View a branch.';
static description = 'view a branch';
static args = [
{
@@ -111,7 +57,7 @@ export default class BranchView extends EasCommand {
}));
}
const { app } = await viewUpdateBranchAsync({
const { app } = await UpdateQuery.viewBranchAsync({
appId: projectId,
name,
});
@@ -110,7 +110,7 @@ async function ensureBuildExistsAsync(buildId: string): Promise<void> {
}
export default class BuildCancel extends EasCommand {
static description = 'Cancel a build.';
static description = 'cancel a build';
static args = [{ name: 'BUILD_ID' }];
@@ -1,16 +1,22 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { configureAsync } from '../../build/configure';
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { findProjectRootAsync, isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { getVcsClient } from '../../vcs';
export default class BuildConfigure extends EasCommand {
static description = 'Configure the project to support EAS Build.';
static description = 'configure the project to support EAS Build';
static flags = {
platform: Flags.enum({
@@ -26,45 +32,60 @@ export default class BuildConfigure extends EasCommand {
Log.log(
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
);
Log.newLine();
await getVcsClient().ensureRepoExistsAsync();
const projectDir = await findProjectRootAsync();
const expoUpdatesIsInstalled = isExpoUpdatesInstalled(projectDir);
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
await configureAsync({
platform,
projectDir: await findProjectRootAsync(),
// clean up old Android configuration
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
}
// ensure eas.json exists
Log.newLine();
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: false,
});
Log.newLine();
logSuccess(platform);
}
}
// configure expo-updates
if (expoUpdatesIsInstalled) {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
function logSuccess(platform: RequestedPlatform): void {
let platformsText = 'iOS and Android projects are';
let storesText = 'the Apple App Store or Google Play Store';
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
}
}
if (platform === 'android') {
platformsText = 'Android project is';
storesText = 'the Google Play Store';
} else if (platform === 'ios') {
platformsText = 'iOS project is';
storesText = 'the Apple App Store Connect';
}
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(projectDir, exp);
}
}
}
Log.log(`🎉 Your ${platformsText} ready to build.
Log.addNewLineIfNone();
Log.log(`🎉 Your project is ready to build.
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to app stores.
- ${learnMore('https://docs.expo.dev/build/introduction', {
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
})}`);
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
})}`);
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
Log.addNewLineIfNone();
const { platform } = await promptAsync({
type: 'select',
message: 'Which platforms would you like to configure for EAS Build?',
+22 -4
View File
@@ -3,6 +3,7 @@ import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import path from 'path';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
@@ -23,6 +24,7 @@ interface RawBuildFlags {
profile?: string;
'non-interactive': boolean;
local: boolean;
output?: string;
wait: boolean;
'clear-cache': boolean;
json: boolean;
@@ -31,7 +33,7 @@ interface RawBuildFlags {
}
export default class Build extends EasCommand {
static description = 'Start a build';
static description = 'start a build';
static flags = {
platform: Flags.enum({
@@ -48,7 +50,7 @@ export default class Build extends EasCommand {
}),
'skip-project-configuration': Flags.boolean({
default: false,
description: 'Skip project configuration',
hidden: true,
}),
profile: Flags.string({
description:
@@ -63,6 +65,9 @@ export default class Build extends EasCommand {
default: false,
description: 'Run build locally [experimental]',
}),
output: Flags.string({
description: 'Output path for local build',
}),
wait: Flags.boolean({
default: true,
allowNo: true,
@@ -100,6 +105,9 @@ export default class Build extends EasCommand {
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.local && flags.output) {
Errors.error('--output is allowed only for local builds', { exit: 1 });
}
if (!flags.platform && nonInteractive) {
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
@@ -130,15 +138,25 @@ export default class Build extends EasCommand {
);
Log.newLine();
}
if (flags['skip-project-configuration']) {
Log.warnDeprecatedFlag(
'skip-project-configuration',
'Automatic configuration of native code is no longer optional.'
);
Log.newLine();
}
const profile = flags['profile'];
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile,
nonInteractive,
localBuildOptions: flags['local']
? { enable: true, verbose: true }
? {
enable: true,
verbose: true,
artifactPath: flags.output && path.resolve(process.cwd(), flags.output),
}
: {
enable: false,
},
@@ -26,7 +26,7 @@ const STAGE_DESCRIPTION = `Stage of the build you want to inspect.
export default class BuildInspect extends EasCommand {
static description =
'Inspect the state of the project at specific build stages. Useful for troubleshooting.';
'inspect the state of the project at specific build stages, useful for troubleshooting';
static flags = {
platform: Flags.enum({
@@ -83,7 +83,6 @@ export default class BuildInspect extends EasCommand {
const projectDir = await findProjectRootAsync();
try {
await runBuildAndSubmitAsync(projectDir, {
skipProjectConfiguration: false,
nonInteractive: false,
wait: true,
clearCache: false,
@@ -9,10 +9,10 @@ import {
CreateUpdateChannelOnAppMutation,
CreateUpdateChannelOnAppMutationVariables,
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
@@ -60,7 +60,7 @@ export async function createUpdateChannelOnAppAsync({
}
export default class ChannelCreate extends EasCommand {
static description = 'Create a channel on the current project.';
static description = 'create a channel';
static args = [
{
@@ -106,14 +106,15 @@ export default class ChannelCreate extends EasCommand {
let branchId: string;
let branchMessage: string;
try {
const existingBranch = await getBranchByNameAsync({
appId: projectId,
name: channelName,
});
const existingBranch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: channelName,
});
if (existingBranch) {
branchId = existingBranch.id;
branchMessage = `We found a branch with the same name`;
} catch (e) {
} else {
const newBranch = await createUpdateBranchOnAppAsync({
appId: projectId,
name: channelName,
@@ -22,7 +22,7 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class ChannelDelete extends EasCommand {
static hidden = true;
static description = 'Delete a channel on the current project';
static description = 'Delete a channel';
static args = [
{
@@ -33,7 +33,7 @@ export default class ChannelDelete extends EasCommand {
];
static flags = {
json: Flags.boolean({
description: `Delete a channel on the current project`,
description: 'print output as a JSON object',
default: false,
}),
'non-interactive': Flags.boolean({
+42 -38
View File
@@ -13,12 +13,9 @@ import {
UpdateChannelBranchMappingMutation,
UpdateChannelBranchMappingMutationVariables,
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -96,7 +93,7 @@ export async function updateChannelBranchMappingAsync({
}
export default class ChannelEdit extends EasCommand {
static description = 'Point a channel at a new branch.';
static description = 'point a channel at a new branch';
static args = [
{
@@ -111,17 +108,14 @@ export default class ChannelEdit extends EasCommand {
description: 'Name of the branch to point to',
}),
json: Flags.boolean({
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
description: 'Print output as a JSON object with the channel ID, name and branch mapping',
default: false,
}),
};
async runAsync(): Promise<void> {
let {
args: { name: channelName },
flags: { branch: branchName, json: jsonFlag },
} = await this.parse(ChannelEdit);
if (jsonFlag) {
const { args, flags } = await this.parse(ChannelEdit);
if (flags.json) {
enableJsonOutput();
}
@@ -129,18 +123,7 @@ export default class ChannelEdit extends EasCommand {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!channelName) {
const validationMessage = 'A channel name is required to edit a specific channel.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: channelName } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the channel to edit:',
validate: value => (value ? true : validationMessage),
}));
}
const channelName = args.name ?? (await promptForChannelAsync());
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
if (existingChannel.updateBranches.length > 1) {
@@ -149,20 +132,19 @@ export default class ChannelEdit extends EasCommand {
);
}
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: chalk`What branch should it change to?`,
validate: value => (value ? true : validationMessage),
}));
}
const branchName = flags.branch ?? (await promptForBranchAsync());
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
const branch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: branchName,
});
if (!branch) {
throw new Error(
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
'eas branch:list'
)}.`
);
}
const channel = await updateChannelBranchMappingAsync({
channelId: existingChannel.id,
// todo: move branch mapping logic to utility
@@ -172,7 +154,7 @@ export default class ChannelEdit extends EasCommand {
}),
});
if (jsonFlag) {
if (flags.json) {
printJsonOnlyOutput(channel);
} else {
Log.withTick(
@@ -185,3 +167,25 @@ export default class ChannelEdit extends EasCommand {
}
}
}
async function promptForChannelAsync(): Promise<string> {
Log.addNewLineIfNone();
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the channel to edit:',
validate: value => (value ? true : 'The channel name may not be empty.'),
});
return name;
}
async function promptForBranchAsync(): Promise<string> {
Log.addNewLineIfNone();
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: `To which branch should the channel link?`,
validate: value => (value ? true : 'The branch name may not be empty.'),
});
return name;
}
@@ -67,7 +67,7 @@ async function getAllUpdateChannelForAppAsync({
}
export default class ChannelList extends EasCommand {
static description = 'List all channels on the current project.';
static description = 'list all channels';
static flags = {
json: Flags.boolean({
@@ -4,17 +4,18 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { updateChannelBranchMappingAsync } from './edit';
import { BranchMapping, getBranchMapping, getUpdateChannelByNameForAppAsync } from './view';
import { BranchMapping, getBranchMapping } from './view';
async function promptForRolloutPercentAsync({
promptMessage,
@@ -39,28 +40,20 @@ async function promptForRolloutPercentAsync({
return rolloutPercent;
}
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
function getRolloutInfo(
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>
): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
} {
const { branchMapping } = getBranchMapping(
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
);
const { branchMapping } = getBranchMapping(channel.branchMapping);
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
const newBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const newBranch = channel.updateBranches.filter(branch => branch.id === newBranchId)[0];
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
if (!newBranch || !oldBranch) {
throw new Error(
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.name}".`
);
throw new Error(`Branch mapping rollout is missing a branch for channel "${channel.name}".`);
}
const currentPercent = 100 * branchMapping.data[0].branchMappingLogic.operand;
@@ -75,16 +68,16 @@ async function startRolloutAsync({
projectId,
fullName,
currentBranchMapping,
getUpdateChannelByNameForAppResult,
channel,
}: {
channelName?: string;
branchName?: string;
branchName: string;
percent?: number;
jsonFlag: boolean;
projectId: string;
fullName: string;
currentBranchMapping: BranchMapping;
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery;
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
}): Promise<{
newChannelInfo: {
id: string;
@@ -93,19 +86,18 @@ async function startRolloutAsync({
};
logMessage: string;
}> {
if (!branchName) {
const validationMessage = 'A branch must be specified.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: `Select a branch to rollout onto ${channelName}`,
validate: value => (value ? true : validationMessage),
}));
const branch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: branchName,
});
if (!branch) {
throw new Error(
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
'eas branch:list'
)}.`
);
}
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
const oldBranchId = currentBranchMapping.data[0].branchId;
if (branch.id === oldBranchId) {
throw new Error(
@@ -138,14 +130,11 @@ async function startRolloutAsync({
],
};
const newChannelInfo = await updateChannelBranchMappingAsync({
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
@@ -168,13 +157,13 @@ async function editRolloutAsync({
percent,
jsonFlag,
currentBranchMapping,
getUpdateChannelByNameForAppResult,
channel,
}: {
channelName?: string;
percent?: number;
jsonFlag: boolean;
currentBranchMapping: BranchMapping;
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery;
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
}): Promise<{
newChannelInfo: {
id: string;
@@ -183,9 +172,7 @@ async function editRolloutAsync({
};
logMessage: string;
}> {
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(
getUpdateChannelByNameForAppResult
);
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
if (percent == null) {
if (jsonFlag) {
@@ -205,7 +192,7 @@ async function editRolloutAsync({
newBranchMapping.data[0].branchMappingLogic.operand = percent / 100;
const newChannelInfo = await updateChannelBranchMappingAsync({
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
@@ -225,13 +212,13 @@ async function endRolloutAsync({
branchName,
jsonFlag,
projectId,
getUpdateChannelByNameForAppResult,
channel,
}: {
channelName?: string;
branchName?: string;
jsonFlag: boolean;
projectId: string;
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery;
channel: NonNullable<GetChannelByNameForAppQuery['app']['byId']['updateChannelByName']>;
}): Promise<{
newChannelInfo: {
id: string;
@@ -241,13 +228,22 @@ async function endRolloutAsync({
logMessage: string;
}> {
// end rollout
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(
getUpdateChannelByNameForAppResult
);
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
let endOnNewBranch;
if (branchName) {
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
const branch = await BranchQuery.getBranchByNameAsync({
appId: projectId,
name: branchName,
});
if (!branch) {
throw new Error(
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
'eas branch:list'
)}.`
);
}
switch (branch.id) {
case newBranch.id:
endOnNewBranch = true;
@@ -295,7 +291,7 @@ async function endRolloutAsync({
};
const newChannelInfo = await updateChannelBranchMappingAsync({
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `️Rollout on channel ${chalk.bold(
@@ -352,12 +348,20 @@ export default class ChannelRollout extends EasCommand {
const fullName = await getProjectFullNameAsync(exp);
const projectId = await getProjectIdAsync(exp);
const getUpdateChannelByNameForAppResult = await getUpdateChannelByNameForAppAsync({
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
appId: projectId,
channelName: channelName!,
});
if (!channel) {
throw new Error(
`Could not find a channel named "${channelName}". Please check what channels exist on this project with ${chalk.bold(
'eas channel:list'
)}.`
);
}
const { branchMapping: currentBranchMapping, isRollout } = getBranchMapping(
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
channel.branchMapping
);
if (currentBranchMapping.data.length === 0) {
@@ -391,13 +395,13 @@ export default class ChannelRollout extends EasCommand {
if (!isRollout) {
rolloutMutationResult = await startRolloutAsync({
channelName,
branchName,
branchName: branchName ?? (await promptForBranchNameAsync(channelName)),
percent,
jsonFlag,
projectId,
fullName,
currentBranchMapping,
getUpdateChannelByNameForAppResult,
channel,
});
} else if (endFlag) {
rolloutMutationResult = await endRolloutAsync({
@@ -405,7 +409,7 @@ export default class ChannelRollout extends EasCommand {
branchName,
jsonFlag,
projectId,
getUpdateChannelByNameForAppResult,
channel,
});
} else {
rolloutMutationResult = await editRolloutAsync({
@@ -413,7 +417,7 @@ export default class ChannelRollout extends EasCommand {
percent,
jsonFlag,
currentBranchMapping,
getUpdateChannelByNameForAppResult,
channel,
});
}
if (!rolloutMutationResult) {
@@ -427,3 +431,13 @@ export default class ChannelRollout extends EasCommand {
}
}
}
async function promptForBranchNameAsync(channelName: string): Promise<string> {
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: `Select a branch to rollout onto ${channelName}`,
validate: value => (value ? true : 'A branch must be specified.'),
});
return name;
}
+3 -59
View File
@@ -3,14 +3,9 @@ import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import Table from 'cli-table3';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetChannelByNameForAppQuery,
GetChannelByNameForAppQueryVariables,
} from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -91,56 +86,6 @@ export function getBranchMapping(branchMappingString?: string): {
return { branchMapping, isRollout, rolloutPercent };
}
export async function getUpdateChannelByNameForAppAsync({
appId,
channelName,
}: GetChannelByNameForAppQueryVariables): Promise<GetChannelByNameForAppQuery> {
return await withErrorHandlingAsync(
graphqlClient
.query<GetChannelByNameForAppQuery, GetChannelByNameForAppQueryVariables>(
gql`
query GetChannelByNameForApp($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
name
createdAt
branchMapping
updateBranches(offset: 0, limit: 1000) {
id
name
updates(offset: 0, limit: 10) {
id
group
message
runtimeVersion
createdAt
platform
actor {
id
... on User {
username
}
... on Robot {
firstName
}
}
}
}
}
}
}
}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
);
}
export function logChannelDetails(channel: {
branchMapping: string;
updateBranches: {
@@ -193,7 +138,7 @@ export function logChannelDetails(channel: {
}
export default class ChannelView extends EasCommand {
static description = 'View a channel on the current project.';
static description = 'view a channel';
static args = [
{
@@ -236,11 +181,10 @@ export default class ChannelView extends EasCommand {
}));
}
const getUpdateChannelByNameForAppresult = await getUpdateChannelByNameForAppAsync({
const channel = await ChannelQuery.getUpdateChannelByNameForAppAsync({
appId: projectId,
channelName,
});
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
if (!channel) {
throw new Error(`Could not find a channel with name: ${channelName}`);
}
+9 -3
View File
@@ -2,16 +2,19 @@ import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { appPlatformEmojis } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export default class Config extends EasCommand {
static description = 'show the eas.json config';
static description = 'display project configuration (app.json + eas.json)';
static flags = {
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
@@ -58,12 +61,15 @@ export default class Config extends EasCommand {
const profile = await reader.getBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
Log.addNewLineIfNone();
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
const appPlatform = toAppPlatform(platform);
const platformEmoji = appPlatformEmojis[appPlatform];
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
+1 -1
View File
@@ -2,7 +2,7 @@ import EasCommand from '../commandUtils/EasCommand';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
export default class Credentials extends EasCommand {
static description = 'manage your credentials';
static description = 'manage credentials';
async runAsync(): Promise<void> {
await new SelectPlatform().runAsync();
@@ -0,0 +1,246 @@
import { Device, DeviceStatus } from '@expo/apple-utils';
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import EasCommand from '../../commandUtils/EasCommand';
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import {
AppleDeviceQuery,
AppleDeviceQueryResult,
AppleDevicesByTeamIdentifierQueryResult,
} from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
import formatDevice from '../../devices/utils/formatDevice';
import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class DeviceDelete extends EasCommand {
static description = 'remove a registered device from your account';
static flags = {
'apple-team-id': Flags.string(),
udid: Flags.string({ multiple: true }),
};
async runAsync(): Promise<void> {
let {
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
} = await this.parse(DeviceDelete);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const accountName = await getProjectAccountNameAsync(exp);
if (!appleTeamIdentifier) {
appleTeamIdentifier = await this.askForAppleTeamAsync(accountName);
}
assert(appleTeamIdentifier, 'No team identifier is specified');
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
if (!appleDevicesResult) {
return;
}
const { appleTeamName, appleDevices } = appleDevicesResult;
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
if (chosenDevices.length === 0) {
Log.newLine();
Log.warn('No devices were chosen to be removed.');
return;
}
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
if (!hasRemoved) {
return;
}
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
}
async askAndDisableOnAppleAsync(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamIdentifier: string
): Promise<void> {
Log.newLine();
const deleteOnApple = await toggleConfirmAsync({
message: 'Do you want to disable these devices on your Apple account as well?',
});
if (!deleteOnApple) {
return;
}
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
const context = getRequestContext(ctx);
Log.addNewLineIfNone();
const removeAppleSpinner = ora('Disabling devices on Apple').start();
try {
let realDevices = await Device.getAllIOSProfileDevicesAsync(context);
realDevices = realDevices.filter(d =>
chosenDevices.map(cd => cd.identifier).includes(d.attributes.udid)
);
for (const device of realDevices) {
await device.updateAsync({ status: DeviceStatus.DISABLED });
}
removeAppleSpinner.succeed('Disabled devices on Apple');
} catch (err) {
removeAppleSpinner.fail();
throw err;
}
}
async askAndRemoveFromExpoAsync(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
): Promise<boolean> {
Log.warn(
`You are about to remove the Apple device${
chosenDevices.length > 1 ? 's' : ''
} listed above from your Expo account.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (confirmed) {
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
try {
for (const chosenDevice of chosenDevices) {
await AppleDeviceMutation.deleteAppleDeviceAsync(chosenDevice.id);
}
removalSpinner.succeed('Removed Apple devices from Expo');
} catch (err) {
removalSpinner.fail();
throw err;
}
}
return confirmed;
}
logChosenDevices(
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamName: Maybe<string> | undefined,
appleTeamIdentifier: string
): void {
Log.addNewLineIfNone();
chosenDevices.forEach(device => {
Log.log(
formatDevice(device, {
appleTeamName,
appleTeamIdentifier: appleTeamIdentifier!,
})
);
Log.newLine();
});
}
async chooseDevicesToDeleteAsync(
appleDevices: AppleDeviceQueryResult[],
udids: string[]
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
Log.newLine();
if (udids) {
udids.forEach(udid => {
const foundDevice = appleDevices.find(device => device.identifier === udid);
if (foundDevice) {
chosenDevices.push(foundDevice);
} else {
Log.warn(`No device found with UDID ${udid}.`);
}
});
}
if (chosenDevices.length === 0) {
Log.addNewLineIfNone();
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
Log.newLine();
}
return chosenDevices;
}
async getDevicesForTeamAsync(
accountName: string,
appleTeamIdentifier: string
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
const devicesSpinner = ora().start('Fetching the list of devices for the team…');
try {
const result = await AppleDeviceQuery.getAllForAppleTeamAsync(
accountName,
appleTeamIdentifier
);
if (result?.appleDevices.length) {
const { appleTeamName, appleDevices } = result;
devicesSpinner.succeed(
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
);
return result;
} else {
devicesSpinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
return;
}
} catch (e) {
devicesSpinner.fail(`Something went wrong and we couldn't fetch the device list`);
throw e;
}
}
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
const teamSpinner = ora().start('Fetching the list of teams for the project…');
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
if (teams.length > 0) {
teamSpinner.succeed('Fetched the list of teams for the project');
if (teams.length === 1) {
return teams[0].appleTeamIdentifier;
}
const result = await promptAsync({
type: 'select',
name: 'appleTeamIdentifier',
message: 'What Apple Team would you like to list devices for?',
choices: teams.map(team => ({
title: team.appleTeamName
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
: team.appleTeamIdentifier,
value: team.appleTeamIdentifier,
})),
});
return result.appleTeamIdentifier;
} else {
teamSpinner.fail(`Couldn't find any teams for the account ${accountName}`);
return;
}
} catch (e) {
teamSpinner.fail(`Something went wrong and we couldn't fetch the list of teams`);
throw e;
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ export default class BuildList extends EasCommand {
try {
const teams = await AppleTeamQuery.getAllForAccountAsync(accountName);
if (teams.length) {
if (teams.length > 0) {
spinner.succeed();
if (teams.length === 1) {
+1 -1
View File
@@ -8,7 +8,7 @@ import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand {
static description = 'log environment info to the console';
static description = 'display environment info';
protected requiresAuthentication = false;
@@ -20,7 +20,7 @@ import { getActorDisplayName } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
export default class EnvironmentSecretCreate extends EasCommand {
static description = 'Create an environment secret on the current project or owner account.';
static description = 'create an environment secret on the current project or owner account';
static flags = {
scope: Flags.enum({
@@ -1,6 +1,5 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
@@ -18,8 +17,7 @@ import {
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
static description = 'delete an environment secret by ID';
static flags = {
id: Flags.string({
+1 -1
View File
@@ -13,7 +13,7 @@ import {
} from '../../project/projectUtils';
export default class EnvironmentSecretList extends EasCommand {
static description = 'Lists environment secrets available for your current app';
static description = 'list environment secrets available for your current app';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
+2 -3
View File
@@ -5,7 +5,7 @@ import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { link } from '../log';
import Log from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -41,8 +41,7 @@ interface CommandFlags {
}
export default class Submit extends EasCommand {
static description = `Submit build archive to App Store Connect
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
static description = 'submit app binary to App Store and/or Play Store';
static aliases = ['build:submit'];
static flags = {
+203 -56
View File
@@ -1,32 +1,180 @@
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import { getEASUpdateURL } from '../../api';
import EasCommand from '../../commandUtils/EasCommand';
import { AppPlatform } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
import {
findProjectRootAsync,
getProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0';
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
async function configureProjectForEASUpdateAsync(
projectDir: string,
exp: ExpoConfig,
isBare: boolean
): Promise<void> {
export default class UpdateConfigure extends EasCommand {
static description = 'configure the project to support EAS Update';
static flags = {
platform: Flags.enum({
description: 'Platform to configure',
char: 'p',
options: ['android', 'ios', 'all'],
default: 'all',
}),
};
async runAsync(): Promise<void> {
Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const { flags } = await this.parse(UpdateConfigure);
const platform = flags.platform as RequestedPlatform;
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
await installExpoUpdatesAsync(projectDir);
}
const [androidWorkflow, iosWorkflow] = await Promise.all([
resolveWorkflowAsync(projectDir, Platform.ANDROID),
resolveWorkflowAsync(projectDir, Platform.IOS),
]);
const updatedExp = await configureAppJSONForEASUpdateAsync({
projectDir,
exp,
platform,
workflows: {
android: androidWorkflow,
ios: iosWorkflow,
},
});
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
// configure native files for EAS Update
if (
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
androidWorkflow === Workflow.GENERIC
) {
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp);
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
}
if (
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
iosWorkflow === Workflow.GENERIC
) {
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp);
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
}
Log.addNewLineIfNone();
Log.log(`🎉 Your app is configured to run EAS Update!`);
}
}
async function configureAppJSONForEASUpdateAsync({
projectDir,
exp,
platform,
workflows,
}: {
projectDir: string;
exp: ExpoConfig;
platform: RequestedPlatform;
workflows: {
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
};
}): Promise<ExpoConfig> {
const projectId = await getProjectIdAsync(exp);
const easUpdateURL = getEASUpdateURL(projectId);
const preexistingRuntimeVersion = exp.runtimeVersion;
const defaultRuntimeVersion = isBare
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const result = await modifyConfigAsync(projectDir, {
runtimeVersion: preexistingRuntimeVersion ?? defaultRuntimeVersion,
updates: { ...exp.updates, url: easUpdateURL },
});
const updates = { ...exp.updates, url: easUpdateURL };
const androidDefaultRuntimeVersion =
workflows['android'] === Workflow.GENERIC
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const iosDefaultRuntimeVersion =
workflows['ios'] === Workflow.GENERIC
? DEFAULT_BARE_RUNTIME_VERSION
: DEFAULT_MANAGED_RUNTIME_VERSION;
const newAndroidRuntimeVersion =
exp.android?.runtimeVersion ?? exp.runtimeVersion ?? androidDefaultRuntimeVersion;
const newIosRuntimeVersion =
exp.ios?.runtimeVersion ?? exp.runtimeVersion ?? iosDefaultRuntimeVersion;
let newConfig: Partial<ExpoConfig>;
switch (platform) {
case RequestedPlatform.All: {
if (isRuntimeEqual(newAndroidRuntimeVersion, newIosRuntimeVersion)) {
newConfig = {
runtimeVersion: newAndroidRuntimeVersion,
android: {
...exp.android,
runtimeVersion: undefined,
},
ios: { ...exp.ios, runtimeVersion: undefined },
updates,
};
} else {
newConfig = {
runtimeVersion: undefined, // top level runtime is redundant if it is specified in both android and ios
android: {
...exp.android,
runtimeVersion: newAndroidRuntimeVersion,
},
ios: {
...exp.ios,
runtimeVersion: newIosRuntimeVersion,
},
updates,
};
}
break;
}
case RequestedPlatform.Android: {
newConfig = {
android: {
...exp.android,
runtimeVersion: newAndroidRuntimeVersion,
},
updates,
};
break;
}
case RequestedPlatform.Ios: {
newConfig = {
ios: {
...exp.ios,
runtimeVersion: newIosRuntimeVersion,
},
updates,
};
break;
}
default: {
throw new Error(`Unsupported platform: ${platform}`);
}
}
const result = await modifyConfigAsync(projectDir, newConfig);
const prexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
const prexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
switch (result.type) {
case 'success':
if (exp.updates?.url) {
@@ -38,12 +186,27 @@ async function configureProjectForEASUpdateAsync(
} else {
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
}
if (!preexistingRuntimeVersion) {
if (
!prexistingAndroidRuntimeVersion &&
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform)
) {
Log.withTick(
`Set runtimeVersion to "${JSON.stringify(defaultRuntimeVersion)}" in app.json`
`Set ${appPlatformDisplayNames[AppPlatform.Android]} runtimeVersion to "${JSON.stringify(
newConfig.android?.runtimeVersion ?? newConfig.runtimeVersion
)}" in app.json`
);
}
if (
!prexistingIosRuntimeVersion &&
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)
) {
Log.withTick(
`Set ${appPlatformDisplayNames[AppPlatform.Ios]} runtimeVersion to "${JSON.stringify(
newConfig.ios?.runtimeVersion ?? newConfig.runtimeVersion
)}" in app.json`
);
}
break;
case 'warn': {
Log.addNewLineIfNone();
@@ -57,11 +220,15 @@ async function configureProjectForEASUpdateAsync(
'https://expo.fyi/eas-update-config.md'
)}\n`
);
Log.log(
chalk.bold(
`{\n "updates": {\n "url": "${easUpdateURL}"\n },\n "runtimeVersion": {\n "policy": "sdkVersion"\n }\n}`
)
);
Log.log(chalk.bold(JSON.stringify(newConfig, null, 2)));
Log.addNewLineIfNone();
if (workflows['android'] === Workflow.GENERIC || workflows['ios'] === Workflow.GENERIC) {
Log.warn(
`You will also have to manually edit the projects ${chalk.bold(
'Expo.plist/AndroidManifest.xml'
)}. ${learnMore('https://expo.fyi/eas-update-config.md#native-configuration')}`
);
}
Log.addNewLineIfNone();
throw new Error(result.message);
}
@@ -70,40 +237,20 @@ async function configureProjectForEASUpdateAsync(
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
assert(result.config, 'A successful result should have a config');
return result.config.expo;
}
export default class UpdateConfigure extends EasCommand {
static description = 'Configure the project to support EAS Update.';
async runAsync(): Promise<void> {
Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
const isBare = hasAndroidNativeProject || hasIosNativeProject;
await configureProjectForEASUpdateAsync(projectDir, exp, isBare);
Log.addNewLineIfNone();
if (isBare) {
Log.log(
`🧐 It seems you are on the bare workflow! Please also update your native files. You can do this by either running ${chalk.bold(
'eas build:configure'
)} or manually editing Expo.plist/AndroidManifest.xml. ${learnMore(
'https://expo.fyi/eas-update-config.md#native-configuration'
)}`
);
} else {
Log.log(`🎉 Your app is configured to run EAS Update!`);
}
function isRuntimeEqual(
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' },
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' }
): boolean {
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
return runtimeVersionA === runtimeVersionB;
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
return runtimeVersionA.policy === runtimeVersionB.policy;
} else {
return false;
}
}
@@ -36,7 +36,7 @@ async function deleteUpdateGroupAsync({
}
export default class UpdateDelete extends EasCommand {
static description = 'Delete all the updates in an update Group.';
static description = 'delete all the updates in an update group';
static args = [
{
@@ -68,8 +68,8 @@ export default class UpdateDelete extends EasCommand {
`${chalk.yellow(`This will delete all of the updates in group "${group}".`)} ${chalk.red(
'This is a permanent operation.'
)}\n\n` +
`If you want to revert to a previous publish, you should use 'branch:publish --republish' targeted at the last working update group instead.\n\n` +
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'branch:publish --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
`If you want to revert to a previous publish, you should use 'update --republish' targeted at the last working update group instead.\n\n` +
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'update --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
`Would you like to abort?`,
});
+159 -30
View File
@@ -1,28 +1,38 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import { Errors, Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import gql from 'graphql-tag';
import nullthrows from 'nullthrows';
import { getEASUpdateURL } from '../../api';
import EasCommand from '../../commandUtils/EasCommand';
import fetch from '../../fetch';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
GetUpdateGroupAsyncQuery,
PublishUpdateGroupInput,
Robot,
RootQueryUpdatesByGroupArgs,
Update,
UpdateInfoGroup,
UpdatePublishMutation,
User,
ViewBranchQuery,
ViewBranchUpdatesQuery,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getProjectIdAsync,
installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils';
import {
PublishPlatform,
buildBundlesAsync,
@@ -31,15 +41,20 @@ import {
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync, selectAsync } from '../../prompts';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { formatUpdate } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getManifestBodyAsync,
signManifestBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
import { createUpdateBranchOnAppAsync } from '../branch/create';
import { listBranchesAsync } from '../branch/list';
import { viewUpdateBranchAsync } from '../branch/view';
import { createUpdateChannelOnAppAsync } from '../channel/create';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
@@ -110,11 +125,11 @@ async function ensureBranchExistsAsync({
}): Promise<{
id: string;
updates: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'];
}> {
const { app } = await viewUpdateBranchAsync({
const { app } = await UpdateQuery.viewBranchAsync({
appId,
name: branchName,
});
@@ -132,7 +147,7 @@ async function ensureBranchExistsAsync({
}
export default class UpdatePublish extends EasCommand {
static description = 'Publish an update group.';
static description = 'publish an update group';
static flags = {
branch: Flags.string({
@@ -175,6 +190,14 @@ export default class UpdatePublish extends EasCommand {
'Use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
async runAsync(): Promise<void> {
@@ -189,11 +212,14 @@ export default class UpdatePublish extends EasCommand {
'input-dir': inputDir,
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
},
} = await this.parse(UpdatePublish);
if (jsonFlag) {
enableJsonOutput();
}
const platformFlag = platform as PlatformFlag;
// If a group was specified, that means we are republishing it.
republish = group ? true : republish;
@@ -204,6 +230,39 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: true,
});
const { exp: expPrivate } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: false,
});
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
if (!hasExpoUpdates && nonInteractive) {
Errors.error(
`${chalk.bold(
'expo-updates'
)} must already be installed when executing in non-interactive mode`,
{ exit: 1 }
);
}
if (!hasExpoUpdates) {
const install = await confirmAsync({
message: `You are creating an update which requires ${chalk.bold(
'expo-updates'
)} to be installed in your app.\n Do you want EAS CLI to install it for you?`,
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoUpdatesAsync(projectDir);
} else {
Errors.error(`Install ${chalk.bold('expo-updates')} manually and come back later.`, {
exit: 1,
});
}
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
await checkEASUpdateURLIsSetAsync(exp);
@@ -215,6 +274,10 @@ export default class UpdatePublish extends EasCommand {
}
if (!branchName) {
if (nonInteractive) {
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
}
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
@@ -264,6 +327,10 @@ export default class UpdatePublish extends EasCommand {
if (group) {
updatesToRepublish = await getUpdateGroupAsync({ group });
} else {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
// Drop into interactive mode if the user has not specified an update group to republish.
if (jsonFlag) {
throw new Error('You must specify the update group to republish.');
@@ -328,6 +395,10 @@ export default class UpdatePublish extends EasCommand {
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!message) {
if (nonInteractive) {
throw new Error('Must supply --message when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
@@ -346,6 +417,10 @@ export default class UpdatePublish extends EasCommand {
}
if (!message) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
@@ -392,30 +467,76 @@ export default class UpdatePublish extends EasCommand {
}
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Can not find the runtime version of the update group that is being republished.'
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Cannot find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message,
};
});
let newUpdates;
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
if (codeSigningInfo) {
Log.log('🔒 Signing updates');
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(0, Object.keys(updateGroup.updateInfoGroup).length);
return {
updateGroup,
newUpdates,
};
});
await Promise.all(
updateGroupsAndTheirUpdates.map(async ({ updateGroup, newUpdates }) => {
await Promise.all(
newUpdates.map(async newUpdate => {
const response = await fetch(newUpdate.manifestPermalink, {
method: 'GET',
headers: { accept: 'multipart/mixed' },
});
const manifestBody = nullthrows(await getManifestBodyAsync(response));
checkManifestBodyAgainstUpdateInfoGroup(
manifestBody,
nullthrows(
updateGroup.updateInfoGroup[newUpdate.platform as keyof UpdateInfoGroup]
)
);
const manifestSignature = signManifestBody(manifestBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
keyid: codeSigningInfo.codeSigningMetadata.keyid,
sig: manifestSignature,
});
})
);
})
);
}
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to published updates');
@@ -468,19 +589,27 @@ async function getRuntimeVersionObjectAsync(
if (isPolicy) {
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error('Runtime version policies are only supported in the managed workflow.');
throw new Error(
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
);
}
}
}
return Object.fromEntries(
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
platforms.map(platform => [
platform,
nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${platform}`
),
])
);
}
function formatUpdateTitle(
update: Exclude<
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
null | undefined
>['updates'][number]
): string {
@@ -0,0 +1,139 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import CliTable from 'cli-table3';
import EasCommand from '../../commandUtils/EasCommand';
import { ViewAllUpdatesQuery } from '../../graphql/generated';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { FormatUpdateParameter, UPDATE_COLUMNS, formatUpdate } from '../../update/utils';
import groupBy from '../../utils/expodash/groupBy';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getVcsClient } from '../../vcs';
type UpdateGroupDescription = FormatUpdateParameter & {
branch: string;
group: string;
platforms: string;
runtimeVersion: string;
};
export default class BranchView extends EasCommand {
static description = 'view the recent updates for a branch';
static flags = {
branch: Flags.string({
description: 'List all updates on this branch',
exclusive: ['all'],
}),
all: Flags.boolean({
description: 'List all updates associated with this project',
exclusive: ['branch'],
default: false,
}),
json: Flags.boolean({
description: `Return a json with all of the recent update groups.`,
default: false,
}),
};
async runAsync(): Promise<void> {
const {
flags: { branch: branchFlag, all, json: jsonFlag },
} = await this.parse(BranchView);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
let updateGroupDescriptions: UpdateGroupDescription[];
if (all) {
const branchesAndUpdates = await UpdateQuery.viewAllAsync({ appId: projectId });
updateGroupDescriptions = getUpdateGroupDescriptions(
branchesAndUpdates.app.byId.updateBranches
);
} else {
let branchInteractive: string | undefined;
if (!branchFlag) {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ name: branchInteractive } = await promptAsync({
type: 'text',
name: 'name',
message: 'Please enter the name of the branch whose updates you wish to view:',
initial: (await getVcsClient().getBranchNameAsync()) ?? undefined,
validate: (value: any) => (value ? true : validationMessage),
}));
}
const branch = branchFlag ?? branchInteractive;
assert(branch, 'Branch name may not be empty.');
const branchesAndUpdates = await UpdateQuery.viewBranchAsync({
appId: projectId,
name: branch,
});
const UpdateBranch = branchesAndUpdates.app?.byId.updateBranchByName;
if (!UpdateBranch) {
throw new Error(`Could not find branch "${branch}"`);
}
updateGroupDescriptions = getUpdateGroupDescriptions([UpdateBranch]);
}
if (jsonFlag) {
printJsonOnlyOutput(updateGroupDescriptions);
} else {
logAsTable(updateGroupDescriptions);
}
}
}
function getUpdateGroupDescriptions(
branchesAndUpdates: ViewAllUpdatesQuery['app']['byId']['updateBranches']
): UpdateGroupDescription[] {
const flattenedBranchesAndUpdates = branchesAndUpdates.flatMap(branch =>
branch.updates.map(update => {
return { branch: branch.name, ...update };
})
);
const updateGroupDescriptions = Object.values(
groupBy(flattenedBranchesAndUpdates, update => update.group)
).map(updateGroup => {
const platforms = updateGroup
.map(update => update.platform)
.sort()
.join(', ');
return { ...updateGroup[0], platforms };
});
updateGroupDescriptions.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
return updateGroupDescriptions;
}
function logAsTable(updateGroupDescriptions: UpdateGroupDescription[]): void {
const table = new CliTable({
head: ['Branch', ...UPDATE_COLUMNS],
wordWrap: true,
});
table.push(
...updateGroupDescriptions.map(updateGroupDescription => [
updateGroupDescription.branch,
formatUpdate(updateGroupDescription),
updateGroupDescription.runtimeVersion,
updateGroupDescription.group,
updateGroupDescription.platforms,
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('Recently published update groups:'));
Log.log(table.toString());
}
+1 -1
View File
@@ -51,7 +51,7 @@ export async function viewUpdateAsync({
return data;
}
export default class UpdateView extends EasCommand {
static description = 'Update group details.';
static description = 'update group details';
static args = [
{
@@ -9,7 +9,7 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookCreate extends EasCommand {
static description = 'Create a webhook on the current project.';
static description = 'create a webhook';
static flags = {
event: Flags.enum({
@@ -14,7 +14,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookDelete extends EasCommand {
static description = 'Delete a webhook on the current project.';
static description = 'delete a webhook';
static args = [
{
@@ -15,7 +15,7 @@ import {
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookList extends EasCommand {
static description = 'List webhooks on the current project.';
static description = 'list webhooks';
static flags = {
event: Flags.enum({
@@ -9,7 +9,7 @@ import pick from '../../utils/expodash/pick';
import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookUpdate extends EasCommand {
static description = 'Create a webhook on the current project.';
static description = 'update a webhook';
static flags = {
id: Flags.string({
@@ -5,7 +5,7 @@ import { ora } from '../../ora';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookView extends EasCommand {
static description = 'View a webhook on the current project.';
static description = 'view a webhook';
static args = [
{
@@ -1,5 +1,5 @@
import AppStoreApi from '../ios/appstore/AppStoreApi';
import { AuthCtx } from '../ios/appstore/authenticate';
import { AuthCtx } from '../ios/appstore/authenticateTypes';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
@@ -1,5 +1,8 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
@@ -41,14 +44,33 @@ export class CreateGoogleServiceAccountKey {
return detectedPath;
}
const { keyJsonPath } = await promptAsync([
{
type: 'text',
name: 'keyJsonPath',
message: 'Path to Google Service Account Key JSON file:',
validate: (value: string) => value.length > 0 || "Path can't be empty",
Log.log(
`${chalk.bold(
'A Google Service Account JSON key is required to upload your app to Google Play Store'
)}.\n` +
`If you're not sure what this is or how to create one, ${learnMore(
'https://expo.fyi/creating-google-service-account',
{ learnMoreMessage: 'learn more' }
)}`
);
const { filePath } = await promptAsync({
name: 'filePath',
message: 'Path to Google Service Account file:',
initial: 'api-0000000000000000000-111111-aaaaaabbbbbb.json',
type: 'text',
// eslint-disable-next-line async-protect/async-suffix
validate: async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
return true;
}
return 'Input is not a file.';
} catch {
return 'File does not exist.';
}
},
]);
return keyJsonPath;
});
return filePath;
}
}
@@ -9,7 +9,7 @@ import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey'
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
filePath: '/google-service-account-key.json',
}));
beforeEach(() => {
@@ -15,7 +15,7 @@ import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
filePath: '/google-service-account-key.json',
}));
beforeEach(() => {
@@ -2,14 +2,11 @@ import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
// (dsokal) We actually want to use node-fetch but the change is in progress.
// See https://github.com/expo/eas-cli/issues/32 for context.
// eslint-disable-next-line no-restricted-imports
import fetch from 'node-fetch';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { Analytics, BuildEvent } from '../../../analytics/events';
import fetch from '../../../fetch';
import { AndroidKeystoreType } from '../../../graphql/generated';
import { KeystoreGenerationUrlMutation } from '../../../graphql/mutations/KeystoreGenerationUrlMutation';
import Log from '../../../log';
@@ -149,7 +146,7 @@ async function createKeystoreInCloudAsync(
body: JSON.stringify(keystoreParams),
headers: { 'Content-Type': 'application/json' },
});
const result: KeystoreServiceResult = await response.json();
const result = (await response.json()) as KeystoreServiceResult;
spinner.succeed();
return {
type: AndroidKeystoreType.Jks,
+8 -2
View File
@@ -10,6 +10,7 @@ import { Actor, getActorDisplayName } from '../user/User';
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';
export class CredentialsContext {
public readonly android = AndroidGraphqlClient;
@@ -99,6 +100,11 @@ export class CredentialsContext {
return;
}
if (this.appStore.defaultAuthenticationMode === AuthenticationMode.API_KEY) {
await this.appStore.ensureAuthenticatedAsync();
return;
}
Log.log(
chalk.green(
'If you provide your Apple account credentials we will be able to generate all necessary build credentials and fully validate them.'
@@ -106,7 +112,7 @@ export class CredentialsContext {
);
Log.log(
chalk.green(
'This is optional, but without Apple account access you will need to provide all the values manually and we can only run minimal validation on them.'
'This is optional, but without Apple account access you will need to provide all the missing values manually and we can only run minimal validation on them.'
)
);
const confirm = await confirmAsync({
@@ -117,7 +123,7 @@ export class CredentialsContext {
} else {
Log.log(
chalk.green(
'No problem! 👌 If you select an action that requires those credentials we will ask you again about it.'
'No problem! 👌 If any of the next steps will require Apple account access we will ask you again about it.'
)
);
}
@@ -94,7 +94,6 @@ async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
//console.log('debug');
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
@@ -12,7 +12,7 @@ import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticate';
import { AuthCtx } from '../appstore/authenticateTypes';
export class ConfigureProvisioningProfile {
constructor(
@@ -84,7 +84,7 @@ export class ConfigureProvisioningProfile {
certP12: certificateP12,
certPassword: certificatePassword,
distCertSerialNumber: serialNumber,
teamId: authCtx.appleId,
teamId: authCtx.team.id,
}
);
@@ -9,7 +9,7 @@ import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { ProvisioningProfile } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticate';
import { AuthCtx } from '../appstore/authenticateTypes';
import { provisioningProfileSchema } from '../credentials';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { generateProvisioningProfileAsync } from './ProvisioningProfileUtils';
@@ -68,7 +68,7 @@ export class CreateProvisioningProfile {
certP12: this.distributionCertificate.certificateP12,
certPassword: this.distributionCertificate.certificatePassword,
distCertSerialNumber: this.distributionCertificate.serialNumber,
teamId: this.distributionCertificate.appleTeam?.appleTeamIdentifier ?? appleAuthCtx.appleId,
teamId: this.distributionCertificate.appleTeam?.appleTeamIdentifier ?? appleAuthCtx.team.id,
});
}
}
@@ -26,6 +26,23 @@ export async function chooseDevicesAsync(
return devices;
}
export async function chooseDevicesToDeleteAsync(
allDevices: AppleDeviceFragment[]
): Promise<AppleDevice[]> {
const { devices } = await promptAsync({
type: 'multiselect',
name: 'devices',
message: 'Which devices do you want to remove?',
choices: allDevices.map(device => ({
value: device,
title: formatDeviceLabel(device),
selected: false,
})),
instructions: false,
});
return devices;
}
export function formatDeviceLabel(device: AppleDeviceFragment): string {
const deviceDetails = formatDeviceDetails(device);
return `${device.name ?? device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}`;
@@ -11,7 +11,7 @@ export class SelectAndRemoveAscApiKey {
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
if (selected) {
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
await new RemoveAscApiKey(selected).runAsync(ctx);
Log.succeed('Removed App Store Connect API Key');
Log.newLine();
}
@@ -19,7 +19,7 @@ export class SelectAndRemoveAscApiKey {
}
export class RemoveAscApiKey {
constructor(private account: Account, private ascApiKey: AppStoreConnectApiKeyFragment) {}
constructor(private ascApiKey: AppStoreConnectApiKeyFragment) {}
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
@@ -11,7 +11,7 @@ export class SelectAndRemovePushKey {
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectPushKeyAsync(ctx, this.account);
if (selected) {
await new RemovePushKey(this.account, selected).runAsync(ctx);
await new RemovePushKey(selected).runAsync(ctx);
Log.succeed('Removed push key');
Log.newLine();
}
@@ -19,7 +19,7 @@ export class SelectAndRemovePushKey {
}
export class RemovePushKey {
constructor(private account: Account, private pushKey: ApplePushKeyFragment) {}
constructor(private pushKey: ApplePushKeyFragment) {}
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
@@ -47,7 +47,7 @@ export class SetUpDistributionCertificate {
}
private async runNonInteractiveAsync(
ctx: CredentialsContext,
_ctx: CredentialsContext,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<AppleDistributionCertificateFragment> {
// TODO: implement validation
@@ -1,5 +1,5 @@
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
@@ -51,6 +51,11 @@ export class SetUpInternalProvisioningProfile {
} else if (enterpriseBuildCredentialsExist) {
return await this.setupUniversalProvisioningProfileAsync(ctx);
} else {
Log.addNewLineIfNone();
Log.log(
'You need to log in to your Apple Developer account to generate credentials for internal distribution builds, or provide credentials via credentials.json'
);
Log.log(learnMore('https://docs.expo.dev/app-signing/local-credentials/'));
const { team } = await ctx.appStore.ensureAuthenticatedAsync();
if (team.inHouse) {
return await this.askForDistributionTypeAndSetupAsync(
@@ -1,21 +1,10 @@
import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
} from '../../../../graphql/generated';
import { testProvisioningProfileFragment } from '../../../__tests__/fixtures-ios';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../../api/graphql/mutations/AppleProvisioningProfileMutation';
export class ConfigureProvisioningProfile {
constructor(
private app: AppLookupParams,
private distributionCertificate: AppleDistributionCertificateFragment,
private originalProvisioningProfile: AppleProvisioningProfileFragment
) {}
public async runAsync(
ctx: CredentialsContext
_ctx: CredentialsContext
): Promise<AppleProvisioningProfileMutationResult | null> {
return testProvisioningProfileFragment;
}
@@ -1,16 +1,9 @@
import { AppleDistributionCertificateFragment } from '../../../../graphql/generated';
import { testProvisioningProfileFragment } from '../../../__tests__/fixtures-ios';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { AppleProvisioningProfileMutationResult } from '../../api/graphql/mutations/AppleProvisioningProfileMutation';
export class CreateProvisioningProfile {
constructor(
private app: AppLookupParams,
private distributionCertificate: AppleDistributionCertificateFragment
) {}
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
async runAsync(_ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
return testProvisioningProfileFragment;
}
}
@@ -1,13 +1,7 @@
import {
AppleDistributionCertificateFragment,
IosDistributionType,
} from '../../../../graphql/generated';
import { AppleDistributionCertificateFragment } from '../../../../graphql/generated';
import { testDistCertFragmentOneDependency } from '../../../__tests__/fixtures-ios';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export class SetUpDistributionCertificate {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public async runAsync(_ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
return testDistCertFragmentOneDependency;
}
@@ -1,9 +1,7 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { testAscApiKeyFragment } from '../../../__tests__/fixtures-ios';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
@@ -12,22 +10,14 @@ asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
const removeAscApiKeyAction = new RemoveAscApiKey(testAscApiKeyFragment);
await removeAscApiKeyAction.runAsync(ctx);
expect(ctx.ios.deleteAscApiKeyAsync).toHaveBeenCalledTimes(1);
expect(ctx.appStore.revokeAscApiKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
const removeAscApiKeyAction = new RemoveAscApiKey(testAscApiKeyFragment);
await expect(removeAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -1,9 +1,7 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testPushKey, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { testPushKey } from '../../../__tests__/fixtures-ios';
import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
@@ -12,8 +10,7 @@ asMock(confirmAsync).mockImplementation(() => true);
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removePushKeyAction = new RemovePushKey(appLookupParams.account, testPushKey);
const removePushKeyAction = new RemovePushKey(testPushKey);
await removePushKeyAction.runAsync(ctx);
// expect push key to be deleted on expo servers
@@ -23,8 +20,7 @@ describe(RemovePushKey, () => {
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removePushKeyAction = new RemovePushKey(appLookupParams.account, testPushKey);
const removePushKeyAction = new RemovePushKey(testPushKey);
await expect(removePushKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -6,6 +6,7 @@ import {
AppleDeviceFragment,
AppleDeviceInput,
CreateAppleDeviceMutation,
DeleteAppleDeviceResult,
} from '../../../../../graphql/generated';
import { AppleDeviceFragmentNode } from '../../../../../graphql/types/credentials/AppleDevice';
@@ -40,4 +41,25 @@ export const AppleDeviceMutation = {
);
return data.appleDevice.createAppleDevice;
},
async deleteAppleDeviceAsync(deviceId: string): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteAppleDeviceResult>(
gql`
mutation DeleteAppleDeviceMutation($deviceId: ID!) {
appleDevice {
deleteAppleDevice(id: $deviceId) {
id
}
}
}
`,
{
deviceId,
}
)
.toPromise()
);
return data.id;
},
};
@@ -1,3 +1,4 @@
import Log from '../../../log';
import {
AscApiKey,
AscApiKeyInfo,
@@ -14,7 +15,13 @@ import {
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from './ascApiKey';
import { AuthCtx, Options as AuthenticateOptions, authenticateAsync } from './authenticate';
import {
Options as AuthenticateOptions,
assertUserAuthCtx,
authenticateAsync,
isUserAuthCtx,
} from './authenticate';
import { AuthCtx, AuthenticationMode, UserAuthCtx } from './authenticateTypes';
import {
createDistributionCertificateAsync,
listDistributionCertificatesAsync,
@@ -34,13 +41,36 @@ import {
} from './provisioningProfile';
import { createOrReuseAdhocProvisioningProfileAsync } from './provisioningProfileAdhoc';
import { createPushKeyAsync, listPushKeysAsync, revokePushKeyAsync } from './pushKey';
import { hasAscEnvVars } from './resolveCredentials';
export default class AppStoreApi {
public authCtx?: AuthCtx;
public defaultAuthenticationMode: AuthenticationMode;
constructor() {
this.defaultAuthenticationMode = hasAscEnvVars()
? AuthenticationMode.API_KEY
: AuthenticationMode.USER;
}
public async ensureUserAuthenticatedAsync(options?: AuthenticateOptions): Promise<UserAuthCtx> {
if (this.authCtx && !isUserAuthCtx(this.authCtx)) {
// already authenticated, but with the wrong type
Log.log(`Only user authentication is supported. Reauthenticating as user...`);
this.authCtx = undefined;
}
const updatedAuthCtx = await this.ensureAuthenticatedAsync({
...options,
mode: AuthenticationMode.USER,
});
return assertUserAuthCtx(updatedAuthCtx);
}
public async ensureAuthenticatedAsync(options?: AuthenticateOptions): Promise<AuthCtx> {
if (!this.authCtx) {
this.authCtx = await authenticateAsync(options);
const mode = options?.mode ?? this.defaultAuthenticationMode;
this.authCtx = await authenticateAsync({ mode, ...options });
}
return this.authCtx;
}
@@ -69,33 +99,31 @@ export default class AppStoreApi {
}
public async listPushKeysAsync(): Promise<PushKeyStoreInfo[]> {
const ctx = await this.ensureAuthenticatedAsync();
return await listPushKeysAsync(ctx);
const userCtx = await this.ensureUserAuthenticatedAsync();
return await listPushKeysAsync(userCtx);
}
public async createPushKeyAsync(name?: string): Promise<PushKey> {
const ctx = await this.ensureAuthenticatedAsync();
return await createPushKeyAsync(ctx, name);
const userCtx = await this.ensureUserAuthenticatedAsync();
return await createPushKeyAsync(userCtx, name);
}
public async revokePushKeyAsync(ids: string[]): Promise<void> {
const ctx = await this.ensureAuthenticatedAsync();
return await revokePushKeyAsync(ctx, ids);
const userCtx = await this.ensureUserAuthenticatedAsync();
return await revokePushKeyAsync(userCtx, ids);
}
public async useExistingProvisioningProfileAsync(
bundleIdentifier: string,
provisioningProfile: ProvisioningProfile,
distCert: DistributionCertificate,
profileClass?: ProfileClass
distCert: DistributionCertificate
): Promise<ProvisioningProfile> {
const ctx = await this.ensureAuthenticatedAsync();
return await useExistingProvisioningProfileAsync(
ctx,
bundleIdentifier,
provisioningProfile,
distCert,
profileClass
distCert
);
}
@@ -146,22 +174,22 @@ export default class AppStoreApi {
}
public async listAscApiKeysAsync(): Promise<AscApiKeyInfo[]> {
const ctx = await this.ensureAuthenticatedAsync();
return await listAscApiKeysAsync(ctx);
const userCtx = await this.ensureUserAuthenticatedAsync();
return await listAscApiKeysAsync(userCtx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo | null> {
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
const userCtx = await this.ensureUserAuthenticatedAsync();
return await getAscApiKeyAsync(userCtx, keyId);
}
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
const ctx = await this.ensureAuthenticatedAsync();
return await createAscApiKeyAsync(ctx, { nickname });
const userCtx = await this.ensureUserAuthenticatedAsync();
return await createAscApiKeyAsync(userCtx, { nickname });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const ctx = await this.ensureAuthenticatedAsync();
return await revokeAscApiKeyAsync(ctx, keyId);
const userCtx = await this.ensureUserAuthenticatedAsync();
return await revokeAscApiKeyAsync(userCtx, keyId);
}
}
@@ -1,4 +1,4 @@
import { BundleIdCapability, CapabilityType } from '@expo/apple-utils';
import { BundleIdCapability } from '@expo/apple-utils';
import {
CapabilityMapping,
@@ -222,10 +222,6 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_IN_APP_PURCHASE', { settings: null }),
new BundleIdCapability({}, 'FOOBAR', {
settings: null,
capabilityType: CapabilityType.PUSH_NOTIFICATIONS,
}),
new BundleIdCapability({}, 'XXX_GAME_CENTER', { settings: null }),
]),
updateBundleIdCapabilityAsync: jest.fn(),
@@ -10,7 +10,7 @@ function mockCapabilities(Apple: any): void {
},
]);
Apple.MerchantId.createAsync = jest.fn((ctx, { identifier }) => ({
Apple.MerchantId.createAsync = jest.fn((_ctx, { identifier }) => ({
id: 'XXX-merch-2',
attributes: {
identifier,
@@ -25,7 +25,7 @@ function mockCapabilities(Apple: any): void {
},
},
]);
Apple.AppGroup.createAsync = jest.fn((ctx, { identifier }) => ({
Apple.AppGroup.createAsync = jest.fn((_ctx, { identifier }) => ({
id: 'XXX-group-2',
attributes: {
identifier,
@@ -41,7 +41,7 @@ function mockCapabilities(Apple: any): void {
},
]);
Apple.CloudContainer.createAsync = jest.fn((ctx, { identifier }) => ({
Apple.CloudContainer.createAsync = jest.fn((_ctx, { identifier }) => ({
id: 'XXX-icloud-2',
attributes: {
identifier,
@@ -150,7 +150,7 @@ describe(syncCapabilityIdentifiersForEntitlementsAsync, () => {
const Apple = require('@expo/apple-utils');
mockCapabilities(Apple);
Apple.MerchantId.createAsync = jest.fn(
(ctx: any, { identifier }: { identifier: string }): any => {
(_ctx: any, { identifier }: { identifier: string }): any => {
// e2e test with: merchant.expodemo
throw new Error(
`There is a problem with the request entity - A Merchant ID with Identifier '${identifier}' is not available. Please enter a different string.`
@@ -0,0 +1,92 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { AppleTeamType } from '../authenticateTypes';
import { resolveAppleTeamAsync, resolveAscApiKeyAsync } from '../resolveCredentials';
jest.mock('../../../../prompts');
jest.mock('fs');
const testAscApiKey = {
keyP8: 'test-key-p8',
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
};
beforeEach(() => {
vol.reset();
process.env = {};
asMock(promptAsync).mockReset();
});
describe(resolveAscApiKeyAsync, () => {
it(`uses option overrides over environment variables`, async () => {
process.env.EXPO_ASC_API_KEY_PATH = 'not supposed to be here';
process.env.EXPO_ASC_KEY_ID = 'not supposed to be here';
process.env.EXPO_ASC_ISSUER_ID = 'not supposed to be here';
const ascApiKey = await resolveAscApiKeyAsync(testAscApiKey);
expect(ascApiKey).toMatchObject(testAscApiKey);
});
it(`uses environment variables if no option overrides are provided`, async () => {
vol.fromJSON({
'/test-asc-key.p8': testAscApiKey.keyP8,
});
process.env.EXPO_ASC_API_KEY_PATH = '/test-asc-key.p8';
process.env.EXPO_ASC_KEY_ID = testAscApiKey.keyId;
process.env.EXPO_ASC_ISSUER_ID = testAscApiKey.issuerId;
const ascApiKey = await resolveAscApiKeyAsync();
expect(ascApiKey).toMatchObject(testAscApiKey);
});
it(`prompts the user if it can't find anything else`, async () => {
vol.fromJSON({
'/test-asc-key.p8': testAscApiKey.keyP8,
});
asMock(promptAsync).mockImplementation(() => ({
ascApiKeyPath: '/test-asc-key.p8',
ascIssuerId: testAscApiKey.issuerId,
ascApiKeyId: testAscApiKey.keyId,
}));
const ascApiKey = await resolveAscApiKeyAsync();
expect(ascApiKey).toMatchObject(testAscApiKey);
});
});
const testTeam = {
teamId: 'test-id',
teamName: 'test-name',
teamType: AppleTeamType.IN_HOUSE,
};
describe(resolveAppleTeamAsync, () => {
it(`uses option overrides over environment variables`, async () => {
process.env.EXPO_APPLE_TEAM_ID = 'not supposed to be here';
process.env.EXPO_APPLE_TEAM_TYPE = 'not supposed to be here';
const team = await resolveAppleTeamAsync(testTeam);
expect(team).toMatchObject({ id: testTeam.teamId, name: testTeam.teamName, inHouse: true });
});
it(`uses environment variables if no option overrides are provided`, async () => {
process.env.EXPO_APPLE_TEAM_ID = testTeam.teamId;
process.env.EXPO_APPLE_TEAM_TYPE = testTeam.teamType;
const team = await resolveAppleTeamAsync();
expect(team).toMatchObject({
id: testTeam.teamId,
name: undefined,
inHouse: true,
});
});
it(`prompts the user if it can't find anything else`, async () => {
asMock(promptAsync).mockImplementation(() => ({
appleTeamId: testTeam.teamId,
appleTeamType: testTeam.teamType,
}));
const team = await resolveAppleTeamAsync();
expect(team).toMatchObject({
id: testTeam.teamId,
name: undefined,
inHouse: true,
});
});
});
@@ -3,31 +3,40 @@ import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
import Log from '../../../log';
import { ora } from '../../../ora';
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
import { getRequestContext } from './authenticate';
import { AuthCtx, UserAuthCtx } from './authenticateTypes';
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
/**
* List App Store Connect API Keys.
* **Does not support App Store Connect API (CI).**
*/
export async function listAscApiKeysAsync(userAuthCtx: UserAuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
return keys.map(key => getAscApiKeyInfo(key, authCtx));
return keys.map(key => getAscApiKeyInfo(key, userAuthCtx));
} catch (error) {
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
throw error;
}
}
/**
* Get an App Store Connect API Key.
* **Does not support App Store Connect API (CI).**
*/
export async function getAscApiKeyAsync(
authCtx: AuthCtx,
userAuthCtx: UserAuthCtx,
keyId: string
): Promise<AscApiKeyInfo | null> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
return getAscApiKeyInfo(apiKey, userAuthCtx);
} catch (error: any) {
const message = error?.message ?? '';
if (message.includes("There is no resource of type 'apiKeys' with id")) {
@@ -40,8 +49,12 @@ export async function getAscApiKeyAsync(
}
}
/**
* Create an App Store Connect API Key.
* **Does not support App Store Connect API (CI).**
*/
export async function createAscApiKeyAsync(
authCtx: AuthCtx,
userAuthCtx: UserAuthCtx,
{
nickname,
allAppsVisible,
@@ -51,7 +64,7 @@ export async function createAscApiKeyAsync(
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const key = await ApiKey.createAsync(context, {
nickname: nickname ?? `[expo] ${new Date().getTime()}`,
allAppsVisible: allAppsVisible ?? true,
@@ -76,7 +89,7 @@ export async function createAscApiKeyAsync(
const fullKey = await ApiKey.infoAsync(context, { id: key.id });
spinner.succeed(`Created App Store Connect API Key.`);
return {
...getAscApiKeyInfo(fullKey, authCtx),
...getAscApiKeyInfo(fullKey, userAuthCtx),
keyP8,
};
} catch (err: any) {
@@ -85,17 +98,21 @@ export async function createAscApiKeyAsync(
}
}
/**
* Revoke an App Store Connect API Key.
* **Does not support App Store Connect API (CI).**
*/
export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
userAuthCtx: UserAuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
return getAscApiKeyInfo(revokedKey, authCtx);
return getAscApiKeyInfo(revokedKey, userAuthCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
@@ -4,16 +4,28 @@ import {
RequestContext,
Session,
Teams,
Token,
} from '@expo/apple-utils';
import assert from 'assert';
import chalk from 'chalk';
import Log from '../../../log';
import { toggleConfirmAsync } from '../../../prompts';
import { MinimalAscApiKey } from '../credentials';
import {
ApiKeyAuthCtx,
AppleTeamType,
AuthCtx,
AuthenticationMode,
Team,
UserAuthCtx,
} from './authenticateTypes';
import {
deletePasswordAsync,
promptPasswordAsync,
resolveCredentialsAsync,
resolveAppleTeamAsync,
resolveAscApiKeyAsync,
resolveUserCredentialsAsync,
} from './resolveCredentials';
const APPLE_IN_HOUSE_TEAM_TYPE = 'in-house';
@@ -21,31 +33,27 @@ const APPLE_IN_HOUSE_TEAM_TYPE = 'in-house';
export type Options = {
appleId?: string;
teamId?: string;
teamName?: string;
teamType?: AppleTeamType;
ascApiKey?: MinimalAscApiKey;
/**
* Can be used to restore the Apple auth state via apple-utils.
*/
cookies?: Session.AuthState['cookies'];
/** Indicates how Apple network requests will be made. */
mode?: AuthenticationMode;
};
export type Team = {
id: string;
name: string;
inHouse?: boolean;
};
export function isUserAuthCtx(authCtx: AuthCtx | undefined): authCtx is UserAuthCtx {
return !!authCtx && typeof (authCtx as UserAuthCtx).appleId === 'string';
}
export type AuthCtx = {
appleId: string;
appleIdPassword?: string;
team: Team;
/**
* Defined when using Fastlane
*/
fastlaneSession?: string;
/**
* Can be used to restore the Apple auth state via apple-utils.
*/
authState?: Session.AuthState;
};
export function assertUserAuthCtx(authCtx: AuthCtx | undefined): UserAuthCtx {
if (isUserAuthCtx(authCtx)) {
return authCtx;
}
throw new Error('Expected user authentication context (login/password).');
}
export function getRequestContext(authCtx: AuthCtx): RequestContext {
assert(authCtx.authState?.context, 'Apple request context must be defined');
@@ -67,7 +75,7 @@ async function loginAsync(
}
// Resolve the user credentials, optimizing for password-less login.
const { username, password } = await resolveCredentialsAsync(userCredentials);
const { username, password } = await resolveUserCredentialsAsync(userCredentials);
assert(username);
// Clear data
@@ -144,6 +152,35 @@ async function loginWithUserCredentialsAsync({
}
export async function authenticateAsync(options: Options = {}): Promise<AuthCtx> {
if (options.mode === AuthenticationMode.API_KEY) {
return await authenticateWithApiKeyAsync(options);
} else {
return await authenticateAsUserAsync(options);
}
}
async function authenticateWithApiKeyAsync(options: Options = {}): Promise<ApiKeyAuthCtx> {
// Resolve the user credentials, optimizing for password-less login.
const ascApiKey = await resolveAscApiKeyAsync(options.ascApiKey);
const team = await resolveAppleTeamAsync(options);
const jwtDurationSeconds = 1200; // 20 minutes
return {
team,
authState: {
context: {
token: new Token({
key: ascApiKey.keyP8,
issuerId: ascApiKey.issuerId,
keyId: ascApiKey.keyId,
duration: jwtDurationSeconds,
}),
},
},
ascApiKey,
};
}
async function authenticateAsUserAsync(options: Options = {}): Promise<AuthCtx> {
// help keep apple login visually apart from the other operations.
Log.addNewLineIfNone();
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more