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
113 changed files with 4553 additions and 1783 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
+92
View File
@@ -10,6 +10,98 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 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))
+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.47.0"
"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.8.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.5.0",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"lerna": "4.0.0",
"oclif": "2.4.3",
"prettier": "2.5.1",
"ts-jest": "27.1.3",
"typescript": "4.5.5"
"oclif": "3.0.1",
"prettier": "2.6.1",
"ts-jest": "27.1.4",
"typescript": "4.6.3"
},
"volta": {
"node": "16.10.0",
+149 -51
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.47.0/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.47.0/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,7 +132,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -141,7 +146,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -190,7 +195,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -210,7 +215,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -227,7 +232,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -246,7 +251,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -266,7 +271,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -274,9 +279,8 @@ 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,14 +294,13 @@ 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
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -311,7 +314,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -328,7 +331,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -363,7 +366,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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.47.0/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,7 +444,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -433,7 +464,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -454,7 +485,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -471,7 +502,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -491,7 +522,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -509,7 +540,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -523,7 +554,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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.47.0/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.47.0/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.47.0/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,7 +617,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -600,7 +631,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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.11/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.47.0/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,7 +727,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -671,7 +747,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -688,7 +764,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -702,11 +778,11 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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 app binary to app store
submit app binary to App Store and/or Play Store
```
USAGE
@@ -726,13 +802,13 @@ FLAGS
--[no-]wait Wait for submission to complete
DESCRIPTION
submit app binary to app store
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.47.0/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`
@@ -741,7 +817,7 @@ 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]
@@ -751,6 +827,10 @@ 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
@@ -758,7 +838,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -766,13 +846,16 @@ 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
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -792,7 +875,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -811,7 +894,7 @@ DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -831,7 +914,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -851,7 +934,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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]`
@@ -868,7 +951,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -885,7 +968,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -906,7 +989,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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`
@@ -923,5 +1006,20 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.47.0/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',
+31 -23
View File
@@ -1,46 +1,52 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.47.0",
"version": "0.51.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
"eas": "./bin/run.js"
},
"type": "module",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.27",
"@expo/config": "6.0.16",
"@expo/config-plugins": "4.0.16",
"@expo/eas-build-job": "0.2.65",
"@expo/eas-json": "0.47.0",
"@expo/apple-utils": "0.0.0-alpha.31",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.19",
"@expo/config-plugins": "4.1.0",
"@expo/config-types": "44.0.0",
"@expo/eas-build-job": "0.2.68",
"@expo/eas-json": "0.51.0",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.6",
"@expo/multipart-body-parser": "1.1.0",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.1.0",
"@expo/prebuild-config": "3.0.16",
"@expo/prebuild-config": "3.1.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.6.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.3.1",
"@oclif/core": "1.6.4",
"@oclif/plugin-autocomplete": "1.2.0",
"@oclif/plugin-help": "5.1.11",
"@urql/core": "2.4.1",
"@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.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.2",
@@ -49,9 +55,9 @@
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "3.0.5",
"nanoid": "3.2.0",
"nanoid": "3.3.2",
"node-fetch": "2.6.7",
"node-forge": "1.2.1",
"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.5.0",
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.3",
"@graphql-codegen/typescript-operations": "2.2.4",
"@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.25.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.4",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"typescript": "4.5.5"
"typescript": "4.6.3"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.16"
},
"files": [
"/bin",
-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) {
@@ -9,7 +9,7 @@ import path from 'path';
import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
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();
}
@@ -5,7 +5,7 @@ import type { XCBuildConfiguration } from 'xcode';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
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;
+8 -20
View File
@@ -6,33 +6,21 @@ import resolveFrom from 'resolve-from';
import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
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';
// 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>
): Promise<Metadata> {
@@ -48,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,9 +0,0 @@
import { ExpoConfig } from '@expo/config';
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 {
@@ -3,16 +3,16 @@ 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 EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
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 {
+8 -1
View File
@@ -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 {
+1 -1
View File
@@ -41,7 +41,7 @@ interface CommandFlags {
}
export default class Submit extends EasCommand {
static description = 'submit app binary to app store';
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;
}
}
+131 -23
View File
@@ -6,16 +6,20 @@ 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';
@@ -39,6 +43,12 @@ import {
import { resolveWorkflowAsync } from '../../project/workflow';
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';
@@ -180,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> {
@@ -194,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;
@@ -209,7 +230,24 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: true,
});
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
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'
@@ -217,7 +255,7 @@ export default class UpdatePublish extends EasCommand {
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoUpdatesAsync(projectDir, { nonInteractive: false });
await installExpoUpdatesAsync(projectDir);
} else {
Errors.error(`Install ${chalk.bold('expo-updates')} manually and come back later.`, {
exit: 1,
@@ -236,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);
@@ -285,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.');
@@ -349,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);
@@ -367,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);
@@ -413,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');
@@ -489,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,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!');
+238 -137
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;
},
};
@@ -23,6 +23,7 @@ export const BuildFragmentNode = gql`
__typename
id
name
slug
... on App {
ownerAccount {
id
@@ -40,6 +41,10 @@ export const BuildFragmentNode = gql`
appBuildVersion
runtimeVersion
gitCommitHash
initialQueuePosition
queuePosition
estimatedWaitTimeLeftSeconds
priority
createdAt
updatedAt
}
+12 -1
View File
@@ -1 +1,12 @@
export { run } from '@oclif/core';
import { run } from '@oclif/core';
/**
* Invokes the `testcli-modern` CLI with args programmatically.
*
* @param {...string} args - args to pass to CLI.
*
* @returns {Promise<void>}
*/
export default async function eascliAsync(...args: any[]): Promise<unknown> {
return run(args, import.meta.url);
}
+1 -1
View File
@@ -13,7 +13,7 @@ export const appPlatformEmojis = {
[AppPlatform.Android]: '🤖',
};
// for `eas build` and `eas submit`
// for `eas build`, `eas submit`, and `eas update`
export enum RequestedPlatform {
Android = 'android',
Ios = 'ios',
@@ -1,12 +1,27 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import Joi from 'joi';
import { Target } from '../../credentials/ios/types';
import { resolveWorkflowAsync } from '../workflow';
import { getBundleIdentifierAsync } from './bundleIdentifier';
import { XcodeBuildContext } from './scheme';
interface UserDefinedTarget {
targetName: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
}
const AppExtensionsConfigSchema = Joi.array().items(
Joi.object({
targetName: Joi.string().required(),
bundleIdentifier: Joi.string().required(),
parentBundleIdentifier: Joi.string(),
})
);
export async function resolveTargetsAsync(
{ exp, projectDir }: { exp: ExpoConfig; projectDir: string },
{ buildConfiguration, buildScheme }: XcodeBuildContext
@@ -35,9 +50,54 @@ export async function resolveTargetsAsync(
result.push(...dependencies);
}
result.push(
...(await resolveManagedAppExtensionsAsync({
exp,
projectDir,
buildConfiguration,
applicationTargetBundleIdentifier: bundleIdentifier,
}))
);
return result;
}
async function resolveManagedAppExtensionsAsync({
exp,
projectDir,
buildConfiguration,
applicationTargetBundleIdentifier,
}: {
exp: ExpoConfig;
projectDir: string;
buildConfiguration?: string;
applicationTargetBundleIdentifier: string;
}): Promise<Target[]> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const managedAppExtensions: UserDefinedTarget[] =
exp.extra?.eas?.build?.experimental?.ios?.appExtensions;
if (workflow === Workflow.GENERIC || !managedAppExtensions) {
return [];
}
const { error } = AppExtensionsConfigSchema.validate(managedAppExtensions, {
allowUnknown: false,
abortEarly: false,
});
if (error) {
throw new Error(
`Failed to validate "extra.eas.build.experimental.ios.appExtensions" in you app config\n${error.message}`
);
}
return managedAppExtensions.map(extension => ({
targetName: extension.targetName,
buildConfiguration,
bundleIdentifier: extension.bundleIdentifier,
parentBundleIdentifier: extension.parentBundleIdentifier ?? applicationTargetBundleIdentifier,
}));
}
async function resolveDependenciesAsync({
exp,
projectDir,
+4 -4
View File
@@ -54,7 +54,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(
}
}
function importMetroConfigFromProject(projectDir: string): typeof MetroConfig {
async function importMetroConfigFromProjectAsync(projectDir: string): Promise<typeof MetroConfig> {
const resolvedPath = resolveFrom.silent(projectDir, 'metro-config');
if (!resolvedPath) {
throw new MetroConfigPackageMissingError(
@@ -64,12 +64,12 @@ function importMetroConfigFromProject(projectDir: string): typeof MetroConfig {
'and run `yarn` or `npm install`.'
);
}
return require(resolvedPath);
return await import(resolvedPath);
}
async function configExistsAsync(projectRoot: string): Promise<boolean> {
try {
const MetroConfig = importMetroConfigFromProject(projectRoot);
const MetroConfig = await importMetroConfigFromProjectAsync(projectRoot);
const result = await MetroConfig.resolveConfig(undefined, projectRoot);
return !result.isEmpty;
} catch (err) {
@@ -82,7 +82,7 @@ async function configExistsAsync(projectRoot: string): Promise<boolean> {
}
async function loadConfigAsync(projectDir: string): Promise<MetroConfig.ConfigT> {
const MetroConfig = importMetroConfigFromProject(projectDir);
const MetroConfig = await importMetroConfigFromProjectAsync(projectDir);
return await MetroConfig.loadConfig({ cwd: projectDir }, {});
}
+13 -21
View File
@@ -1,10 +1,4 @@
import {
AppJSONConfig,
ExpoConfig,
getConfigFilePaths,
getPackageJson,
modifyConfigAsync,
} from '@expo/config';
import { ExpoConfig, getConfigFilePaths, getPackageJson, modifyConfigAsync } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import path from 'path';
@@ -17,6 +11,7 @@ import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { expoCommandAsync } from '../utils/expoCli';
import { getVcsClient } from '../vcs';
import {
ensureProjectExistsAsync,
findProjectIdByAccountNameAndSlugNullableAsync,
@@ -73,6 +68,15 @@ export async function findProjectRootAsync({
return process.cwd();
}
} else {
let vcsRoot;
try {
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
} catch {}
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
throw new Error(
`package.json is outside of the current git repository (project root: ${projectRootDir}, git root: ${vcsRoot}.`
);
}
return projectRootDir;
}
}
@@ -118,16 +122,7 @@ export async function setProjectIdAsync(
}
Log.withTick(`Linked app.json to project with ID ${chalk.bold(projectId)}`);
/**
* result.config will always be an AppJSONConfig if result.type === 'success'
* PR to fix this typing: https://github.com/expo/expo-cli/pull/3482/files
*
* Code is written to safely handle the case where config type is not
* AppJSONConfig (namely there will be no expo key and the result will be undefined).
* TODO-JJ delete AppJSONConfig casting once typing is updated in the published @expo/config and
* remove undefined alternative from return type
*/
return (result.config as AppJSONConfig)?.expo;
return result.config?.expo;
}
export async function getProjectIdAsync(
@@ -243,10 +238,7 @@ export function isExpoUpdatesInstalledOrAvailable(
return isExpoUpdatesInstalled(projectDir);
}
export async function installExpoUpdatesAsync(
projectDir: string,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
export async function installExpoUpdatesAsync(projectDir: string): Promise<void> {
Log.newLine();
Log.log(`Running ${chalk.bold('expo install expo-updates')}`);
Log.newLine();
+3 -2
View File
@@ -10,6 +10,7 @@ import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated'
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log from '../log';
import { uploadWithPresignedPostAsync } from '../uploads';
import { expoCommandAsync } from '../utils/expoCli';
import uniqBy from '../utils/expodash/uniqBy';
@@ -159,7 +160,7 @@ export async function buildBundlesAsync({
await expoCommandAsync(
projectDir,
['export', '--output-dir', inputDir, '--experimental-bundle'],
{ silent: true }
{ silent: !Log.isDebug }
);
}
@@ -277,7 +278,7 @@ export async function uploadAssetsAsync(assetsForUpdateInfoGroup: CollectedAsset
await Promise.all(
missingAssets.map((missingAsset, i) => {
const presignedPost: PresignedPost = JSON.parse(specifications[i]);
return uploadWithPresignedPostAsync(fs.createReadStream(missingAsset.path), presignedPost);
return uploadWithPresignedPostAsync(missingAsset.path, presignedPost);
})
);
@@ -169,7 +169,8 @@ async function askForServiceAccountPathAsync(): Promise<string> {
'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'
'https://expo.fyi/creating-google-service-account',
{ learnMoreMessage: 'learn more' }
)}`
);
const { filePath } = await promptAsync({
@@ -24,7 +24,6 @@ interface CreateAppOptions {
}
type AppStoreResult = {
appleIdUsername: string;
ascAppIdentifier: string;
};
@@ -67,29 +66,29 @@ async function createAppStoreConnectAppAsync(
sku,
} = options;
const authCtx = await ctx.credentialsCtx.appStore.ensureAuthenticatedAsync({
const userAuthCtx = await ctx.credentialsCtx.appStore.ensureUserAuthenticatedAsync({
appleId,
teamId: appleTeamId,
});
const requestCtx = getRequestContext(authCtx);
const requestCtx = getRequestContext(userAuthCtx);
Log.addNewLineIfNone();
if (await isProvisioningAvailableAsync(requestCtx)) {
await ensureBundleIdExistsWithNameAsync(authCtx, {
await ensureBundleIdExistsWithNameAsync(userAuthCtx, {
name: appName,
bundleIdentifier: bundleId,
});
} else {
Log.warn(
`Provisioning is not available for user "${authCtx.appleId}", skipping bundle identifier check.`
`Provisioning is not available for Apple User: ${userAuthCtx.appleId}, skipping bundle identifier check.`
);
}
let app: App | null = null;
try {
app = await ensureAppExistsAsync(authCtx, {
app = await ensureAppExistsAsync(userAuthCtx, {
name: appName,
language,
companyName,
@@ -120,7 +119,6 @@ async function createAppStoreConnectAppAsync(
}
return {
appleIdUsername: authCtx.appleId,
ascAppIdentifier: app.id,
};
}
@@ -1,6 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import getenv from 'getenv';
import { isUserAuthCtx } from '../../credentials/ios/appstore/authenticate';
import { promptAsync } from '../../prompts';
import UserSettings from '../../user/UserSettings';
import { SubmissionContext } from '../context';
@@ -51,7 +52,7 @@ export async function getAppleIdUsernameAsync(
return envAppleId;
}
if (ctx.credentialsCtx.appStore.authCtx?.appleId) {
if (isUserAuthCtx(ctx.credentialsCtx.appStore.authCtx)) {
return ctx.credentialsCtx.appStore.authCtx.appleId;
}
+7 -2
View File
@@ -17,13 +17,14 @@ enum SubmissionErrorCode {
IOS_INCORRECT_CREDENTIALS = 'SUBMISSION_SERVICE_IOS_INVALID_CREDENTIALS',
IOS_IPAD_INVALID_ORIENTATION = 'SUBMISSION_SERVICE_IOS_IPAD_INVALID_ORIENTATION',
IOS_APPLE_MAINTENANCE = 'SUBMISSION_SERVICE_IOS_APPLE_MAINTENANCE',
IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE',
}
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR]:
"Failed to download the archive file (Response code: 404 Not Found). Please make sure the URL you've provided is correct.",
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR]:
'Failed to download the archive file (Response code: 403 Forbidden). This is most probably caused by trying to upload an expired build artifact. All Expo build artifacts expire after 30 days.',
'Failed to download the archive file (Response code: 403 Forbidden). This is most probably caused by trying to upload an expired build artifact. All EAS build artifacts expire after 30 days.',
[SubmissionErrorCode.ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR]:
'The compressed archive is corrupt, in an unsupported format, or contains an invalid application format. Supported files include .apk, .aab, and .ipa files and one of these files compressed into a .tar.gz archive.',
[SubmissionErrorCode.ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR]:
@@ -37,7 +38,7 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
[SubmissionErrorCode.ANDROID_OLD_VERSION_CODE_ERROR]:
"You've already submitted this version of the app.\n" +
'Versions are identified by Android version code (expo.android.versionCode in app.json).\n' +
"If you're submitting a managed Expo project, increment the version code in app.json and build the project with expo build:android.\n" +
"If you're submitting a managed Expo project, increment the version code in app.json and build the project with eas build.\n" +
`${learnMore('https://expo.fyi/bumping-android-version-code')}.`,
[SubmissionErrorCode.ANDROID_MISSING_PRIVACY_POLICY]:
'The app has permissions that require a privacy policy set for the app.\n' +
@@ -65,6 +66,10 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
`${learnMore('https://expo.fyi/ipad-requires-fullscreen')}`,
[SubmissionErrorCode.IOS_APPLE_MAINTENANCE]:
'It looks like Apple servers are undergoing an unscheduled maintenance. Please try again later.',
[SubmissionErrorCode.IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE]:
'Invalid Provisioning Profile Signature (ITMS-90165)\n' +
"Some of Apple's certificates have expired.\n" +
'Please delete your Provisioning Profile from your account. Then rebuild the app interactively to generate a new one, and try submitting it to the App Store again.',
};
export function printSubmissionError(error: SubmissionError): boolean {
+3 -3
View File
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import got from 'got';
import fetch from '../../fetch';
import { SubmissionFragment, SubmissionStatus } from '../../graphql/generated';
import Log from '../../log';
import { printSubmissionError } from './errors';
@@ -25,8 +25,8 @@ async function downloadAndPrintSubmissionLogsAsync(submission: SubmissionFragmen
if (!submission.logsUrl) {
return;
}
const { body: data } = await got.get(submission.logsUrl);
const logs = parseLogs(data);
const response = await fetch(submission.logsUrl);
const logs = parseLogs(await response.text());
Log.addNewLineIfNone();
const prefix = chalk.blueBright('[logs] ');
for (const { level, msg } of logs) {
@@ -1,21 +1,23 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import { RequestedPlatform } from '../../platform';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { ensureValidVersions } from '../utils/updates';
import { ensureValidVersions } from '../utils';
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp);
ensureValidVersions(exp, RequestedPlatform.Android);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
const updatedAndroidManifest = AndroidConfig.Updates.setUpdatesConfig(
projectDir,
exp,
androidManifest,
accountName
@@ -1,20 +1,26 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { RequestedPlatform } from '../../platform';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { getVcsClient } from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { ensureValidVersions } from '../utils';
export async function syncUpdatesConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
ensureValidVersions(exp);
ensureValidVersions(exp, RequestedPlatform.Ios);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const expoPlist = await readExpoPlistAsync(projectDir);
const updatedExpoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
const updatedExpoPlist = IOSConfig.Updates.setUpdatesConfig(
projectDir,
exp,
expoPlist,
accountName
);
await writeExpoPlistAsync(projectDir, updatedExpoPlist);
}
@@ -43,10 +49,8 @@ export async function readReleaseChannelSafelyAsync(projectDir: string): Promise
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
try {
// TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
const expoPlist: any = await readExpoPlistAsync(projectDir);
//TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {};
const expoPlist = await readExpoPlistAsync(projectDir);
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'];
return updatesRequestHeaders['expo-channel-name'] ?? null;
} catch (err) {
return null;
+33
View File
@@ -1,6 +1,10 @@
import { ExpoConfig } from '@expo/config';
import { format } from '@expo/timeago.js';
import chalk from 'chalk';
import { Maybe, Robot, Update, User } from '../graphql/generated';
import { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import { getActorDisplayName } from '../user/User';
import groupBy from '../utils/expodash/groupBy';
@@ -41,3 +45,32 @@ export function formatUpdate(update: FormatUpdateParameter): string {
update.actor as any
)})`;
}
export function ensureValidVersions(exp: ExpoConfig, platform: RequestedPlatform): void {
const error = new Error(
`Couldn't find either ${chalk.bold('runtimeVersion')} or ${chalk.bold(
'sdkVersion'
)} to configure ${chalk.bold(
'expo-updates'
)}. Please specify at least one of these properties under the ${chalk.bold(
'expo'
)} key in ${chalk.bold('app.json')}. ${learnMore(
'https://docs.expo.dev/eas-update/runtime-versions/'
)}`
);
if (
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
!(exp.android?.runtimeVersion || exp.runtimeVersion) &&
!exp.sdkVersion
) {
throw error;
}
if (
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
!(exp.ios?.runtimeVersion || exp.runtimeVersion) &&
!exp.sdkVersion
) {
throw error;
}
}
+29 -23
View File
@@ -1,17 +1,11 @@
import assert from 'assert';
import FormData from 'form-data';
import fs from 'fs-extra';
import got, { Progress } from 'got';
import { Readable } from 'stream';
import fetch from './fetch';
import { UploadSessionType } from './graphql/generated';
import { PresignedPost, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
type ProgressHandler = (props: {
progress?: Progress;
isComplete?: boolean;
error?: Error;
}) => void;
import { ProgressHandler } from './utils/progress';
export async function uploadAsync(
type: UploadSessionType,
@@ -19,42 +13,54 @@ export async function uploadAsync(
handleProgressEvent: ProgressHandler
): Promise<{ url: string; bucketKey: string }> {
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(type);
const url = await uploadWithPresignedPostAsync(
fs.createReadStream(path),
presignedPost,
handleProgressEvent
);
const url = await uploadWithPresignedPostAsync(path, presignedPost, handleProgressEvent);
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
return { url, bucketKey: presignedPost.fields.key };
}
export async function uploadWithPresignedPostAsync(
stream: Readable | Buffer,
file: string,
presignedPost: PresignedPost,
handleProgressEvent?: ProgressHandler
): Promise<string> {
const fileStat = await fs.stat(file);
const fileSize = fileStat.size;
const form = new FormData();
for (const [fieldKey, fieldValue] of Object.entries(presignedPost.fields)) {
form.append(fieldKey, fieldValue);
}
form.append('file', stream);
form.append('file', fs.createReadStream(file), { knownLength: fileSize });
const formHeaders = form.getHeaders();
let uploadPromise = got.post(presignedPost.url, { body: form, headers: { ...formHeaders } });
const uploadPromise = fetch(presignedPost.url, {
method: 'POST',
body: form,
headers: {
...formHeaders,
},
});
let currentSize = 0;
if (handleProgressEvent) {
uploadPromise = uploadPromise.on('uploadProgress', progress =>
handleProgressEvent({ progress })
);
form.addListener('data', (chunk: Buffer) => {
currentSize += Buffer.byteLength(chunk);
handleProgressEvent({
progress: {
total: fileSize,
percent: currentSize / fileSize,
transferred: currentSize,
},
});
});
try {
const response = await uploadPromise;
handleProgressEvent({ isComplete: true });
return String(response.headers.location);
return String(response.headers.get('location'));
} catch (error: any) {
handleProgressEvent({ isComplete: true, error });
throw error;
}
} else {
const response = await uploadPromise;
return String(response.headers.get('location'));
}
const response = await uploadPromise;
return String(response.headers.location);
}
+3 -5
View File
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
import * as Analytics from '../analytics/rudderstackClient';
import { apiClient } from '../api';
import { api } from '../api';
import { graphqlClient } from '../graphql/client';
import { CurrentUserQuery } from '../graphql/generated';
import { UserQuery } from '../graphql/queries/UserQuery';
@@ -54,10 +54,8 @@ export async function loginAsync({
password: string;
otp?: string;
}): Promise<void> {
const body = await apiClient
.post('auth/loginAsync', { json: { username, password, otp } })
.json();
const { sessionSecret } = (body as any).data;
const body = await api.postAsync('auth/loginAsync', { body: { username, password, otp } });
const { sessionSecret } = body.data;
const result = await graphqlClient
.query(
gql`
@@ -13,13 +13,10 @@ import {
jest.mock('fs');
jest.mock('../../api', () => ({
apiClient: {
post: jest.fn(() => {
return {
json: () => Promise.resolve({ data: { sessionSecret: 'SESSION_SECRET' } }),
};
}),
api: {
postAsync: jest.fn(async () => ({ data: { sessionSecret: 'SESSION_SECRET' } })),
},
ApiV2Error: jest.requireActual('../../api').ApiV2Error,
}));
jest.mock('../../graphql/client', () => ({
graphqlClient: {
@@ -1,12 +1,15 @@
import ApiV2Error from '../../ApiV2Error';
import { asMock } from '../../__tests__/utils';
import { ApiV2Error } from '../../api';
import { promptAsync } from '../../prompts';
import { Actor, loginAsync } from '../User';
import { ensureActorHasUsername, showLoginPromptAsync } from '../actions';
import { UserSecondFactorDeviceMethod, retryUsernamePasswordAuthWithOTPAsync } from '../otp';
jest.mock('../../prompts');
jest.mock('../../api');
jest.mock('../../api', () => ({
ApiV2Error: jest.requireActual('../../api').ApiV2Error,
}));
jest.mock('../otp');
jest.mock('../User', () => ({
loginAsync: jest.fn(),
@@ -57,7 +60,7 @@ describe(showLoginPromptAsync, () => {
});
asMock(loginAsync)
.mockImplementationOnce(async () => {
throw new ApiV2Error({ code: 'testcode', request: {} } as any, {
throw new ApiV2Error({
message: 'An OTP is required',
code: 'ONE_TIME_PASSWORD_REQUIRED',
metadata: {
@@ -1,5 +1,5 @@
import { asMock } from '../../__tests__/utils';
import { apiClient } from '../../api';
import { api } from '../../api';
import Log from '../../log';
import { promptAsync, selectAsync } from '../../prompts';
import { loginAsync } from '../User';
@@ -186,9 +186,7 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
throw new Error("shouldn't happen");
});
asMock(apiClient.post).mockReturnValueOnce({
json: () => Promise.resolve({ data: { sessionSecret: 'SESSION_SECRET' } }),
});
asMock(api.postAsync).mockReturnValueOnce(Promise.resolve({ sessionSecret: 'SESSION_SECRET' }));
await retryUsernamePasswordAuthWithOTPAsync('blah', 'blah', {
secondFactorDevices: [
@@ -209,10 +207,10 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
expect(asMock(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
expect(asMock(apiClient.post).mock.calls[0]).toEqual([
expect(asMock(api.postAsync).mock.calls[0]).toEqual([
'auth/send-sms-otp',
{
json: {
body: {
username: 'blah',
password: 'blah',
secondFactorDeviceID: 'p2',
+1 -1
View File
@@ -2,7 +2,7 @@ import { Errors } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import ApiV2Error from '../ApiV2Error';
import { ApiV2Error } from '../api';
import Log, { learnMore } from '../log';
import { promptAsync } from '../prompts';
import { Actor, getUserAsync, loginAsync } from './User';
+8 -10
View File
@@ -1,7 +1,7 @@
import assert from 'assert';
import chalk from 'chalk';
import { apiClient } from '../api';
import { api } from '../api';
import Log from '../log';
import { promptAsync, selectAsync } from '../prompts';
import { loginAsync } from './User';
@@ -92,15 +92,13 @@ async function promptForBackupOTPAsync(
const device = smsNonPrimarySecondFactorDevices[selectedValue];
await apiClient
.post('auth/send-sms-otp', {
json: {
username,
password,
secondFactorDeviceID: device.id,
},
})
.json();
await api.postAsync('auth/send-sms-otp', {
body: {
username,
password,
secondFactorDeviceID: device.id,
},
});
return await promptForOTPAsync('cancel');
}
@@ -0,0 +1,333 @@
import FormData from 'form-data';
import path from 'path';
import { Headers, Response } from '../../fetch';
import { PartialManifest } from '../../graphql/generated';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getKeyAndCertificateFromPathsAsync,
getManifestBodyAsync,
} from '../code-signing';
function generateMultipartBody(stringifiedManifest: string): FormData {
const form = new FormData();
// This generates a 26 + 24 character boundary similar to those used by Firefox and form-data.
let boundary = '-----ExpoManifestBoundary-';
for (let i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 16).toString(16);
}
form.setBoundary(boundary);
form.append('manifest', stringifiedManifest, {
contentType: 'application/json',
});
form.append('extensions', JSON.stringify({}), {
contentType: 'application/json',
});
return form;
}
describe(getCodeSigningInfoAsync, () => {
it('throws when codeSigningMetadata is not specified for EAS', async () => {
await expect(
getCodeSigningInfoAsync(
{
name: 'wat',
slug: 'test',
updates: {
codeSigningCertificate: 'wat',
},
},
'test'
)
).rejects.toThrow(
'Must specify codeSigningMetadata under the "updates" field of your app config file to use EAS code signing'
);
});
});
describe(getKeyAndCertificateFromPathsAsync, () => {
it('throws an informative error when file at either path does not exist', async () => {
const codeSigningCertificatePath = path.join(
__dirname,
'./fixtures/certificate-path-not-exist.pem'
);
const privateKeyPath = path.join(__dirname, './fixtures/test-private-key.pem');
await expect(
getKeyAndCertificateFromPathsAsync({
codeSigningCertificatePath,
privateKeyPath,
})
).rejects.toThrow(
`Code signing certificate cannot be read from path: ${codeSigningCertificatePath}`
);
const codeSigningCertificatePath2 = path.join(__dirname, './fixtures/test-certificate.pem');
const privateKeyPath2 = path.join(__dirname, './fixtures/private-key-path-not-exist.pem');
await expect(
getKeyAndCertificateFromPathsAsync({
codeSigningCertificatePath: codeSigningCertificatePath2,
privateKeyPath: privateKeyPath2,
})
).rejects.toThrow(`Code signing private key cannot be read from path: ${privateKeyPath2}`);
});
it('loads certificate and private key', async () => {
const result = await getKeyAndCertificateFromPathsAsync({
codeSigningCertificatePath: path.join(__dirname, './fixtures/test-certificate.pem'),
privateKeyPath: path.join(__dirname, './fixtures/test-private-key.pem'),
});
expect(result.certificate).not.toBe(null);
expect(result.privateKey).not.toBe(null);
});
it('validates certificate and private key', async () => {
await expect(
getKeyAndCertificateFromPathsAsync({
codeSigningCertificatePath: path.join(__dirname, './fixtures/invalid-certificate.pem'),
privateKeyPath: path.join(__dirname, './fixtures/invalid-private-key.pem'),
})
).rejects.toThrow('Certificate validity expired');
});
});
describe(getManifestBodyAsync, () => {
it('gets multipart manifest body', async () => {
const stringifiedManifest = JSON.stringify({ hello: 'world' });
const form = generateMultipartBody(stringifiedManifest);
const body = await getManifestBodyAsync({
arrayBuffer: async () => new Uint8Array(form.getBuffer()).buffer,
headers: new Headers({
'content-type': `multipart/mixed; boundary=${form.getBoundary()}`,
}),
} as any as Response);
expect(body).toEqual(stringifiedManifest);
});
});
describe(checkManifestBodyAgainstUpdateInfoGroup, () => {
it('returns void when valid', () => {
const manifestResponseBodyJSON = JSON.stringify({
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
hash: '1',
contentType: 'test/hex',
key: '1',
},
assets: [
{
hash: '2',
contentType: 'test/hex',
key: '2',
},
],
});
const partialManifest: PartialManifest = {
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
fileSHA256: '1',
contentType: 'test/hex',
bundleKey: '1',
storageKey: 'wat',
},
assets: [
{
fileSHA256: '2',
contentType: 'test/hex',
bundleKey: '2',
storageKey: 'wat',
},
],
};
expect(() =>
checkManifestBodyAgainstUpdateInfoGroup(manifestResponseBodyJSON, partialManifest)
).not.toThrow();
});
it('throws when extra.expoClient is tampered with', () => {
const manifestResponseBodyJSON = JSON.stringify({
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
fileSHA256: '1',
contentType: 'test/hex',
bundleKey: '1',
storageKey: 'wat',
},
});
const partialManifest: PartialManifest = {
extra: {
expoClient: {
test: 'wat',
maliciousExtraKey: {
nestedDeeply: {
hello: 'world',
},
},
},
},
launchAsset: {
fileSHA256: '1',
contentType: 'test/hex',
bundleKey: '1',
storageKey: 'wat',
},
assets: [],
};
expect(() =>
checkManifestBodyAgainstUpdateInfoGroup(manifestResponseBodyJSON, partialManifest)
).toThrow(
`Code signing manifest integrity error: The manifest being signed contains an extra.expoClient field that does not match the initially uploaded manifest's extra.expoClient field`
);
});
it('throws when assets differ in length', () => {
const manifestResponseBodyJSON = JSON.stringify({
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
hash: '1',
contentType: 'test/hex',
key: '1',
},
assets: [
{
hash: '2',
contentType: 'test/hex',
key: '2',
},
],
});
const partialManifest: PartialManifest = {
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
fileSHA256: '1',
contentType: 'test/hex',
bundleKey: '1',
storageKey: 'wat',
},
assets: [],
};
expect(() =>
checkManifestBodyAgainstUpdateInfoGroup(manifestResponseBodyJSON, partialManifest)
).toThrow(
'Code signing manifest integrity error: The manifest being signed has an assets array of differing length from the initially uploaded manifest'
);
});
it('throws when an asset is not present', () => {
const manifestResponseBodyJSON = JSON.stringify({
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
hash: '1',
contentType: 'test/hex',
key: '1',
},
assets: [
{
hash: '2',
contentType: 'test/hex',
key: '2',
},
],
});
const partialManifest: PartialManifest = {
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
fileSHA256: '1',
contentType: 'test/hex',
bundleKey: '1',
storageKey: 'wat',
},
assets: [
{
fileSHA256: '3',
contentType: 'test/hex',
bundleKey: '3',
storageKey: 'wat',
},
],
};
expect(() =>
checkManifestBodyAgainstUpdateInfoGroup(manifestResponseBodyJSON, partialManifest)
).toThrow(
'Code signing manifest integrity error: The manifest being signed has is missing an asset specified in the initially uploaded manifest: 3'
);
});
it('throws when an asset has been tampered with', () => {
const manifestResponseBodyJSON = JSON.stringify({
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
hash: '1',
contentType: 'test/hex',
key: '1',
},
assets: [
{
hash: '2',
contentType: 'test/hex',
key: '2',
},
],
});
const partialManifest: PartialManifest = {
extra: {
expoClient: {
test: 'wat',
},
},
launchAsset: {
fileSHA256: '1',
contentType: 'test/hex',
bundleKey: '1',
storageKey: 'wat',
},
assets: [
{
fileSHA256: '2',
contentType: 'test/malicious',
bundleKey: '2',
storageKey: 'wat',
},
],
};
expect(() =>
checkManifestBodyAgainstUpdateInfoGroup(manifestResponseBodyJSON, partialManifest)
).toThrow(
'Code signing manifest integrity error: Manifest asset tamper detected for asset: 2; field: contentType'
);
});
});
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIICzTCCAbWgAwIBAgIJbOc3N87bRTW9MA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV
BAMTBWhlbGxvMB4XDTIyMDIwNDAxNDkzNFoXDTIyMDIwNDAxNDkzNFowEDEOMAwG
A1UEAxMFaGVsbG8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpQHor
Eh7eb1L8HJuKciyL/qoLAvzrHEvn0bafL6rywLu+sBqnI0jBLi76rjYOlO7Ruy8X
/tbZ6rMjoAIjW5zjk6SAZMYTVAcoJk6oKg1hgRVA6ZMQ3uvTBvw9JAvnJy2nsjXQ
NamW/ELFtlMAj9XOnTU7BGohunUcIQTfyii5IZZwaG/VsyJXBVOGUcJoG+cf6aS9
E+7ID/BNH4qYIE7IK+jZu5Etmx38BmzXJko1S0jRWGvaSZKM48S1ENsP3NgbI99V
Tu7eQyjonK5gpeYZaaRHYOT+iPNU3lGYqyLZ680IaBzZCzCjUNCG0e86b/H70yUd
Z7qH11ygtjjRMDATAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E
DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAKk5tUXJM9ofr7UWeElm0
G30mMWG6heQm/Id0oSy5JY3w4+fbWxqYqUzEiUEwJnTFkfFw3l5AJUwNXKs57XqZ
MFdxNTrFVE7N8hE3RzW93J3cS/cDF4gkmhDJFT5svDwsuQUnfrTMsC1SpNx2HJzK
AcpDOgKHfDf/5H1eV9THMK3uqCpg0144iVxWazBFENZ+GGWUjAhyEFM8VIazk/6S
8vc2DvWHI0hN0gNGIddY1rEjbZ6Vdi5yXTtja5A2mE7adHNvrc+aAUX8o94n59Af
STflvOlXarKkGGiZnyrkVovH71QOfoQBES8W1fGmRdJu2sjhkf/68XZTXFp+z/8a
Ww==
-----END CERTIFICATE-----
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA6UB6KxIe3m9S/BybinIsi/6qCwL86xxL59G2ny+q8sC7vrAa
pyNIwS4u+q42DpTu0bsvF/7W2eqzI6ACI1uc45OkgGTGE1QHKCZOqCoNYYEVQOmT
EN7r0wb8PSQL5yctp7I10DWplvxCxbZTAI/Vzp01OwRqIbp1HCEE38oouSGWcGhv
1bMiVwVThlHCaBvnH+mkvRPuyA/wTR+KmCBOyCvo2buRLZsd/AZs1yZKNUtI0Vhr
2kmSjOPEtRDbD9zYGyPfVU7u3kMo6JyuYKXmGWmkR2Dk/ojzVN5RmKsi2evNCGgc
2Qswo1DQhtHvOm/x+9MlHWe6h9dcoLY40TAwEwIDAQABAoIBAQCJ5n7T3AC4DSUI
vHkQ3vOLSOKLZBbXgYE26vuHHY15u4zD7iWzzt6Cp6VKu5hmApM3F+19yEKJz4Jd
e4oIEW+t9Gn7JpkccD9S82LpdQM4NDK2j+FMxh6yQpz6IMeQv870KB48nJ1JEJcB
gQuNd3w2jScdXzzH+A6otLTHAiQVXq7uLcLZ3fqBYev4yh4wOQUETjr+HxPMPEFZ
hN42ZnGIRnV4AdrKPR4RSVXGJAhP1zaEg/jNjCWvljFkM9Db8fQcmjTBUEj3jil3
1Mtw5kfWrXHBHjnOMxjb5OZF1nQAjayMlNJ6RAkq6YTqegjkRTpDaBt5dYmwngdN
JjIXG7ihAoGBAP/MGklGE0wnDtRgqed/lDLb93Kr6ra3FKY8wAVyNANfIjrLbcJu
5fkTQzADn6qtF63euODFzJTAXPsBuu21Wr0kH5TwI2ARxTxMbesnOv5M3IH95Ft/
Jx/8vgMHDw9wEyUgXGT0IP3rHZXA0MHU5He4OMTjIuGekq3rHiWZIs2bAoGBAOlv
zOiFwtNZuvh9L7e+LwB2nVMxYH6suX+LKJ3ZtkZsfJ6KC/tol0BhB5aHNvpQnL+y
y4m/ohbDL1DyKVkyXwIDQEY/xFN7IyzU219GCgAX/w0glqRzrnl7DZgq2cma6bwR
2L5w+C68W6qXtW/0dNMm+n2bFUt29Bo2fUSyoQrpAoGASRXi6M6p0tdSCGI6CVfN
Wx64O75dCKmUr86puqSfsdrrp8rZ6HTRJnJXfw95/kUIf/gj8KzzQAkaR+l9pE9t
xp7cmyxlnxxUs17PBSInW+NQGaAWMAJLhnsrOyg4KFwITFJLs9iA2KvlvxThUJBo
WsB7D94p6Dbrh9+mtG1loUMCgYEAyR2wbZhojQyewF2ikeyBtCV9T+KXnyuSuacb
/DtJBg2LpE+NGTI4NZgjIHYoUKrJEYOqRmha7r+E8XlhVF7IagSBCMUBkWbinEpb
Ig9GqGQEMrwj7VVZOYJh6PrqHf6gZ478rL7JpAJV+3ivMeBsEktfvn2xcA3xKvBN
JrW7d5kCgYAP+PJAfksrkv3yXYG9Ks0a/A6Qfdyi5wXhRg8W+tO2BZnrZM1RSKOe
TQ0lSpAA7SLCSJwSEqxt22sY4mzM7PjWi824BF/eNJ4lB+DCRFIuSsv4dxTa9HdG
theL0jT06zzlIuUsA0npbadnkOGi1p7u5LtGbA6RI4Kc6L7sBow71w==
-----END RSA PRIVATE KEY-----
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICyzCCAbOgAwIBAgIJWVXVdVgS9u+DMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV
BAMTBFRlc3QwHhcNMjIwMjA0MDEzODU2WhcNMjMwMjA0MDEzODU2WjAPMQ0wCwYD
VQQDEwRUZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp1ILmaSl
1pUVFgt/nLtzoaGuBW4OPRFN5uU2rYPow3uy4NfULKiir4AWxGETcm4gFIOG3RIb
DJys6TAJJLnFEFnzpV+qaNeqaMqnXm4OBspx/WS2rSSGIwr7raKy4yrpPYJB1A2D
5fZ6oFoWROyjbHJvPddu0PqbbEWSM3PzdZYOKfF13ofzFOPlsTnlo12QWgksxrcT
wgeX7cjHsXaUc+I8U9DRR+9SgKhejWdFNPGcMkEc5BIXwtkTau+8+DwQzXz775Wl
hf0eJwfnNQeleblbLQIp8i+RmfIjPSt8W/iTSN64iBROpOvnOeJ9gKX922QDZoqW
z9o4RTNCPfyYBQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAww
CgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAHTyG2f/CReXVrS3zZjpgnje
Vn2/XYevnaHNWYIxjlUVGaASE/k6qWkM832f0gaZwJYXsll2YxJa5Jcdqa1yq8QA
Ay5YBq8Vv6K6iGTeZ5wTQc3xNys7zcS95eNq7yhDDjbLDbSDdZ8T/1hZqiha2DG5
itLGfBH+HywW2G9njLgIzNcmKx3d76y3jlVXqug1/AVFK9in+r/0t0TYlk+opqZb
H7cjJSefHusnq2vvvR6gbkZzyzSYP5FMlj/RjRCSJ+VHNwCn3fS+KDlJgVOk0Cxc
7n52UGazeFwH3mem89DN+Bhw/uyU0IcIaDLykb6PsTPy2tjHsLpK5kpvfrp2m0o=
-----END CERTIFICATE-----
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff. Show more