mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd02e8be4a | ||
|
|
89dda20149 | ||
|
|
b591698c31 | ||
|
|
66543b658b | ||
|
|
4769048578 | ||
|
|
64c1e47674 | ||
|
|
be3a86abda | ||
|
|
eb2ca117fc | ||
|
|
5043385077 | ||
|
|
bb4c08a52a | ||
|
|
8b75057fa1 | ||
|
|
80cc849137 | ||
|
|
2d407ea9d4 | ||
|
|
91456d1c65 | ||
|
|
757a0ccad7 | ||
|
|
4b7cac0392 | ||
|
|
8ff8022b3f | ||
|
|
a2acea9fe9 | ||
|
|
9abdfcfdbe | ||
|
|
696f9dde13 | ||
|
|
1e73c8c96f | ||
|
|
1b02a67d88 | ||
|
|
d0f158a546 | ||
|
|
16a2dc681b | ||
|
|
73800252ae | ||
|
|
e7c4ba1f54 | ||
|
|
84cddaf0e1 | ||
|
|
896f7f0385 | ||
|
|
7f31ea6a2d | ||
|
|
3ef397f96c | ||
|
|
680a684a8e | ||
|
|
1eae0c75ee | ||
|
|
483e0f2dca | ||
|
|
6ee843e41c | ||
|
|
5402ebdc5c | ||
|
|
1b5119d818 | ||
|
|
8a82365680 | ||
|
|
9c55621719 | ||
|
|
1afc3093ec | ||
|
|
790c3d776e | ||
|
|
be84ffd62a | ||
|
|
fa812f448f | ||
|
|
d09161b745 | ||
|
|
653f26257b | ||
|
|
b1e874966c | ||
|
|
315abf0a03 | ||
|
|
89a9255929 | ||
|
|
cae0ba21eb | ||
|
|
ed52e291cb | ||
|
|
6db7ac617b | ||
|
|
1dd9614dbe | ||
|
|
67c58a75d9 | ||
|
|
5b3e4cb3e1 | ||
|
|
caf257ce9c | ||
|
|
31122623fa | ||
|
|
eb36dd0fef | ||
|
|
bb0723354f | ||
|
|
1b9feed040 | ||
|
|
0220ca17a6 | ||
|
|
74cec12c23 | ||
|
|
e807fc105d | ||
|
|
ae30c4c7db | ||
|
|
35b9fda122 | ||
|
|
8d010ad7e8 | ||
|
|
d7181478c8 |
No files matched your search
@@ -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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement'
|
||||
labels: 'enhancement,needs review'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.47.0"
|
||||
"version": "0.51.0"
|
||||
}
|
||||
+8
-7
@@ -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
@@ -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.
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/core')
|
||||
.run()
|
||||
.then(require('@oclif/core/flush'))
|
||||
.catch(require('@oclif/core/handle'));
|
||||
Executable
+6
@@ -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);
|
||||
Generated
+998
-184
File diff suppressed because it is too large.
Load diff
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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(() => {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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
-12
@@ -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
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }, {});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+4
-2
@@ -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
|
||||
File renamed without changes.
+11
-7
@@ -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;
|
||||
File renamed without changes.
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
Reference in new issue
Block a user