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
144 changed files with 5878 additions and 3898 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
+120 -2
View File
@@ -8,11 +8,129 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 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))
### 🐛 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))
+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.45.1"
"version": "0.51.0"
}
+8 -7
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.4.0",
"@types/jest": "27.4.1",
"@types/node": "16.7.8",
"eslint": "8.7.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.4.7",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"lerna": "4.0.0",
"oclif": "2.2.0",
"prettier": "2.5.1",
"ts-jest": "27.1.3",
"typescript": "4.5.4"
"oclif": "3.0.1",
"prettier": "2.6.1",
"ts-jest": "27.1.4",
"typescript": "4.6.3"
},
"volta": {
"node": "16.10.0",
+211 -117
View File
@@ -48,6 +48,7 @@ eas --help COMMAND
* [`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)
@@ -61,6 +62,9 @@ eas --help COMMAND
* [`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)
@@ -77,6 +81,7 @@ eas --help COMMAND
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
## `eas account:login`
@@ -93,7 +98,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/account/login.ts)_
_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`
@@ -110,7 +115,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/account/logout.ts)_
_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`
@@ -127,21 +132,21 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/account/view.ts)_
_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]`
view or change analytics settings
display or change analytics settings
```
USAGE
$ eas analytics [STATUS]
DESCRIPTION
view or change analytics settings
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/analytics.ts)_
_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]`
@@ -170,11 +175,11 @@ EXAMPLES
$ eas autocomplete --refresh-cache
```
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.1.1/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 on the current project.
create a branch
```
USAGE
@@ -187,14 +192,14 @@ FLAGS
--json return a json with the new branch ID and name.
DESCRIPTION
Create a branch on the current project.
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/branch/create.ts)_
_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 on the current project
delete a branch
```
USAGE
@@ -207,14 +212,14 @@ FLAGS
--json return JSON with the edited branch's ID and name.
DESCRIPTION
Delete a branch on the current project
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/branch/delete.ts)_
_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 on this project.
list all branches
```
USAGE
@@ -224,14 +229,14 @@ FLAGS
--json return output as JSON
DESCRIPTION
List all branches on this project.
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/branch/list.ts)_
_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.
rename a branch
```
USAGE
@@ -243,14 +248,14 @@ FLAGS
--to=<value> new name of the branch.
DESCRIPTION
Rename a branch.
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/branch/rename.ts)_
_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.
view a branch
```
USAGE
@@ -263,20 +268,19 @@ FLAGS
--json return a json with the branch's ID name and recent update groups.
DESCRIPTION
View a branch.
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/branch/view.ts)_
_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
start a build
```
USAGE
$ eas build [-p android|ios|all] [--json] [--skip-project-configuration] [--profile <value>]
[--non-interactive] [--local] [--output <value>] [--wait] [--clear-cache] [--auto-submit |
--auto-submit-with-profile <value>]
$ 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)
@@ -290,32 +294,31 @@ FLAGS
--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.
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
DESCRIPTION
Start a build
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/build/index.ts)_
_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.
cancel a build
```
USAGE
$ eas build:cancel [BUILD_ID]
DESCRIPTION
Cancel a build.
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/build/cancel.ts)_
_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.
configure the project to support EAS Build
```
USAGE
@@ -325,14 +328,14 @@ FLAGS
-p, --platform=(android|ios|all) Platform to configure
DESCRIPTION
Configure the project to support EAS Build.
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/build/configure.ts)_
_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.
inspect the state of the project at specific build stages, useful for troubleshooting
```
USAGE
@@ -360,10 +363,10 @@ FLAGS
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.
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.45.1/packages/eas-cli/src/commands/build/inspect.ts)_
_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`
@@ -396,7 +399,35 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/build/list.ts)_
_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]`
@@ -413,11 +444,11 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/build/view.ts)_
_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 on the current project.
create a channel
```
USAGE
@@ -430,14 +461,14 @@ FLAGS
--json print output as a JSON object with the new channel ID, name and branch mapping.
DESCRIPTION
Create a channel on the current project.
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/channel/create.ts)_
_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.
point a channel at a new branch
```
USAGE
@@ -448,17 +479,17 @@ ARGUMENTS
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.
--json Print output as a JSON object with the channel ID, name and branch mapping
DESCRIPTION
Point a channel at a new branch.
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/channel/edit.ts)_
_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 on the current project.
list all channels
```
USAGE
@@ -468,14 +499,14 @@ FLAGS
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
List all channels on the current project.
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/channel/list.ts)_
_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 on the current project.
view a channel
```
USAGE
@@ -488,14 +519,14 @@ FLAGS
--json print output as a JSON object with the channel ID, name and branch mapping.
DESCRIPTION
View a channel on the current project.
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/channel/view.ts)_
_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`
show the eas.json config
display project configuration (app.json + eas.json)
```
USAGE
@@ -506,24 +537,24 @@ FLAGS
--profile=<value>
DESCRIPTION
show the eas.json config
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/config.ts)_
_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 your credentials
manage credentials
```
USAGE
$ eas credentials
DESCRIPTION
manage your credentials
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/credentials.ts)_
_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`
@@ -537,7 +568,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/device/create.ts)_
_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`
@@ -555,7 +586,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/device/delete.ts)_
_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`
@@ -572,7 +603,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/device/list.ts)_
_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]`
@@ -586,21 +617,21 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/device/view.ts)_
_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`
log environment info to the console
display environment info
```
USAGE
$ eas diagnostics
DESCRIPTION
log environment info to the console
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/diagnostics.ts)_
_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]`
@@ -620,7 +651,52 @@ 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`
@@ -634,7 +710,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/project/info.ts)_
_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`
@@ -651,11 +727,11 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/project/init.ts)_
_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.
create an environment secret on the current project or owner account
```
USAGE
@@ -668,14 +744,14 @@ FLAGS
--value=<value> Value of the secret
DESCRIPTION
Create an environment secret on the current project or owner account.
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.45.1/packages/eas-cli/src/commands/secret/create.ts)_
_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.
delete an environment secret by ID
```
USAGE
@@ -685,30 +761,28 @@ FLAGS
--id=<value> ID of the secret to delete
DESCRIPTION
Delete an environment secret by ID.
Unsure where to find the secret's ID? Run eas secret:list
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/secret/delete.ts)_
_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`
Lists environment secrets available for your current app
list environment secrets available for your current app
```
USAGE
$ eas secret:list
DESCRIPTION
Lists environment secrets available for your current app
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/secret/list.ts)_
_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 build archive to App Store Connect
submit app binary to App Store and/or Play Store
```
USAGE
@@ -728,24 +802,22 @@ FLAGS
--[no-]wait Wait for submission to complete
DESCRIPTION
Submit build archive to App Store Connect
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
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.45.1/packages/eas-cli/src/commands/submit.ts)_
_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.
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]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all]
@@ -755,32 +827,39 @@ FLAGS
--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.
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/update/index.ts)_
_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.
configure the project to support EAS Update
```
USAGE
$ eas update:configure
$ 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.
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/update/configure.ts)_
_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.
delete all the updates in an update group
```
USAGE
@@ -793,14 +872,14 @@ FLAGS
--json Return a json with the group ID of the deleted updates.
DESCRIPTION
Delete all the updates in an update Group.
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/update/delete.ts)_
_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
view the recent updates for a branch
```
USAGE
@@ -812,14 +891,14 @@ FLAGS
--json Return a json with all of the recent update groups.
DESCRIPTION
View the recent updates for a branch
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/update/list.ts)_
_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.
update group details
```
USAGE
@@ -832,14 +911,14 @@ FLAGS
--json Return a json with the updates belonging to the group.
DESCRIPTION
Update group details.
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/update/view.ts)_
_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 on the current project.
create a webhook
```
USAGE
@@ -852,14 +931,14 @@ FLAGS
--url=<value> Webhook URL
DESCRIPTION
Create a webhook on the current project.
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/webhook/create.ts)_
_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 on the current project.
delete a webhook
```
USAGE
@@ -869,14 +948,14 @@ ARGUMENTS
ID ID of the webhook to delete
DESCRIPTION
Delete a webhook on the current project.
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_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 on the current project.
list webhooks
```
USAGE
@@ -886,14 +965,14 @@ FLAGS
--event=(BUILD|SUBMIT) Event type that triggers the webhook
DESCRIPTION
List webhooks on the current project.
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/webhook/list.ts)_
_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 on the current project.
update a webhook
```
USAGE
@@ -907,14 +986,14 @@ FLAGS
--url=<value> Webhook URL
DESCRIPTION
Update a webhook on the current project.
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/webhook/update.ts)_
_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 on the current project.
view a webhook
```
USAGE
@@ -924,8 +1003,23 @@ ARGUMENTS
ID ID of the webhook to view
DESCRIPTION
View a webhook on the current project.
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.45.1/packages/eas-cli/src/commands/webhook/view.ts)_
_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);
+998 -184
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',
+47 -36
View File
@@ -1,57 +1,63 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.45.1",
"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.27",
"@expo/config": "6.0.16",
"@expo/config-plugins": "4.0.16",
"@expo/eas-build-job": "0.2.64",
"@expo/eas-json": "0.45.1",
"@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/multipart-body-parser": "1.1.0",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.0.4",
"@expo/prebuild-config": "3.0.16",
"@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.1.2",
"@oclif/plugin-autocomplete": "1.1.1",
"@oclif/plugin-help": "5.1.10",
"@urql/core": "2.3.6",
"@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-progress": "3.10.0",
"cli-table3": "0.6.1",
"dateformat": "4.6.3",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.2.11",
"figures": "3.2.0",
"form-data": "4.0.0",
"fs-extra": "10.0.0",
"fs-extra": "10.0.1",
"getenv": "1.0.0",
"got": "11.8.2",
"gradle-to-js": "2.0.0",
"graphql": "16.2.0",
"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.5.0",
"jks-js": "1.0.2",
"joi": "17.6.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "3.0.4",
"nanoid": "3.2.0",
"node-fetch": "2.6.1",
"node-forge": "1.2.1",
"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",
@@ -70,34 +76,36 @@
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "2.3.1",
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.2",
"@graphql-codegen/typescript-operations": "2.2.2",
"@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/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "1.0.0",
"@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.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.24.0",
"axios": "0.26.1",
"eslint-plugin-graphql": "4.0.0",
"form-data": "4.0.0",
"memfs": "3.4.1",
"mockdate": "3.0.5",
"nock": "13.2.2",
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "1.1.0",
"typescript": "4.5.4"
"ts-deepmerge": "2.0.1",
"typescript": "4.6.3"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.16"
},
"files": [
"/bin",
@@ -121,7 +129,10 @@
],
"topics": {
"account": {
"description": "manage your account"
"description": "manage account"
},
"branch": {
"description": "manage update branches"
},
"build": {
"description": "build app binaries"
@@ -130,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) {
@@ -7,9 +7,9 @@ import fs from 'fs-extra';
import path from 'path';
import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { syncUpdatesConfigurationAsync } from './UpdatesModule';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync({
+279 -128
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';
@@ -100,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> {
@@ -208,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();
}
@@ -3,9 +3,9 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { syncUpdatesConfigurationAsync } from './UpdatesModule';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync({
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.62';
const PLUGIN_PACKAGE_VERSION = '0.0.74';
export interface LocalBuildOptions {
enable: boolean;
+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,
@@ -137,7 +137,11 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
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(
@@ -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({
+1 -1
View File
@@ -14,7 +14,7 @@ import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BranchView extends EasCommand {
static description = 'View a branch.';
static description = 'view a branch';
static args = [
{
@@ -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' }];
@@ -3,21 +3,20 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../build/android/UpdatesModule';
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../build/ios/UpdatesModule';
import { isExpoUpdatesInstalled } from '../../build/utils/updates';
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({
+9 -2
View File
@@ -33,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({
@@ -50,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:
@@ -138,6 +138,13 @@ 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 {
@@ -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({
@@ -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();
+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?`,
});
+154 -25
View File
@@ -1,21 +1,25 @@
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,
ViewBranchUpdatesQuery,
} from '../../graphql/generated';
@@ -23,7 +27,12 @@ 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,
@@ -32,8 +41,14 @@ 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';
@@ -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,13 +589,21 @@ 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}`
),
])
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ type UpdateGroupDescription = FormatUpdateParameter & {
};
export default class BranchView extends EasCommand {
static description = 'View the recent updates for a branch';
static description = 'view the recent updates for a branch';
static flags = {
branch: Flags.string({
+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 = 'Update 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,
});
}
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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();
@@ -0,0 +1,48 @@
import { Session } from '@expo/apple-utils';
import { MinimalAscApiKey } from '../credentials';
export enum AuthenticationMode {
/** App Store API requests will be made using the official API via an API key, used for CI environments where 2FA cannot be performed. */
API_KEY,
/** Uses cookies based authentication and the unofficial App Store web API, this provides more functionality than the official API but cannot be reliably used in CI because it requires 2FA. */
USER,
}
export enum AppleTeamType {
IN_HOUSE = 'IN_HOUSE',
COMPANY_OR_ORGANIZATION = 'COMPANY_OR_ORGANIZATION',
INDIVIDUAL = 'INDIVIDUAL',
}
export type Team = {
id: string;
/** Name of the development team, this is undefined when ASC API keys are used instead of cookies for authentication. */
name?: string;
inHouse?: boolean;
};
export type UserAuthCtx = {
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 type ApiKeyAuthCtx = {
ascApiKey: MinimalAscApiKey;
team: Team;
/**
* Can be used to restore the Apple auth state via apple-utils.
*/
authState?: Partial<Session.AuthState>;
};
export type AuthCtx = UserAuthCtx | ApiKeyAuthCtx;
@@ -2,6 +2,7 @@ import { BundleId, CapabilityTypeOption } from '@expo/apple-utils';
import { JSONObject } from '@expo/json-file';
import Log from '../../../log';
import { isAppStoreConnectTokenOnlyContext } from '../utils/authType';
import {
CapabilityMapping,
EXPO_NO_CAPABILITY_SYNC,
@@ -15,6 +16,8 @@ type UpdateCapabilityRequest = Parameters<BundleId['updateBundleIdCapabilityAsyn
* If a capability identifier is missing, then attempt to create it.
* Link all of the capability identifiers at the same time after parsing the entitlements file.
*
* **Does not support App Store Connect API (CI).**
*
* @param bundleId Bundle identifier object.
* @param entitlements JSON representation of the iOS entitlements plist
*
@@ -28,6 +31,15 @@ export async function syncCapabilityIdentifiersForEntitlementsAsync(
return { created: [], linked: [] };
}
// App Store Connect token authentication is not currently supported,
// the team ID is required to create capability identifiers.
if (isAppStoreConnectTokenOnlyContext(bundleId.context)) {
Log.warn(
`Skipping capability identifier syncing because the current Apple authentication session is not using Cookies (username/password).`
);
return { created: [], linked: [] };
}
const createdIds: string[] = [];
const linkedIds: string[] = [];
const CapabilityIdMapping = CapabilityMapping.filter(capability => capability.capabilityIdModel);
@@ -5,6 +5,9 @@ import Log from '../../../log';
import type { Ora } from '../../../ora';
import { convertHTMLToASCII } from '../utils/convertHTMLToASCII';
/**
* **Does not support App Store Connect API (CI).**
*/
async function getContractStatusAsync(
context: RequestContext
): Promise<ITCAgreements.ITCContractStatus | null> {
@@ -17,6 +20,9 @@ async function getContractStatusAsync(
}
}
/**
* **Does not support App Store Connect API (CI).**
*/
async function getContractMessagesAsync(
context: RequestContext
): Promise<ITCAgreements.ITCContractMessage[] | null> {
@@ -28,6 +34,9 @@ async function getContractMessagesAsync(
}
}
/**
* **Does not support App Store Connect API (CI).**
*/
async function getRequiredContractMessagesAsync(
context: RequestContext
): Promise<{ messages: ITCAgreements.ITCContractMessage[]; isFatal: boolean }> {
@@ -77,6 +86,9 @@ export function formatContractMessage(message: ITCAgreements.ITCContractMessage)
});
}
/**
* **Does not support App Store Connect API (CI).**
*/
export async function assertContractMessagesAsync(
context: RequestContext,
spinner?: Ora
@@ -7,7 +7,8 @@ import {
import { ora } from '../../../ora';
import { DistributionCertificate, DistributionCertificateStoreInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
import { getRequestContext } from './authenticate';
import { AuthCtx } from './authenticateTypes';
export class AppleTooManyCertsError extends Error {}
@@ -4,7 +4,8 @@ import chalk from 'chalk';
import Log from '../../../log';
import { ora } from '../../../ora';
import { AuthCtx, getRequestContext } from './authenticate';
import { getRequestContext, isUserAuthCtx } from './authenticate';
import { AuthCtx, UserAuthCtx } from './authenticateTypes';
import { syncCapabilitiesForEntitlementsAsync } from './bundleIdCapabilities';
import { syncCapabilityIdentifiersForEntitlementsAsync } from './capabilityIdentifiers';
import { assertContractMessagesAsync } from './contractMessages';
@@ -72,7 +73,13 @@ export async function ensureBundleIdExistsWithNameAsync(
// Unable to process request - PLA Update available - You currently don't have access to this membership resource. To resolve this issue, agree to the latest Program License Agreement in your developer account.
err.message.match(/agree/)
) {
await assertContractMessagesAsync(context);
if (isUserAuthCtx(authCtx)) {
await assertContractMessagesAsync(context);
} else {
Log.warn(
`You currently don't have access to this membership resource. To resolve this issue, agree to the latest Program License Agreement in your developer account.`
);
}
}
}
@@ -150,7 +157,7 @@ export async function syncCapabilityIdentifiersAsync(
}
export async function ensureAppExistsAsync(
authCtx: AuthCtx,
userAuthCtx: UserAuthCtx,
{
name,
language,
@@ -165,7 +172,7 @@ export async function ensureAppExistsAsync(
sku?: string;
}
): Promise<App> {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const spinner = ora(`Linking to App Store Connect ${chalk.dim(bundleIdentifier)}`).start();
let app = await App.findAsync(context, { bundleId: bundleIdentifier });
@@ -176,7 +183,9 @@ export async function ensureAppExistsAsync(
try {
// Assert contract errors when the user needs to create an app.
await assertContractMessagesAsync(context, spinner);
/**
* **Does not support App Store Connect API (CI).**
*/
app = await App.createAsync(context, {
bundleId: bundleIdentifier,
name,
@@ -186,9 +195,8 @@ export async function ensureAppExistsAsync(
});
} catch (error: any) {
if (error.message.match(/An App ID with Identifier '(.*)' is not available/)) {
const providerName = authCtx.authState?.session.provider.name;
throw new Error(
`\nThe bundle identifier "${bundleIdentifier}" is not available to provider "${providerName}". Please change it in your app config and try again.\n`
`\nThe bundle identifier "${bundleIdentifier}" is not available to provider "${userAuthCtx.authState?.session.provider.name}. Please change it in your app config and try again.\n`
);
}
@@ -1,13 +1,15 @@
import { Profile, ProfileType, RequestContext } from '@expo/apple-utils';
import { ora } from '../../../ora';
import { isAppStoreConnectTokenOnlyContext } from '../utils/authType';
import { findP12CertSerialNumber } from '../utils/p12Certificate';
import {
DistributionCertificate,
ProvisioningProfile,
ProvisioningProfileStoreInfo,
} from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
import { getRequestContext } from './authenticate';
import { AuthCtx } from './authenticateTypes';
import { getBundleIdForIdentifierAsync, getProfilesForBundleIdAsync } from './bundleId';
import { getCertificateBySerialNumberAsync, transformCertificate } from './distributionCertificate';
@@ -70,6 +72,12 @@ async function addCertificateToProfileAsync(
// Assign the new certificate
profile.attributes.certificates = [cert];
// Experimentally regenerate the provisioning profile using App Store Connect API.
if (isAppStoreConnectTokenOnlyContext(profile.context)) {
return await profile.regenerateManuallyAsync();
}
// This method does not support App Store Connect API.
return await profile.regenerateAsync();
}
@@ -77,8 +85,7 @@ export async function useExistingProvisioningProfileAsync(
authCtx: AuthCtx,
bundleIdentifier: string,
provisioningProfile: ProvisioningProfile,
distCert: DistributionCertificate,
profileClass: ProfileClass = ProfileClass.General
distCert: DistributionCertificate
): Promise<ProvisioningProfile> {
if (!provisioningProfile.provisioningProfileId) {
throw new Error('Provisioning profile: cannot use existing profile, insufficient id');
@@ -1,8 +1,10 @@
import { Device, Profile, ProfileState, ProfileType, RequestContext } from '@expo/apple-utils';
import { ora } from '../../../ora';
import { isAppStoreConnectTokenOnlyContext } from '../utils/authType';
import { ProvisioningProfile } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
import { getRequestContext } from './authenticate';
import { AuthCtx } from './authenticateTypes';
import { getBundleIdForIdentifierAsync, getProfilesForBundleIdAsync } from './bundleId';
import { getDistributionCertificateAsync } from './distributionCertificate';
@@ -89,7 +91,15 @@ async function findProfileByBundleIdAsync(
}
const profile = expoProfiles.sort(sortByExpiration)[expoProfiles.length - 1];
profile.attributes.certificates = [distributionCertificate];
return { profile: await profile.regenerateAsync(), didUpdate: true };
return {
profile: isAppStoreConnectTokenOnlyContext(profile.context)
? // Experimentally regenerate the provisioning profile using App Store Connect API.
await profile.regenerateManuallyAsync()
: // This method does not support App Store Connect API.
await profile.regenerateAsync(),
didUpdate: true,
};
}
// there is no valid provisioning profile available
@@ -173,7 +183,14 @@ async function manageAdHocProfilesAsync(
}
// We need to add new devices to the list and create a new provisioning profile.
existingProfile.attributes.devices = devices;
await existingProfile.regenerateAsync();
if (isAppStoreConnectTokenOnlyContext(existingProfile.context)) {
// Experimentally regenerate the provisioning profile using App Store Connect API.
await existingProfile.regenerateManuallyAsync();
} else {
// This method does not support App Store Connect API.
await existingProfile.regenerateAsync();
}
const updatedProfile = (await findProfileByBundleIdAsync(context, bundleId, certSerialNumber))
.profile;
@@ -5,7 +5,8 @@ import dateformat from 'dateformat';
import Log from '../../../log';
import { ora } from '../../../ora';
import { PushKey, PushKeyStoreInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
import { getRequestContext } from './authenticate';
import { UserAuthCtx } from './authenticateTypes';
const { MaxKeysCreatedError } = Keys;
@@ -15,10 +16,14 @@ Please revoke the old ones or reuse existing from your other apps.
Please remember that Apple Keys are not application specific!
`;
export async function listPushKeysAsync(authCtx: AuthCtx): Promise<PushKeyStoreInfo[]> {
/**
* List all existing push keys on Apple servers.
* **Does not support App Store Connect API (CI).**
*/
export async function listPushKeysAsync(userAuthCtx: UserAuthCtx): Promise<PushKeyStoreInfo[]> {
const spinner = ora(`Fetching Apple push keys`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const keys = await Keys.getKeysAsync(context);
spinner.succeed(`Fetched Apple push keys`);
return keys;
@@ -28,21 +33,25 @@ export async function listPushKeysAsync(authCtx: AuthCtx): Promise<PushKeyStoreI
}
}
/**
* Create a new push key on Apple servers.
* **Does not support App Store Connect API (CI).**
*/
export async function createPushKeyAsync(
authCtx: AuthCtx,
userAuthCtx: UserAuthCtx,
name: string = `Expo Push Notifications Key ${dateformat('yyyymmddHHMMss')}`
): Promise<PushKey> {
const spinner = ora(`Creating Apple push key`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
const key = await Keys.createKeyAsync(context, { name, isApns: true });
const apnsKeyP8 = await Keys.downloadKeyAsync(context, { id: key.id });
spinner.succeed(`Created Apple push key`);
return {
apnsKeyId: key.id,
apnsKeyP8,
teamId: authCtx.team.id,
teamName: authCtx.team.name,
teamId: userAuthCtx.team.id,
teamName: userAuthCtx.team.name,
};
} catch (err: any) {
spinner.fail('Failed to create Apple push key');
@@ -57,12 +66,16 @@ export async function createPushKeyAsync(
}
}
export async function revokePushKeyAsync(authCtx: AuthCtx, ids: string[]): Promise<void> {
/**
* Revoke an existing push key on Apple servers.
* **Does not support App Store Connect API (CI).**
*/
export async function revokePushKeyAsync(userAuthCtx: UserAuthCtx, ids: string[]): Promise<void> {
const name = `Apple push key${ids?.length === 1 ? '' : 's'}`;
const spinner = ora(`Revoking ${name}`).start();
try {
const context = getRequestContext(authCtx);
const context = getRequestContext(userAuthCtx);
await Promise.all(ids.map(id => Keys.revokeKeyAsync(context, { id })));
spinner.succeed(`Revoked ${name}`);
@@ -5,6 +5,8 @@ import wrapAnsi from 'wrap-ansi';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { MinimalAscApiKey } from '../credentials';
import { AppleTeamType, Team } from './authenticateTypes';
import * as Keychain from './keychain';
/**
@@ -13,7 +15,7 @@ import * as Keychain from './keychain';
*
* @param options
*/
export async function resolveCredentialsAsync(
export async function resolveUserCredentialsAsync(
options: Partial<Auth.UserCredentials>
): Promise<Partial<Auth.UserCredentials>> {
const credentials = getAppleIdFromEnvironmentOrOptions(options);
@@ -25,6 +27,162 @@ export async function resolveCredentialsAsync(
return credentials;
}
export function hasAscEnvVars(): boolean {
return (
!!process.env.EXPO_ASC_API_KEY_PATH ||
!!process.env.EXPO_ASC_KEY_ID ||
!!process.env.EXPO_ASC_ISSUER_ID
);
}
export async function resolveAscApiKeyAsync(
ascApiKey?: MinimalAscApiKey
): Promise<MinimalAscApiKey> {
const passedKeyP8 = await getAscKeyP8FromEnvironmentOrOptionsAsync(ascApiKey);
const passedKeyId = await getAscKeyIdFromEnvironmentOrOptionsAsync(ascApiKey);
const passedIssuerId = await getAscIssuerIdFromEnvironmentOrOptionsAsync(ascApiKey);
return {
keyP8: passedKeyP8,
keyId: passedKeyId,
issuerId: passedIssuerId,
};
}
async function getAscKeyP8FromEnvironmentOrOptionsAsync(
ascApiKey?: MinimalAscApiKey
): Promise<string> {
if (ascApiKey?.keyP8) {
return ascApiKey?.keyP8;
} else if (process.env.EXPO_ASC_API_KEY_PATH) {
return await fs.readFile(process.env.EXPO_ASC_API_KEY_PATH, 'utf-8');
}
const { ascApiKeyPath } = await promptAsync({
type: 'text',
name: 'ascApiKeyPath',
message: `Path to ASC Api Key Path (.p8):`,
validate: (val: string) => val !== '',
});
return await fs.readFile(ascApiKeyPath, 'utf-8');
}
async function getAscKeyIdFromEnvironmentOrOptionsAsync(
ascApiKey?: MinimalAscApiKey
): Promise<string> {
if (ascApiKey?.keyId) {
return ascApiKey?.keyId;
} else if (process.env.EXPO_ASC_KEY_ID) {
return process.env.EXPO_ASC_KEY_ID;
}
const { ascApiKeyId } = await promptAsync({
type: 'text',
name: 'ascApiKeyId',
message: `ASC Api Key ID:`,
validate: (val: string) => val !== '',
});
return ascApiKeyId;
}
async function getAscIssuerIdFromEnvironmentOrOptionsAsync(
ascApiKey?: MinimalAscApiKey
): Promise<string> {
if (ascApiKey?.issuerId) {
return ascApiKey?.issuerId;
} else if (process.env.EXPO_ASC_ISSUER_ID) {
return process.env.EXPO_ASC_ISSUER_ID;
}
const { ascIssuerId } = await promptAsync({
type: 'text',
name: 'ascIssuerId',
message: `ASC Issuer ID:`,
validate: (val: string) => val !== '',
});
return ascIssuerId;
}
function isAppleTeamType(maybeTeamType: any): maybeTeamType is AppleTeamType {
return maybeTeamType in AppleTeamType;
}
function assertAppleTeamType(maybeTeamType: any): AppleTeamType {
if (!isAppleTeamType(maybeTeamType)) {
throw new Error(
`Invalid Apple Team Type: ${maybeTeamType}. Must be one of ${Object.keys(AppleTeamType).join(
', '
)}`
);
}
return maybeTeamType;
}
function resolveAppleTeamTypeFromEnvironment(): AppleTeamType | undefined {
if (!process.env.EXPO_APPLE_TEAM_TYPE) {
return undefined;
}
return assertAppleTeamType(process.env.EXPO_APPLE_TEAM_TYPE);
}
async function getAppleTeamIdFromEnvironmentOrOptionsAsync(options: {
teamId?: string;
}): Promise<string> {
if (options.teamId) {
return options.teamId;
} else if (process.env.EXPO_APPLE_TEAM_ID) {
return process.env.EXPO_APPLE_TEAM_ID;
}
const { appleTeamId } = await promptAsync({
type: 'text',
name: 'appleTeamId',
message: `Apple Team ID:`,
validate: (val: string) => val !== '',
});
return appleTeamId;
}
async function getAppleTeamTypeFromEnvironmentOrOptionsAsync(options: {
teamType?: AppleTeamType;
}): Promise<string> {
if (options.teamType) {
return options.teamType;
}
const appleTeamTypeFromEnvironment = resolveAppleTeamTypeFromEnvironment();
if (appleTeamTypeFromEnvironment) {
return appleTeamTypeFromEnvironment;
}
const { appleTeamType } = await promptAsync({
type: 'select',
message: 'Select your Apple Team Type:',
name: 'appleTeamType',
choices: [
{ title: 'Enterprise', value: AppleTeamType.IN_HOUSE },
{ title: 'Company/Organization', value: AppleTeamType.COMPANY_OR_ORGANIZATION },
{ title: 'Individual', value: AppleTeamType.INDIVIDUAL },
],
});
return appleTeamType;
}
export async function resolveAppleTeamAsync(
options: {
teamId?: string;
teamName?: string;
teamType?: AppleTeamType;
} = {}
): Promise<Team> {
const passedTeamType = await getAppleTeamTypeFromEnvironmentOrOptionsAsync(options);
return {
id: await getAppleTeamIdFromEnvironmentOrOptionsAsync(options),
name: options.teamName,
inHouse: passedTeamType === AppleTeamType.IN_HOUSE,
};
}
function getAppleIdFromEnvironmentOrOptions({
username,
password,
@@ -0,0 +1,6 @@
import { RequestContext } from '@expo/apple-utils';
/** Is the request context App Store Connect only with no access to cookies authentication. */
export function isAppStoreConnectTokenOnlyContext(authContext: RequestContext): boolean {
return !authContext.teamId && !!authContext.token;
}
+1 -1
View File
@@ -97,7 +97,7 @@ export class AccountResolver {
async function ensureAppleTeamExistsAsync(
accountId: string,
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName: string }
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName?: string }
): Promise<AppleTeamFragment> {
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
accountId,
+32
View File
@@ -0,0 +1,32 @@
import { Agent } from 'https';
import createHttpsProxyAgent from 'https-proxy-agent';
import fetch, { RequestInfo, RequestInit, Response } from 'node-fetch';
export * from 'node-fetch';
export class RequestError extends Error {
constructor(message: string, public readonly response: Response) {
super(message);
}
}
function createHttpsAgent(): Agent | null {
const httpsProxyUrl = process.env.https_proxy;
if (!httpsProxyUrl) {
return null;
}
return createHttpsProxyAgent(httpsProxyUrl);
}
export const httpsProxyAgent: Agent | null = createHttpsAgent();
export default async function (url: RequestInfo, init?: RequestInit): Promise<Response> {
const response = await fetch(url, {
...init,
...(httpsProxyAgent ? { agent: httpsProxyAgent } : {}),
});
if (response.status >= 400) {
throw new RequestError(`Request failed: ${response.status} (${response.statusText})`, response);
}
return response;
}
+18 -27
View File
@@ -12,23 +12,13 @@ import {
} from '@urql/core';
import { retryExchange } from '@urql/exchange-retry';
import { DocumentNode } from 'graphql';
// node-fetch is used here because @urql/core uses the fetch API under the hood
// don't use node-fetch elsewhere
// eslint-disable-next-line
import fetch from 'node-fetch';
import { getExpoApiBaseUrl } from '../api';
import { httpsProxyAgent } from '../fetch';
import Log from '../log';
import { getAccessToken, getSessionSecret } from '../user/sessionStorage';
type AccessTokenHeaders = {
authorization: string;
};
type SessionHeaders = {
'expo-session': string;
};
export const graphqlClient = createUrqlClient({
url: getExpoApiBaseUrl() + '/graphql',
exchanges: [
@@ -36,31 +26,32 @@ export const graphqlClient = createUrqlClient({
cacheExchange,
retryExchange({
maxDelayMs: 4000,
retryIf: err =>
!!(err && (err.networkError || err.graphQLErrors.some(e => e?.extensions?.isTransient))),
retryIf: (err, operation) => {
return !!(
err &&
!operation.context.noRetry &&
(err.networkError || err.graphQLErrors.some(e => e?.extensions?.isTransient))
);
},
}),
fetchExchange,
],
// @ts-expect-error Type 'typeof fetch' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
fetch,
fetchOptions: (): { headers?: AccessTokenHeaders | SessionHeaders } => {
fetchOptions: (): RequestInit => {
const headers: Record<string, string> = {};
const token = getAccessToken();
if (token) {
return {
headers: {
authorization: `Bearer ${token}`,
},
};
headers.authorization = `Bearer ${token}`;
}
const sessionSecret = getSessionSecret();
if (sessionSecret) {
return {
headers: {
'expo-session': sessionSecret,
},
};
if (!token && sessionSecret) {
headers['expo-session'] = sessionSecret;
}
return {};
return {
...(httpsProxyAgent ? { agent: httpsProxyAgent } : {}),
headers,
};
},
}) as StricterClient;
@@ -84,7 +75,7 @@ export async function withErrorHandlingAsync<T>(promise: Promise<OperationResult
throw error;
}
// Check for malfolmed response. This only checks the root query existence,
// Check for malformed response. This only checks the root query existence,
// It doesn't affect returning responses with empty resultset.
if (!data) {
throw new Error('Returned query result data is null!');
+245 -136
View File
File diff suppressed because it is too large. Load diff
@@ -51,7 +51,8 @@ export const BuildMutation = {
}
${print(BuildFragmentNode)}
`,
input
input,
{ noRetry: true }
)
.toPromise()
);
@@ -86,7 +87,8 @@ export const BuildMutation = {
}
${print(BuildFragmentNode)}
`,
input
input,
{ noRetry: true }
)
.toPromise()
);
@@ -1,7 +1,12 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { PublishUpdateGroupInput, UpdatePublishMutation } from '../generated';
import {
CodeSigningInfoInput,
PublishUpdateGroupInput,
SetCodeSigningInfoMutation,
UpdatePublishMutation,
} from '../generated';
export const PublishMutation = {
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
@@ -43,6 +48,7 @@ export const PublishMutation = {
group
runtimeVersion
platform
manifestPermalink
}
}
}
@@ -53,4 +59,37 @@ export const PublishMutation = {
);
return data.updateBranch.publishUpdateGroups;
},
async setCodeSigningInfoAsync(
updateId: string,
codeSigningInfo: CodeSigningInfoInput
): Promise<SetCodeSigningInfoMutation['update']['setCodeSigningInfo']> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<SetCodeSigningInfoMutation>(
gql`
mutation SetCodeSigningInfoMutation(
$updateId: ID!
$codeSigningInfo: CodeSigningInfoInput!
) {
update {
setCodeSigningInfo(updateId: $updateId, codeSigningInfo: $codeSigningInfo) {
id
group
awaitingCodeSigningInfo
codeSigningInfo {
keyid
alg
sig
}
}
}
}
`,
{ updateId, codeSigningInfo }
)
.toPromise()
);
return data.update.setCodeSigningInfo;
},
};
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more