Compare commits

..
Author SHA1 Message Date
Dominik Sokal 7768a62f48 remove plugin-warn-if-update-available 2022-04-01 12:32:51 +02:00
Dominik Sokal 85f0372738 upgrade dependencies 2022-04-01 12:31:33 +02:00
134 changed files with 2392 additions and 4184 deletions

No files matched your search

-24
View File
@@ -1,24 +0,0 @@
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
+1 -50
View File
@@ -10,60 +10,11 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
### 🧹 Chores
- Update dependencies. ([#1095](https://github.com/expo/eas-cli/pull/1095) by [@dsokal](https://github.com/dsokal))
## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26
### 🎉 New features
- Support target specific entitlements. ([#1078](https://github.com/expo/eas-cli/pull/1078) by [@wkozyra95](https://github.com/wkozyra95))
## [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))
- Add annotations regarding App Store Connect Token session support. ([#1029](https://github.com/expo/eas-cli/pull/1029) by [@EvanBacon](https://github.com/EvanBacon))
## [0.48.2](https://github.com/expo/eas-cli/releases/tag/v0.48.2) - 2022-03-21
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.52.0"
"version": "0.48.2"
}
+10 -10
View File
@@ -17,19 +17,19 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "27.5.0",
"@types/node": "17.0.32",
"eslint": "8.15.0",
"eslint-config-universe": "11.0.0",
"@types/jest": "27.4.1",
"@types/node": "16.7.8",
"eslint": "8.12.0",
"eslint-config-universe": "10.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "28.1.0",
"jest-watch-typeahead": "1.1.0",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"lerna": "4.0.0",
"oclif": "3.0.1",
"prettier": "2.6.2",
"ts-jest": "28.0.2",
"typescript": "4.6.4"
"oclif": "2.6.3",
"prettier": "2.6.1",
"ts-jest": "27.1.4",
"typescript": "4.6.3"
},
"volta": {
"node": "16.10.0",
+43 -137
View File
@@ -48,7 +48,6 @@ 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)
@@ -62,9 +61,6 @@ 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)
@@ -81,7 +77,6 @@ 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`
@@ -98,7 +93,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -115,7 +110,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -132,7 +127,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -146,7 +141,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -195,7 +190,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -215,7 +210,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -232,7 +227,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -251,7 +246,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -271,7 +266,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -300,7 +295,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -314,7 +309,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -331,7 +326,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -366,7 +361,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.52.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -399,35 +394,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.52.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
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -444,7 +411,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -464,7 +431,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -485,7 +452,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -502,7 +469,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -522,7 +489,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -540,7 +507,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -554,7 +521,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -568,7 +535,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.52.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -586,7 +553,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -603,7 +570,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -617,7 +584,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -631,7 +598,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -653,51 +620,6 @@ DESCRIPTION
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_
## `eas init`
create or link an EAS project
```
USAGE
$ eas init
DESCRIPTION
create or link an EAS project
ALIASES
$ eas init
```
## `eas login`
log in with your Expo account
```
USAGE
$ eas login
DESCRIPTION
log in with your Expo account
ALIASES
$ eas login
```
## `eas logout`
log out
```
USAGE
$ eas logout
DESCRIPTION
log out
ALIASES
$ eas logout
```
## `eas project:info`
information about the current project
@@ -710,7 +632,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -727,7 +649,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -747,7 +669,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.52.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -764,7 +686,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -778,7 +700,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -808,7 +730,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -817,7 +739,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] [--private-key-path <value>] [--non-interactive]
[--group <value> | | ] [-p android|ios|all] [--json] [--auto] [--private-key-path <value>]
FLAGS
-p, --platform=(android|ios|all) [default: all]
@@ -827,7 +749,6 @@ 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.
@@ -838,7 +759,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -855,7 +776,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -875,7 +796,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.52.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -894,7 +815,7 @@ DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -914,7 +835,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -934,7 +855,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -951,7 +872,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -968,7 +889,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -989,7 +910,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1006,20 +927,5 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.52.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
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.48.2/packages/eas-cli/src/commands/webhook/view.ts)_
<!-- commandsstop -->
+78 -982
View File
File diff suppressed because it is too large. Load diff
+34 -36
View File
@@ -1,51 +1,49 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.52.0",
"version": "0.48.2",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.31",
"@expo/apple-utils": "0.0.0-alpha.30",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.23",
"@expo/config-plugins": "4.1.4",
"@expo/config-types": "45.0.0",
"@expo/eas-build-job": "0.2.72",
"@expo/eas-json": "0.52.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.18",
"@expo/plugin-warn-if-update-available": "2.2.0",
"@expo/prebuild-config": "3.1.6",
"@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.48.0",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@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.8.1",
"@oclif/plugin-autocomplete": "1.3.0",
"@oclif/core": "1.6.4",
"@oclif/plugin-autocomplete": "1.2.0",
"@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.4",
"@urql/exchange-retry": "0.3.3",
"@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2",
"chalk": "4.1.2",
"cli-progress": "3.11.0",
"cli-table3": "0.6.2",
"cli-table3": "0.6.1",
"dateformat": "4.6.3",
"dicer": "0.3.1",
"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.1.0",
"fs-extra": "10.0.1",
"getenv": "1.0.0",
"gradle-to-js": "2.0.1",
"graphql": "16.5.0",
"gradle-to-js": "2.0.0",
"graphql": "16.3.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"https-proxy-agent": "5.0.0",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.2",
@@ -53,8 +51,8 @@
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.0.1",
"nanoid": "3.3.4",
"minimatch": "3.0.5",
"nanoid": "3.3.2",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
@@ -63,12 +61,13 @@
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.7",
"semver": "7.3.5",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"structured-headers": "0.4.1",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"tslib": "2.4.0",
"tslib": "2.3.1",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.2",
@@ -77,23 +76,23 @@
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.11",
"@graphql-codegen/typescript-operations": "2.4.0",
"@types/cli-progress": "3.9.2",
"@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5",
"@types/dateformat": "3.0.1",
"@types/dicer": "0.2.2",
"@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.2",
"@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.2",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.27.2",
"axios": "0.26.1",
"eslint-plugin-graphql": "4.0.0",
"form-data": "4.0.0",
"memfs": "3.4.1",
@@ -101,7 +100,7 @@
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"typescript": "4.6.4"
"typescript": "4.6.3"
},
"engines": {
"node": ">=12.0.0"
@@ -123,8 +122,7 @@
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@oclif/plugin-help",
"@expo/plugin-warn-if-update-available"
"@oclif/plugin-help"
],
"topics": {
"account": {
+3
View File
@@ -0,0 +1,3 @@
export function asMock(fn: any): jest.Mock {
return fn as jest.Mock;
}
@@ -105,7 +105,7 @@ export async function maybeResolveVersionsAsync(
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
};
} catch {
} catch (err: any) {
return {};
}
} else {
+129 -279
View File
@@ -1,29 +1,16 @@
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 { getExpoWebsiteBaseUrl } from '../api';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore } from '../log';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { ora } from '../ora';
import { requestedPlatformDisplayNames } from '../platform';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
@@ -113,43 +100,43 @@ export async function prepareBuildRequestForPlatformAsync<
try {
return await sendBuildRequestAsync(builder, job, metadata);
} catch (error: any) {
handleBuildRequestError(error, job.platform);
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;
}
}
};
}
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> {
@@ -221,242 +208,105 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
);
}
type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
{ 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;
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 - canceled;
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');
}
}
time = new Date().getTime();
await sleepAsync(intervalSec * 1000);
}
spinner.fail('Timed out');
spinner.warn('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();
}
+7 -6
View File
@@ -36,12 +36,13 @@ export async function createIosContextAsync(
},
buildProfile
);
const targets = await resolveTargetsAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env: buildProfile.env,
});
const targets = await resolveTargetsAsync(
{
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
@@ -2,6 +2,7 @@ import { Platform } from '@expo/eas-build-job';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
@@ -18,6 +19,13 @@ export async function ensureIosCredentialsAsync(
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.projectDir,
buildCtx.workflow,
buildCtx.buildProfile.env ?? {}
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
});
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.78';
const PLUGIN_PACKAGE_VERSION = '0.0.67';
export interface LocalBuildOptions {
enable: boolean;
@@ -137,11 +137,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
return;
}
const { accountName } = Object.values(buildCtxByPlatform)[0];
const builds = await waitForBuildEndAsync({
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
});
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
@@ -103,7 +103,10 @@ 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.');
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(deprecationInfo.message);
} else {
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
@@ -1,8 +1,12 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand';
import TestEasCommand from './TestEasCommand';
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
@@ -28,30 +32,17 @@ afterAll(() => {
});
beforeEach(() => {
jest
.mocked(getUserAsync)
asMock(getUserAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).mockReset();
});
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand;
};
describe(EasCommand.name, () => {
describe('without exceptions', () => {
// The first test in this suite should have an increased timeout
@@ -62,36 +53,32 @@ describe(EasCommand.name, () => {
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user is logged in', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
it('ensures the user data is read from cache', async () => {
const TestEasCommand = createTestEasCommand(false);
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
await TestEasCommand.run();
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
expect(getUserAsync).toHaveReturnedWith(mockJester);
});
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
@@ -102,12 +89,11 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => {
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
});
} catch {}
} catch (error) {}
expect(flushAsync).toHaveBeenCalled();
});
@@ -0,0 +1,13 @@
import EasCommand from '../EasCommand';
export default class TestEasCommand extends EasCommand {
requiresAuthentication = this.authValue();
authValue(): boolean {
return false;
}
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
@@ -104,7 +104,7 @@ async function selectBuildToCancelAsync(
async function ensureBuildExistsAsync(buildId: string): Promise<void> {
try {
await BuildQuery.byIdAsync(buildId);
} catch {
} catch (err) {
throw new Error(`Couldn't find a build matching the id ${buildId}`);
}
}
@@ -102,7 +102,7 @@ export default class BuildInspect extends EasCommand {
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
} catch {
} catch (err) {
if (!flags.verbose) {
Log.error('Build failed');
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
@@ -45,7 +45,7 @@ export function getBranchMapping(branchMappingString?: string): {
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch {
} catch (e) {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
+2 -38
View File
@@ -194,10 +194,6 @@ export default class UpdatePublish extends EasCommand {
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> {
@@ -213,7 +209,6 @@ export default class UpdatePublish extends EasCommand {
'skip-bundler': skipBundler,
platform,
'private-key-path': privateKeyPath,
'non-interactive': nonInteractive,
},
} = await this.parse(UpdatePublish);
if (jsonFlag) {
@@ -230,24 +225,9 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: true,
});
const { exp: expPrivate } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
isPublicConfig: false,
});
const codeSigningInfo = await getCodeSigningInfoAsync(exp, privateKeyPath);
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) {
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
const install = await confirmAsync({
message: `You are creating an update which requires ${chalk.bold(
'expo-updates'
@@ -274,10 +254,6 @@ 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);
@@ -327,10 +303,6 @@ 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.');
@@ -395,10 +367,6 @@ 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);
@@ -417,10 +385,6 @@ 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);
@@ -1,5 +1,5 @@
import AppStoreApi from '../ios/appstore/AppStoreApi';
import { AuthCtx } from '../ios/appstore/authenticateTypes';
import { AuthCtx } from '../ios/appstore/authenticate';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
@@ -76,9 +76,7 @@ export const testPushKey: ApplePushKeyFragment = {
iosAppCredentialsList: [],
};
export const testTargets: Target[] = [
{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com', entitlements: {} },
];
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
@@ -1,8 +1,5 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { GoogleServiceAccountKeyFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
@@ -44,33 +41,14 @@ export class CreateGoogleServiceAccountKey {
return detectedPath;
}
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.';
}
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",
},
});
return filePath;
]);
return keyJsonPath;
}
}
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -16,7 +17,7 @@ import {
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
@@ -83,9 +84,7 @@ describe('BuildCredentialsUtils', () => {
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ providedName: 'test-provided-name' }));
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,10 +1,11 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
@@ -1,5 +1,6 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
@@ -7,8 +8,8 @@ import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey'
jest.mock('../../../../prompts');
jest.mock('fs');
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
}));
beforeEach(() => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -14,7 +15,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('CreateKeystore', () => {
beforeEach(() => {
jest.mocked(generateRandomKeystoreAsync).mockReset();
asMock(generateRandomKeystoreAsync).mockReset();
});
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -27,7 +28,7 @@ describe('CreateKeystore', () => {
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
});
it('creates a keystore by uploading', async () => {
jest.mocked(askForUserProvidedAsync).mockImplementationOnce(async () => testKeystore);
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -8,7 +9,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveFcm, () => {
it('removes an FCM API Key', async () => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -7,7 +8,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { RemoveGoogleServiceAccountKey } from '../RemoveGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveGoogleServiceAccountKey, () => {
it('removes an Google Service Account Key', async () => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -6,7 +7,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
@@ -18,7 +19,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
jest.mocked(generateRandomKeystoreAsync).mockReset();
asMock(generateRandomKeystoreAsync).mockReset();
});
it('skips setup when there are prior credentials', async () => {
@@ -1,5 +1,6 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { confirmAsync } from '../../../../prompts';
import {
@@ -12,14 +13,13 @@ import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { AppLookupParams } from '../../api/GraphqlClient';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
beforeEach(() => {
@@ -28,7 +28,7 @@ beforeEach(() => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
runAsync: () => {
return {
@@ -37,7 +37,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
});
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -78,7 +78,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('uses an existing build configuration upon user request', async () => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
runAsync: () => {
return {
@@ -87,7 +87,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
});
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,5 +1,6 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -13,8 +14,8 @@ import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
asMock(promptAsync).mockImplementation(() => ({
keyJsonPath: '/google-service-account-key.json',
}));
beforeEach(() => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -8,9 +9,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
describe(UseExistingGoogleServiceAccountKey, () => {
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
@@ -1,5 +1,6 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { detectGoogleServiceAccountKeyPathAsync } from '../googleServiceAccountKey';
@@ -24,13 +25,13 @@ afterAll(() => {
});
afterEach(() => {
jest.mocked(promptAsync).mockClear();
jest.mocked(confirmAsync).mockClear();
asMock(promptAsync).mockClear();
asMock(confirmAsync).mockClear();
});
describe('Google Service Account Key path detection', () => {
it('detects a single google-services file and prompts for confirmation', async () => {
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
asMock(confirmAsync).mockResolvedValueOnce(true);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Would you like to use this file?' })
@@ -45,7 +46,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user rejects to use detected file', async () => {
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
asMock(confirmAsync).mockResolvedValueOnce(false);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledTimes(1);
expect(confirmAsync).toHaveBeenCalledWith(
@@ -55,7 +56,7 @@ describe('Google Service Account Key path detection', () => {
});
it('displays a chooser, when multiple files are found', async () => {
jest.mocked(promptAsync).mockResolvedValueOnce({
asMock(promptAsync).mockResolvedValueOnce({
selectedPath: '/project_dir/another-service-account.json',
});
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
@@ -75,7 +76,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user selects the "None of above"', async () => {
jest.mocked(promptAsync).mockResolvedValueOnce({
asMock(promptAsync).mockResolvedValueOnce({
selectedPath: false,
});
@@ -1,5 +1,6 @@
import mockdate from 'mockdate';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import {
testAndroidAppCredentialsFragment,
@@ -24,9 +25,7 @@ describe('print credentials', () => {
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
});
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue.toString(), '');
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -20,7 +20,7 @@ function fileIsServiceAccountKey(keyJsonPath: string): boolean {
try {
readAndValidateServiceAccountKey(keyJsonPath);
return true;
} catch {
} catch (err: any) {
return false;
}
}
@@ -24,7 +24,7 @@ export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch {
} catch (error) {
return false;
}
}
@@ -24,7 +24,7 @@ function isPKCSKeystore(keystore: Keystore): boolean {
try {
parsePKCS12(keystore.keystore, keystore.keystorePassword);
return true;
} catch {
} catch (e) {
return false;
}
}
@@ -33,7 +33,7 @@ function isJKSKeystore(keystore: Keystore): boolean {
try {
jks.parseJks(Buffer.from(keystore.keystore, 'base64'), keystore.keystorePassword);
return true;
} catch {
} catch (e) {
return false;
}
}
+1 -7
View File
@@ -10,7 +10,6 @@ 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;
@@ -50,7 +49,7 @@ export class CredentialsContext {
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch {
} catch (error) {
// ignore error, context might be created outside of expo project
return null;
}
@@ -100,11 +99,6 @@ 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.'
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import prompts from 'prompts';
import { asMock } from '../../../__tests__/utils';
import { IosDistributionType } from '../../../graphql/generated';
import {
testKeystore,
@@ -24,8 +25,8 @@ jest.mock('prompts');
describe('update credentials.json', () => {
beforeEach(() => {
vol.reset();
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -175,7 +176,6 @@ describe('update credentials.json', () => {
{
targetName: 'testapp',
bundleIdentifier: 'com.bundle.id',
entitlements: {},
},
];
@@ -114,7 +114,7 @@ export async function readRawAsync(
try {
const credentialsJSONContents = await fs.readFile(credentialsJsonFilePath, 'utf8');
return JSON.parse(credentialsJSONContents);
} catch {
} catch (err) {
throw new Error(
`credentials.json must exist in the project root directory and contain a valid JSON`
);
@@ -11,6 +11,7 @@ import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -19,6 +20,7 @@ interface Options {
targets: Target[];
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export default class IosCredentialsProvider {
@@ -61,6 +63,7 @@ export default class IosCredentialsProvider {
targets: this.options.targets,
distribution: this.options.distribution,
enterpriseProvisioning: this.options.enterpriseProvisioning,
iosCapabilitiesOptions: this.options.iosCapabilitiesOptions,
}).runAsync(this.ctx);
}
@@ -58,13 +58,7 @@ describe(IosCredentialsProvider, () => {
},
projectName: appLookupParams.projectName,
},
targets: [
{
targetName: 'testapp',
bundleIdentifier: appLookupParams.bundleIdentifier,
entitlements: {},
},
],
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
distribution: 'store',
});
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
@@ -101,13 +95,7 @@ describe(IosCredentialsProvider, () => {
},
projectName: appLookupParams.projectName,
},
targets: [
{
targetName: 'testapp',
bundleIdentifier: appLookupParams.bundleIdentifier,
entitlements: {},
},
],
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
distribution: 'store',
});
@@ -94,6 +94,7 @@ 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/authenticateTypes';
import { AuthCtx } from '../appstore/authenticate';
export class ConfigureProvisioningProfile {
constructor(
@@ -84,7 +84,7 @@ export class ConfigureProvisioningProfile {
certP12: certificateP12,
certPassword: certificatePassword,
distCertSerialNumber: serialNumber,
teamId: authCtx.team.id,
teamId: authCtx.appleId,
}
);
@@ -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/authenticateTypes';
import { AuthCtx } from '../appstore/authenticate';
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.team.id,
teamId: this.distributionCertificate.appleTeam?.appleTeamIdentifier ?? appleAuthCtx.appleId,
});
}
}
@@ -4,6 +4,7 @@ import nullthrows from 'nullthrows';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
@@ -13,6 +14,7 @@ interface Options {
targets: Target[];
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetUpBuildCredentials {
@@ -48,9 +50,7 @@ export class SetUpBuildCredentials {
Log.newLine();
}
const action = new SetUpTargetBuildCredentials({
enterpriseProvisioning: this.options.enterpriseProvisioning,
distribution: this.options.distribution,
entitlements: target.entitlements,
...this.options,
app: {
...this.options.app,
bundleIdentifier: target.bundleIdentifier,
@@ -1,5 +1,4 @@
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { JSONObject } from '@expo/json-file';
import {
IosDistributionType as GraphQLIosDistributionType,
@@ -8,6 +7,7 @@ import {
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
@@ -16,13 +16,13 @@ interface Options {
app: GraphQLAppLookupParams;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
entitlements: JSONObject;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetUpTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const { app, entitlements } = this.options;
const { app, iosCapabilitiesOptions } = this.options;
await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -33,7 +33,7 @@ export class SetUpTargetBuildCredentials {
bundleIdentifier: app.bundleIdentifier,
projectName: app.projectName,
},
{ entitlements }
iosCapabilitiesOptions
);
}
try {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -17,8 +18,8 @@ function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
}
afterEach(() => {
jest.mocked(promptAsync).mockClear();
jest.mocked(getCredentialsFromUserAsync).mockClear();
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
@@ -34,10 +35,10 @@ describe(getAscApiKeyName, () => {
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -61,10 +62,10 @@ describe(promptForAscApiKeyPathAsync, () => {
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
@@ -84,6 +85,6 @@ describe(promptForAscApiKeyPathAsync, () => {
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(jest.mocked(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -14,7 +15,7 @@ import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('ConfigureProvisioningProfile', () => {
it('configures a Provisioning Profile in Interactive Mode', async () => {
@@ -42,9 +43,9 @@ describe('ConfigureProvisioningProfile', () => {
await provProfConfigurator.runAsync(ctx);
// expect provisioning profile not to be updated on expo servers
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be updated on apple portal
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -61,8 +62,8 @@ describe('ConfigureProvisioningProfile', () => {
);
// expect provisioning profile not to be updated on expo servers
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -8,7 +9,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
@@ -26,9 +27,9 @@ describe(CreateAscApiKey, () => {
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(jest.mocked(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -12,7 +13,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('CreateProvisioningProfile', () => {
it('creates a Provisioning Profile in Interactive Mode', async () => {
@@ -33,9 +34,9 @@ describe('CreateProvisioningProfile', () => {
await createProvProfAction.runAsync(ctx);
// expect provisioning profile to be created on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be created on apple portal
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -51,8 +52,8 @@ describe('CreateProvisioningProfile', () => {
);
// expect provisioning profile not to be created on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be created on apple portal
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -7,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreatePushKey } from '../CreatePushKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(CreatePushKey, () => {
it('creates a Push Key in Interactive Mode', async () => {
@@ -24,9 +25,9 @@ describe(CreatePushKey, () => {
await createPushKeyAction.runAsync(ctx);
// expect push key to be created on expo servers
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be created on apple portal
expect(jest.mocked(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,10 +1,11 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment } from '../../../__tests__/fixtures-ios';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -10,7 +11,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
@@ -23,11 +24,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate on Expo servers when there are no App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -39,11 +40,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// not supported in non-interactive mode
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate and its provisioning profile on Expo and Apple servers when there are App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -55,11 +56,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be invoked on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('errors when the distribution certificate has App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testTargets } from '../../../__tests__/fixtures-ios';
@@ -19,6 +20,6 @@ describe('RemoveProvisioningProfile', () => {
await removeProfilesAction.runAsync(ctx);
// expect provisioning profile to be deleted from servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
});
@@ -1,10 +1,11 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testPushKey } from '../../../__tests__/fixtures-ios';
import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
@@ -13,9 +14,9 @@ describe(RemovePushKey, () => {
await removePushKeyAction.runAsync(ctx);
// expect push key to be deleted on expo servers
expect(jest.mocked(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be revoked on apple portal
expect(jest.mocked(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -14,7 +15,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC API Key if it is already configured', async () => {
@@ -35,9 +36,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC API Key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
const ctx = createCtxMock({
@@ -60,9 +61,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC API Key on expo servers
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC API Key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -86,9 +87,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
@@ -98,7 +99,7 @@ describe(SetUpAscApiKey, () => {
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
asMock(promptAsync).mockImplementation(() => ({
choice: SetupAscApiKeyChoice.USE_EXISTING,
chosenAscApiKey: testAscApiKeyFragment,
}));
@@ -110,9 +111,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
const ctx = createCtxMock({
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../../graphql/generated';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -13,10 +14,10 @@ jest.mock('../SetUpProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
beforeEach(() => {
jest.mocked(promptAsync).mockReset();
asMock(promptAsync).mockReset();
jest.mocked(getAllBuildCredentialsAsync).mockReset();
jest.mocked(getAllBuildCredentialsAsync).mockImplementation(() => {
asMock(getAllBuildCredentialsAsync).mockReset();
asMock(getAllBuildCredentialsAsync).mockImplementation(() => {
throw new Error(
`unhandled getAllBuildCredentialsAsync call - this shouldn't happen - fix tests!`
);
@@ -36,7 +37,7 @@ describe(SetUpInternalProvisioningProfile, () => {
describe('interactive mode', () => {
describe('when authenticated with apple', () => {
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -63,10 +64,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('asks the user for an action to run when they have access to an enterprise team', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -98,10 +99,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
describe('when not authenticated with apple', () => {
it('asks the user for an action to run when both adhoc and universal distribution credentials exist', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testAdhocBuildCredentials,
testEnterpriseBuildCredentials,
@@ -132,10 +133,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
@@ -163,10 +164,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testEnterpriseBuildCredentials,
];
@@ -196,10 +197,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('forces the apple authentication when neither adhoc nor enterprise credentials exist', async () => {
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -227,7 +228,7 @@ describe(SetUpInternalProvisioningProfile, () => {
describe('non-interactive mode', () => {
it('throws an error when both adhoc and enterprise credentials are set up', async () => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testAdhocBuildCredentials,
testEnterpriseBuildCredentials,
@@ -248,7 +249,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('throws an error when neither adhoc nor enterprise credentials are set up', async () => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -264,7 +265,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
@@ -286,7 +287,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
return buildCredentials;
});
@@ -1,5 +1,6 @@
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -18,7 +19,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
jest.mock('../SetUpDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
@@ -26,7 +27,7 @@ jest.mock('../../validators/validateProvisioningProfile');
describe('SetUpProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -60,14 +61,12 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -92,14 +91,12 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
asMock(validateProvisioningProfileAsync).mockImplementation(() => true);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -124,14 +121,12 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -152,14 +147,12 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -173,10 +166,8 @@ describe('SetUpProvisioningProfile', () => {
);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
});
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -7,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpPushKey } from '../SetUpPushKey';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
@@ -23,9 +24,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a Push Key, creating a new one if there are no existing push keys', async () => {
const ctx = createCtxMock({
@@ -41,9 +42,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect to create a new push key on expo servers
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new push key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a Push Key, use autoselected push key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -65,9 +66,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers, we are using the existing one
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -14,16 +15,15 @@ import {
} from '../SetUpSubmissionCredentials';
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpSubmissionCredentials, () => {
it('allows user to enter an App Specific Password', async () => {
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({
asMock(promptAsync)
.mockImplementationOnce(() => ({
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
}))
.mockImplementationOnce(async () => ({
.mockImplementationOnce(() => ({
appSpecificPassword: 'super secret',
}));
const ctx = createCtxMock({
@@ -37,7 +37,7 @@ describe(SetUpSubmissionCredentials, () => {
const asp = await setupAscApiKeyAction.runAsync(ctx);
// prompt to choose ASP, then prompt to input ASP
expect(jest.mocked(promptAsync).mock.calls.length).toBe(2);
expect(asMock(promptAsync).mock.calls.length).toBe(2);
expect(asp).toBe('super secret');
});
it('returns an ASC API key', async () => {
@@ -1,3 +1,4 @@
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -20,8 +21,8 @@ jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
beforeEach(() => {
jest.mocked(confirmAsync).mockReset();
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockReset();
asMock(confirmAsync).mockImplementation(() => true);
});
describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
@@ -60,13 +61,11 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
// expect distribution certificate not to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('sets up build credentials with no prior configuration in Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -91,13 +90,11 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect distribution certificate to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('sets up build credentials with different prior configuration in Interactive Mode', async () => {
// create deep clone the quick and dirty way
@@ -126,13 +123,11 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect distribution certificate to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode - sets up build credentials with different prior configuration', async () => {
// create deep clone the quick and dirty way
@@ -162,14 +157,12 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
expect(jest.mocked(confirmAsync).mock.calls.length).toBe(0);
expect(asMock(confirmAsync).mock.calls.length).toBe(0);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
// expect distribution certificate to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
});
@@ -1,4 +1,3 @@
import Log from '../../../log';
import {
AscApiKey,
AscApiKeyInfo,
@@ -15,13 +14,7 @@ import {
listAscApiKeysAsync,
revokeAscApiKeyAsync,
} from './ascApiKey';
import {
Options as AuthenticateOptions,
assertUserAuthCtx,
authenticateAsync,
isUserAuthCtx,
} from './authenticate';
import { AuthCtx, AuthenticationMode, UserAuthCtx } from './authenticateTypes';
import { AuthCtx, Options as AuthenticateOptions, authenticateAsync } from './authenticate';
import {
createDistributionCertificateAsync,
listDistributionCertificatesAsync,
@@ -41,36 +34,13 @@ 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) {
const mode = options?.mode ?? this.defaultAuthenticationMode;
this.authCtx = await authenticateAsync({ mode, ...options });
this.authCtx = await authenticateAsync(options);
}
return this.authCtx;
}
@@ -99,18 +69,18 @@ export default class AppStoreApi {
}
public async listPushKeysAsync(): Promise<PushKeyStoreInfo[]> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await listPushKeysAsync(userCtx);
const ctx = await this.ensureAuthenticatedAsync();
return await listPushKeysAsync(ctx);
}
public async createPushKeyAsync(name?: string): Promise<PushKey> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await createPushKeyAsync(userCtx, name);
const ctx = await this.ensureAuthenticatedAsync();
return await createPushKeyAsync(ctx, name);
}
public async revokePushKeyAsync(ids: string[]): Promise<void> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await revokePushKeyAsync(userCtx, ids);
const ctx = await this.ensureAuthenticatedAsync();
return await revokePushKeyAsync(ctx, ids);
}
public async useExistingProvisioningProfileAsync(
@@ -174,22 +144,22 @@ export default class AppStoreApi {
}
public async listAscApiKeysAsync(): Promise<AscApiKeyInfo[]> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await listAscApiKeysAsync(userCtx);
const ctx = await this.ensureAuthenticatedAsync();
return await listAscApiKeysAsync(ctx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo | null> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await getAscApiKeyAsync(userCtx, keyId);
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
}
public async createAscApiKeyAsync({ nickname }: { nickname: string }): Promise<AscApiKey> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await createAscApiKeyAsync(userCtx, { nickname });
const ctx = await this.ensureAuthenticatedAsync();
return await createAscApiKeyAsync(ctx, { nickname });
}
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
const userCtx = await this.ensureUserAuthenticatedAsync();
return await revokeAscApiKeyAsync(userCtx, keyId);
const ctx = await this.ensureAuthenticatedAsync();
return await revokeAscApiKeyAsync(ctx, keyId);
}
}
@@ -1,5 +1,6 @@
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
import { asMock } from '../../../../__tests__/utils';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
@@ -56,7 +57,7 @@ beforeEach(() => {
test(`listAscApiKeysAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'getAsync').mockImplementation(async () => []);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await listAscApiKeysAsync(mockAuthCtx);
expect(ApiKey.getAsync).toHaveBeenLastCalledWith(mockRequestContext);
expect(result).toEqual([]);
@@ -65,7 +66,7 @@ test(`listAscApiKeysAsync`, async () => {
test(`getAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await getAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, { id: 'test-key-id' });
expect(result).toEqual(mockAscApiKeyInfo);
@@ -74,7 +75,7 @@ test(`getAscApiKeyAsync`, async () => {
test(`createAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
@@ -88,7 +89,7 @@ test(`createAscApiKeyAsync`, async () => {
test(`revokeAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await revokeAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, {
id: 'test-key-id',
@@ -1,91 +0,0 @@
import { vol } from 'memfs';
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 = {};
jest.mocked(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,
});
jest.mocked(promptAsync).mockImplementation(async () => ({
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 () => {
jest.mocked(promptAsync).mockImplementation(async () => ({
appleTeamId: testTeam.teamId,
appleTeamType: testTeam.teamType,
}));
const team = await resolveAppleTeamAsync();
expect(team).toMatchObject({
id: testTeam.teamId,
name: undefined,
inHouse: true,
});
});
});
@@ -3,40 +3,31 @@ import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
import Log from '../../../log';
import { ora } from '../../../ora';
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { getRequestContext } from './authenticate';
import { AuthCtx, UserAuthCtx } from './authenticateTypes';
import { AuthCtx, getRequestContext } from './authenticate';
/**
* List App Store Connect API Keys.
* **Does not support App Store Connect API (CI).**
*/
export async function listAscApiKeysAsync(userAuthCtx: UserAuthCtx): Promise<AscApiKeyInfo[]> {
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
return keys.map(key => getAscApiKeyInfo(key, userAuthCtx));
return keys.map(key => getAscApiKeyInfo(key, authCtx));
} 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(
userAuthCtx: UserAuthCtx,
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo | null> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, userAuthCtx);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error: any) {
const message = error?.message ?? '';
if (message.includes("There is no resource of type 'apiKeys' with id")) {
@@ -49,12 +40,8 @@ export async function getAscApiKeyAsync(
}
}
/**
* Create an App Store Connect API Key.
* **Does not support App Store Connect API (CI).**
*/
export async function createAscApiKeyAsync(
userAuthCtx: UserAuthCtx,
authCtx: AuthCtx,
{
nickname,
allAppsVisible,
@@ -64,7 +51,7 @@ export async function createAscApiKeyAsync(
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
nickname: nickname ?? `[expo] ${new Date().getTime()}`,
allAppsVisible: allAppsVisible ?? true,
@@ -89,7 +76,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, userAuthCtx),
...getAscApiKeyInfo(fullKey, authCtx),
keyP8,
};
} catch (err: any) {
@@ -98,21 +85,17 @@ export async function createAscApiKeyAsync(
}
}
/**
* Revoke an App Store Connect API Key.
* **Does not support App Store Connect API (CI).**
*/
export async function revokeAscApiKeyAsync(
userAuthCtx: UserAuthCtx,
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
return getAscApiKeyInfo(revokedKey, userAuthCtx);
return getAscApiKeyInfo(revokedKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
@@ -4,28 +4,16 @@ 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,
resolveAppleTeamAsync,
resolveAscApiKeyAsync,
resolveUserCredentialsAsync,
resolveCredentialsAsync,
} from './resolveCredentials';
const APPLE_IN_HOUSE_TEAM_TYPE = 'in-house';
@@ -33,27 +21,31 @@ 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 function isUserAuthCtx(authCtx: AuthCtx | undefined): authCtx is UserAuthCtx {
return !!authCtx && typeof (authCtx as UserAuthCtx).appleId === 'string';
}
export type Team = {
id: string;
name: string;
inHouse?: boolean;
};
export function assertUserAuthCtx(authCtx: AuthCtx | undefined): UserAuthCtx {
if (isUserAuthCtx(authCtx)) {
return authCtx;
}
throw new Error('Expected user authentication context (login/password).');
}
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 getRequestContext(authCtx: AuthCtx): RequestContext {
assert(authCtx.authState?.context, 'Apple request context must be defined');
@@ -75,7 +67,7 @@ async function loginAsync(
}
// Resolve the user credentials, optimizing for password-less login.
const { username, password } = await resolveUserCredentialsAsync(userCredentials);
const { username, password } = await resolveCredentialsAsync(userCredentials);
assert(username);
// Clear data
@@ -152,35 +144,6 @@ 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();
@@ -1,48 +0,0 @@
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,7 +2,6 @@ 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,
@@ -33,7 +32,7 @@ export async function syncCapabilityIdentifiersForEntitlementsAsync(
// App Store Connect token authentication is not currently supported,
// the team ID is required to create capability identifiers.
if (isAppStoreConnectTokenOnlyContext(bundleId.context)) {
if (bundleId.context.token && !bundleId.context.teamId) {
Log.warn(
`Skipping capability identifier syncing because the current Apple authentication session is not using Cookies (username/password).`
);
@@ -5,9 +5,6 @@ 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> {
@@ -20,9 +17,6 @@ async function getContractStatusAsync(
}
}
/**
* **Does not support App Store Connect API (CI).**
*/
async function getContractMessagesAsync(
context: RequestContext
): Promise<ITCAgreements.ITCContractMessage[] | null> {
@@ -34,9 +28,6 @@ async function getContractMessagesAsync(
}
}
/**
* **Does not support App Store Connect API (CI).**
*/
async function getRequiredContractMessagesAsync(
context: RequestContext
): Promise<{ messages: ITCAgreements.ITCContractMessage[]; isFatal: boolean }> {
@@ -86,9 +77,6 @@ export function formatContractMessage(message: ITCAgreements.ITCContractMessage)
});
}
/**
* **Does not support App Store Connect API (CI).**
*/
export async function assertContractMessagesAsync(
context: RequestContext,
spinner?: Ora
@@ -7,8 +7,7 @@ import {
import { ora } from '../../../ora';
import { DistributionCertificate, DistributionCertificateStoreInfo } from './Credentials.types';
import { getRequestContext } from './authenticate';
import { AuthCtx } from './authenticateTypes';
import { AuthCtx, getRequestContext } from './authenticate';
export class AppleTooManyCertsError extends Error {}
@@ -4,8 +4,7 @@ import chalk from 'chalk';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getRequestContext, isUserAuthCtx } from './authenticate';
import { AuthCtx, UserAuthCtx } from './authenticateTypes';
import { AuthCtx, getRequestContext } from './authenticate';
import { syncCapabilitiesForEntitlementsAsync } from './bundleIdCapabilities';
import { syncCapabilityIdentifiersForEntitlementsAsync } from './capabilityIdentifiers';
import { assertContractMessagesAsync } from './contractMessages';
@@ -73,13 +72,7 @@ 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/)
) {
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.`
);
}
await assertContractMessagesAsync(context);
}
}
@@ -157,7 +150,7 @@ export async function syncCapabilityIdentifiersAsync(
}
export async function ensureAppExistsAsync(
userAuthCtx: UserAuthCtx,
authCtx: AuthCtx,
{
name,
language,
@@ -172,7 +165,7 @@ export async function ensureAppExistsAsync(
sku?: string;
}
): Promise<App> {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
const spinner = ora(`Linking to App Store Connect ${chalk.dim(bundleIdentifier)}`).start();
let app = await App.findAsync(context, { bundleId: bundleIdentifier });
@@ -183,9 +176,7 @@ 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,
@@ -195,8 +186,9 @@ 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 "${userAuthCtx.authState?.session.provider.name}. Please change it in your app config and try again.\n`
`\nThe bundle identifier "${bundleIdentifier}" is not available to provider "${providerName}". Please change it in your app config and try again.\n`
);
}
@@ -1,14 +1,11 @@
import { IOSConfig, compileModsAsync } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import { JSONObject } from '@expo/json-file';
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
import { readPlistAsync } from '../../utils/plist';
import { readPlistAsync } from '../../../utils/plist';
interface Target {
buildConfiguration?: string;
targetName: string;
}
export async function getManagedApplicationTargetEntitlementsAsync(
export async function getManagedEntitlementsJsonAsync(
projectDir: string,
env: Record<string, string>
): Promise<JSONObject> {
@@ -31,11 +28,22 @@ export async function getManagedApplicationTargetEntitlementsAsync(
}
}
export async function getNativeTargetEntitlementsAsync(
export async function resolveEntitlementsJsonAsync(
projectDir: string,
target: Target
): Promise<JSONObject | null> {
const entitlementsPath = IOSConfig.Entitlements.getEntitlementsPath(projectDir, target);
workflow: Workflow,
env: Record<string, string>
): Promise<JSONObject> {
if (workflow === Workflow.GENERIC) {
return (await getEntitlementsJsonAsync(projectDir)) || {};
} else if (workflow === Workflow.MANAGED) {
return await getManagedEntitlementsJsonAsync(projectDir, env);
} else {
throw new Error(`Unknown workflow: ${workflow}`);
}
}
async function getEntitlementsJsonAsync(projectDir: string): Promise<JSONObject | null> {
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
if (entitlementsPath) {
const plist = await readPlistAsync(entitlementsPath);
return plist ? (plist as unknown as JSONObject) : null;
@@ -1,15 +1,13 @@
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 { getRequestContext } from './authenticate';
import { AuthCtx } from './authenticateTypes';
import { AuthCtx, getRequestContext } from './authenticate';
import { getBundleIdForIdentifierAsync, getProfilesForBundleIdAsync } from './bundleId';
import { getCertificateBySerialNumberAsync, transformCertificate } from './distributionCertificate';
@@ -72,11 +70,6 @@ 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();
}
@@ -1,10 +1,8 @@
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 { getRequestContext } from './authenticate';
import { AuthCtx } from './authenticateTypes';
import { AuthCtx, getRequestContext } from './authenticate';
import { getBundleIdForIdentifierAsync, getProfilesForBundleIdAsync } from './bundleId';
import { getDistributionCertificateAsync } from './distributionCertificate';
@@ -91,13 +89,9 @@ async function findProfileByBundleIdAsync(
}
const profile = expoProfiles.sort(sortByExpiration)[expoProfiles.length - 1];
profile.attributes.certificates = [distributionCertificate];
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(),
// This method does not support App Store Connect API.
profile: await profile.regenerateAsync(),
didUpdate: true,
};
}
@@ -184,13 +178,8 @@ async function manageAdHocProfilesAsync(
// We need to add new devices to the list and create a new provisioning profile.
existingProfile.attributes.devices = devices;
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();
}
// This method does not support App Store Connect API.
await existingProfile.regenerateAsync();
const updatedProfile = (await findProfileByBundleIdAsync(context, bundleId, certSerialNumber))
.profile;
@@ -5,8 +5,7 @@ import dateformat from 'dateformat';
import Log from '../../../log';
import { ora } from '../../../ora';
import { PushKey, PushKeyStoreInfo } from './Credentials.types';
import { getRequestContext } from './authenticate';
import { UserAuthCtx } from './authenticateTypes';
import { AuthCtx, getRequestContext } from './authenticate';
const { MaxKeysCreatedError } = Keys;
@@ -20,10 +19,10 @@ Please remember that Apple Keys are not application specific!
* List all existing push keys on Apple servers.
* **Does not support App Store Connect API (CI).**
*/
export async function listPushKeysAsync(userAuthCtx: UserAuthCtx): Promise<PushKeyStoreInfo[]> {
export async function listPushKeysAsync(authCtx: AuthCtx): Promise<PushKeyStoreInfo[]> {
const spinner = ora(`Fetching Apple push keys`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
const keys = await Keys.getKeysAsync(context);
spinner.succeed(`Fetched Apple push keys`);
return keys;
@@ -38,20 +37,20 @@ export async function listPushKeysAsync(userAuthCtx: UserAuthCtx): Promise<PushK
* **Does not support App Store Connect API (CI).**
*/
export async function createPushKeyAsync(
userAuthCtx: UserAuthCtx,
authCtx: AuthCtx,
name: string = `Expo Push Notifications Key ${dateformat('yyyymmddHHMMss')}`
): Promise<PushKey> {
const spinner = ora(`Creating Apple push key`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
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: userAuthCtx.team.id,
teamName: userAuthCtx.team.name,
teamId: authCtx.team.id,
teamName: authCtx.team.name,
};
} catch (err: any) {
spinner.fail('Failed to create Apple push key');
@@ -70,12 +69,12 @@ export async function createPushKeyAsync(
* 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> {
export async function revokePushKeyAsync(authCtx: AuthCtx, ids: string[]): Promise<void> {
const name = `Apple push key${ids?.length === 1 ? '' : 's'}`;
const spinner = ora(`Revoking ${name}`).start();
try {
const context = getRequestContext(userAuthCtx);
const context = getRequestContext(authCtx);
await Promise.all(ids.map(id => Keys.revokeKeyAsync(context, { id })));
spinner.succeed(`Revoked ${name}`);
@@ -5,8 +5,6 @@ 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';
/**
@@ -15,7 +13,7 @@ import * as Keychain from './keychain';
*
* @param options
*/
export async function resolveUserCredentialsAsync(
export async function resolveCredentialsAsync(
options: Partial<Auth.UserCredentials>
): Promise<Partial<Auth.UserCredentials>> {
const credentials = getAppleIdFromEnvironmentOrOptions(options);
@@ -27,162 +25,6 @@ export async function resolveUserCredentialsAsync(
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,
@@ -1,5 +1,3 @@
import { JSONObject } from '@expo/json-file';
import {
CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment,
@@ -16,7 +14,6 @@ export interface Target {
buildConfiguration?: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
entitlements: JSONObject;
}
export interface TargetCredentials {
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`print credentials prints the IosAppCredentials map 1`] = `
"iOS Credentials
Array [
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
@@ -50,5 +51,6 @@ Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
"
",
]
`;
@@ -15,7 +15,7 @@ describe(getCertData, () => {
});
});
describe('forge.pkcs12.pkcs12FromAsn1', () => {
describe(forge.pkcs12.pkcs12FromAsn1, () => {
describe('when using p12 created from keychain', () => {
it('throws if empty string is passed', async () => {
const p12Der = forge.util.decode64(testDistributionCertificateEmptyPasswordBase64);
@@ -1,6 +1,7 @@
import mockdate from 'mockdate';
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
import { App, Target } from '../../types';
@@ -28,13 +29,9 @@ describe('print credentials', () => {
const appCredentials = {
test52: nullthrows(testIosAppCredentialsData),
};
const targets: Target[] = [
{ targetName: 'test52', bundleIdentifier: 'com.quinlanj.test52', entitlements: {} },
];
const targets: Target[] = [{ targetName: 'test52', bundleIdentifier: 'com.quinlanj.test52' }];
displayIosCredentials(app, appCredentials, targets);
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue, '');
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -1,6 +0,0 @@
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;
}
@@ -123,7 +123,7 @@ export function displayIosCredentials(
export function displayProjectCredentials(
app: App,
appBuildCredentials: IosAppBuildCredentialsMap,
targets: Omit<Target, 'entitlements'>[]
targets: Target[]
): void {
const projectFullName = `@${app.account.name}/${app.projectName}`;
const targetToBundleId = targets.reduce<Record<string, string>>((acc, target) => {
@@ -31,7 +31,7 @@ export function parse(dataBase64: string): PlistObject {
const buffer = Buffer.from(dataBase64, 'base64');
const profile = buffer.toString('utf8');
return plist.parse(profile) as PlistObject;
} catch {
} catch (error) {
throw new Error('Provisioning profile is malformed');
}
}
@@ -75,7 +75,7 @@ function validateProvisioningProfileWithoutApple(
Log.warn('Provisioning Profile has expired.');
return false;
}
} catch {
} catch (error) {
Log.warn('Provisioning Profile is malformed.');
return false;
}
@@ -43,6 +43,7 @@ import { SetUpProvisioningProfile } from '../ios/actions/SetUpProvisioningProfil
import { SetUpPushKey } from '../ios/actions/SetUpPushKey';
import { UpdateCredentialsJson } from '../ios/actions/UpdateCredentialsJson';
import { AppLookupParams } from '../ios/api/GraphqlClient';
import { getManagedEntitlementsJsonAsync } from '../ios/appstore/entitlements';
import { App, IosAppCredentialsMap, Target } from '../ios/types';
import { displayIosCredentials } from '../ios/utils/printCredentials';
import { ActionInfo, IosActionType, Scope } from './Actions';
@@ -191,12 +192,10 @@ export class ManageIos {
},
buildProfile
);
const targets = await resolveTargetsAsync({
exp: ctx.exp,
projectDir: ctx.projectDir,
xcodeBuildContext,
env: buildProfile.env,
});
const targets = await resolveTargetsAsync(
{ exp: ctx.exp, projectDir: ctx.projectDir },
xcodeBuildContext
);
return {
app,
targets,
@@ -236,6 +235,12 @@ export class ManageIos {
targets,
distribution: buildProfile.distribution,
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
iosCapabilitiesOptions: {
entitlements: await getManagedEntitlementsJsonAsync(
ctx.projectDir,
buildProfile.env ?? {}
),
},
}).runAsync(ctx);
return;
}
@@ -1,3 +1,4 @@
import { asMock } from '../../../__tests__/utils';
import { promptAsync } from '../../../prompts';
import {
getNewAndroidApiMock,
@@ -14,12 +15,12 @@ const TEST_STRING = 'TEST_STRING';
jest.mock('../../../prompts');
beforeEach(() => {
jest.mocked(promptAsync).mockReset();
asMock(promptAsync).mockReset();
});
describe(SelectAndroidBuildCredentials, () => {
it('returns a request to make default build credentials when there are no credentials', async () => {
jest.mocked(promptAsync).mockImplementation(async () => ({
asMock(promptAsync).mockImplementation(() => ({
providedName: TEST_STRING,
}));
const ctx = createCtxMock({
@@ -37,7 +38,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make build credentials when the user chooses to make a new one', async () => {
jest.mocked(promptAsync).mockImplementation(async () => ({
asMock(promptAsync).mockImplementation(() => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -59,7 +60,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make default build credentials when the user chooses to make a new one, and if they have no existing credentials', async () => {
jest.mocked(promptAsync).mockImplementation(async () => ({
asMock(promptAsync).mockImplementation(() => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -79,7 +80,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns buildCredentials of the users choice', async () => {
jest.mocked(promptAsync).mockImplementation(async () => ({
asMock(promptAsync).mockImplementation(() => ({
buildCredentialsResultOrRequestToCreateNew: testAndroidBuildCredentialsFragment,
}));
const ctx = createCtxMock({
@@ -1,5 +1,6 @@
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import Log from '../../log';
import { Actor } from '../../user/User';
import { AccountResolver } from '../manager';
@@ -14,8 +15,8 @@ jest.mock('../../project/projectUtils', () => {
jest.mock('../../credentials/ios/api/graphql/queries/AppleTeamQuery');
beforeEach(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -37,7 +38,7 @@ describe(AccountResolver, () => {
const exp = {} as any;
it('returns the account defined in app.json/app.config.js if user confirms', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
value: true,
}));
@@ -47,10 +48,10 @@ describe(AccountResolver, () => {
});
it('asks the user to choose the account from his account list if he rejects to use the one defined in app.json / app.config.js', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
useProjectAccount: false,
}));
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
account: user.accounts[0],
}));
@@ -60,7 +61,7 @@ describe(AccountResolver, () => {
});
it(`asks the user to choose the account from his account list if he doesn't have access to the account defined in app.json / app.config.js`, async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
account: user.accounts[0],
}));
@@ -79,7 +80,7 @@ describe(AccountResolver, () => {
describe('when outside project dir', () => {
it('asks the user to choose the account from his account list', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
account: user.accounts[0],
}));
@@ -1,5 +1,6 @@
import prompts from 'prompts';
import { asMock } from '../../../../__tests__/utils';
import { Account } from '../../../../user/Account';
import DeviceCreateAction, { RegistrationMethod } from '../action';
import { runInputMethodAsync } from '../inputMethod';
@@ -10,19 +11,19 @@ jest.mock('../registrationUrlMethod');
jest.mock('../inputMethod');
beforeEach(() => {
const promptsMock = jest.mocked(prompts);
const promptsMock = asMock(prompts);
promptsMock.mockReset();
promptsMock.mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
jest.mocked(runRegistrationUrlMethodAsync).mockClear();
jest.mocked(runInputMethodAsync).mockClear();
asMock(runRegistrationUrlMethodAsync).mockClear();
asMock(runInputMethodAsync).mockClear();
});
describe(DeviceCreateAction, () => {
describe('#runAsync', () => {
it('calls runRegistrationUrlMethodAsync if user chooses the website method', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
method: RegistrationMethod.WEBSITE,
}));
@@ -42,7 +43,7 @@ describe(DeviceCreateAction, () => {
});
it('calls runInputMethodAsync if user chooses the input method', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
asMock(prompts).mockImplementationOnce(() => ({
method: RegistrationMethod.INPUT,
}));
@@ -1,5 +1,6 @@
import prompts from 'prompts';
import { asMock } from '../../../../__tests__/utils';
import { AppleDeviceMutation } from '../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import { AppleDeviceClass, AppleTeam } from '../../../../graphql/generated';
import { runInputMethodAsync } from '../inputMethod';
@@ -8,19 +9,19 @@ jest.mock('../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation
jest.mock('../../../../ora');
beforeEach(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
jest.mocked(AppleDeviceMutation.createAppleDeviceAsync).mockClear();
asMock(AppleDeviceMutation.createAppleDeviceAsync).mockClear();
});
describe(runInputMethodAsync, () => {
it('should allow for multiple device registration', async () => {
mockDeviceData('00001111-001122334455662E', 'my iPhone', AppleDeviceClass.Iphone);
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: true }));
asMock(prompts).mockImplementationOnce(() => ({ value: true }));
mockDeviceData('b12cba9856d89c932ab7a4b813c4d932534e1679', 'my iPad', AppleDeviceClass.Ipad);
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: false }));
asMock(prompts).mockImplementationOnce(() => ({ value: false }));
const accountId = 'account-id';
// @ts-expect-error appleTeam is missing properties of AppleTeam GraphQL type
@@ -37,8 +38,8 @@ describe(runInputMethodAsync, () => {
});
function mockDeviceData(udid: string, name: string, deviceClass: AppleDeviceClass): void {
jest.mocked(prompts).mockImplementationOnce(async () => ({ udid }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ name }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ deviceClass }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: true }));
asMock(prompts).mockImplementationOnce(() => ({ udid }));
asMock(prompts).mockImplementationOnce(() => ({ name }));
asMock(prompts).mockImplementationOnce(() => ({ deviceClass }));
asMock(prompts).mockImplementationOnce(() => ({ value: true }));
}
+1 -1
View File
@@ -97,7 +97,7 @@ export class AccountResolver {
async function ensureAppleTeamExistsAsync(
accountId: string,
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName?: string }
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName: string }
): Promise<AppleTeamFragment> {
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
accountId,
+23 -139
View File
@@ -94,9 +94,8 @@ export type Account = {
apps: Array<App>;
/** @deprecated Build packs are no longer supported */
availableBuilds?: Maybe<Scalars['Int']>;
/** Billing information. Only visible to members with the ADMIN or OWNER role. */
/** Billing information */
billing?: Maybe<Billing>;
billingPeriod: BillingPeriod;
/** Build Jobs associated with this account */
buildJobs: Array<BuildJob>;
/**
@@ -130,8 +129,6 @@ export type Account = {
/** @deprecated See isCurrent */
unlimitedBuilds: Scalars['Boolean'];
updatedAt: Scalars['DateTime'];
/** Account query object for querying EAS usage metrics */
usageMetrics: AccountUsageMetrics;
/** Pending user invitations for this account */
userInvitations: Array<UserInvitation>;
/** Actors associated with this account and permissions they hold */
@@ -199,15 +196,6 @@ export type AccountAppsArgs = {
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountBillingPeriodArgs = {
date: Scalars['DateTime'];
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
@@ -338,9 +326,7 @@ export type AccountMutationRenameArgs = {
export type AccountMutationRequestRefundArgs = {
accountID: Scalars['ID'];
chargeID: Scalars['ID'];
description?: InputMaybe<Scalars['String']>;
reason?: InputMaybe<Scalars['String']>;
chargeIdentifier: Scalars['ID'];
};
@@ -393,45 +379,6 @@ export type AccountQueryByNameArgs = {
accountName: Scalars['String'];
};
export type AccountUsageMetric = {
__typename?: 'AccountUsageMetric';
id: Scalars['ID'];
metricType: UsageMetricType;
serviceMetric: EasServiceMetric;
timestamp: Scalars['DateTime'];
value: Scalars['Float'];
};
export type AccountUsageMetricAndCost = {
__typename?: 'AccountUsageMetricAndCost';
id: Scalars['ID'];
/** The limit, in units, allowed by this plan */
limit: Scalars['Float'];
metricType: UsageMetricType;
serviceMetric: EasServiceMetric;
/** Total cost of this particular metric, in cents */
totalCost: Scalars['Float'];
value: Scalars['Float'];
};
export type AccountUsageMetrics = {
__typename?: 'AccountUsageMetrics';
byBillingPeriod: UsageMetricTotal;
metricsForServiceMetric: Array<AccountUsageMetric>;
};
export type AccountUsageMetricsByBillingPeriodArgs = {
date: Scalars['DateTime'];
};
export type AccountUsageMetricsMetricsForServiceMetricArgs = {
granularity: UsageMetricsGranularity;
serviceMetric: EasServiceMetric;
timespan: UsageMetricsTimespan;
};
export type ActivityTimelineProjectActivity = {
activityTimestamp: Scalars['DateTime'];
actor?: Maybe<Actor>;
@@ -1023,20 +970,15 @@ export type AppDataInput = {
export type AppIcon = {
__typename?: 'AppIcon';
/** @deprecated No longer supported */
/** Nullable color palette of the app icon. If null, color palette couldn't be retrieved from external service (imgix) */
colorPalette?: Maybe<Scalars['JSON']>;
originalUrl: Scalars['String'];
primaryColor?: Maybe<Scalars['String']>;
url: Scalars['String'];
};
export type AppInfoInput = {
displayName?: InputMaybe<Scalars['String']>;
};
export type AppInput = {
accountId: Scalars['ID'];
appInfo?: InputMaybe<AppInfoInput>;
privacy: AppPrivacy;
projectName: Scalars['String'];
};
@@ -1047,8 +989,6 @@ export type AppMutation = {
createApp: App;
/** @deprecated No longer supported */
grantAccess?: Maybe<App>;
/** Set display info for app */
setAppInfo: App;
/** Require api token to send push notifs for experience */
setPushSecurityEnabled: App;
};
@@ -1065,12 +1005,6 @@ export type AppMutationGrantAccessArgs = {
};
export type AppMutationSetAppInfoArgs = {
appId: Scalars['ID'];
appInfo: AppInfoInput;
};
export type AppMutationSetPushSecurityEnabledArgs = {
appId: Scalars['ID'];
pushSecurityEnabled: Scalars['Boolean'];
@@ -1548,13 +1482,6 @@ export type Billing = {
subscription?: Maybe<SubscriptionDetails>;
};
export type BillingPeriod = {
__typename?: 'BillingPeriod';
anchor: Scalars['DateTime'];
end: Scalars['DateTime'];
start: Scalars['DateTime'];
};
/** Represents an EAS Build */
export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'Build';
@@ -1569,12 +1496,9 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
createdAt?: Maybe<Scalars['DateTime']>;
distribution?: Maybe<DistributionType>;
error?: Maybe<BuildError>;
estimatedWaitTimeLeftSeconds?: Maybe<Scalars['Int']>;
expirationDate?: Maybe<Scalars['DateTime']>;
gitCommitHash?: Maybe<Scalars['String']>;
id: Scalars['ID'];
/** Queue position is 1-indexed */
initialQueuePosition?: Maybe<Scalars['Int']>;
initiatingActor?: Maybe<Actor>;
/** @deprecated User type is deprecated */
initiatingUser?: Maybe<User>;
@@ -1939,13 +1863,6 @@ export type CodeSigningInfoInput = {
sig: Scalars['String'];
};
export type Concurrencies = {
__typename?: 'Concurrencies';
android: Scalars['Int'];
ios: Scalars['Int'];
total: Scalars['Int'];
};
export type CreateAccessTokenInput = {
actorID: Scalars['ID'];
note?: InputMaybe<Scalars['String']>;
@@ -1961,7 +1878,6 @@ export type CreateAccessTokenResponse = {
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
archiveUrl?: InputMaybe<Scalars['String']>;
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
@@ -1979,11 +1895,17 @@ export type CreateEnvironmentSecretInput = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
archiveUrl?: InputMaybe<Scalars['String']>;
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
export type CreateSubmissionInput = {
appId: Scalars['ID'];
config: Scalars['JSONObject'];
platform: AppPlatform;
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
export type CreateSubmissionResult = {
__typename?: 'CreateSubmissionResult';
/** Created submission */
@@ -2092,13 +2014,6 @@ export enum EasBuildDeprecationInfoType {
UserFacing = 'USER_FACING'
}
export enum EasServiceMetric {
AssetsRequests = 'ASSETS_REQUESTS',
BandwidthUsage = 'BANDWIDTH_USAGE',
ManifestRequests = 'MANIFEST_REQUESTS',
UniqueUsers = 'UNIQUE_USERS'
}
export type EditUpdateBranchInput = {
appId?: InputMaybe<Scalars['ID']>;
id?: InputMaybe<Scalars['ID']>;
@@ -3147,8 +3062,6 @@ export type Submission = ActivityTimelineProjectActivity & {
actor?: Maybe<Actor>;
androidConfig?: Maybe<AndroidSubmissionConfig>;
app: App;
archiveUrl?: Maybe<Scalars['String']>;
canRetry: Scalars['Boolean'];
cancelingActor?: Maybe<Actor>;
createdAt: Scalars['DateTime'];
error?: Maybe<SubmissionError>;
@@ -3156,7 +3069,6 @@ export type Submission = ActivityTimelineProjectActivity & {
initiatingActor?: Maybe<Actor>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
parentSubmission?: Maybe<Submission>;
platform: AppPlatform;
status: SubmissionStatus;
submittedBuild?: Maybe<Build>;
@@ -3201,13 +3113,16 @@ export type SubmissionMutation = {
createAndroidSubmission: CreateSubmissionResult;
/** Create an iOS EAS Submit submission */
createIosSubmission: CreateSubmissionResult;
/** Retry an EAS Submit submission */
retrySubmission: CreateSubmissionResult;
/**
* Create an EAS Submit submission
* @deprecated Use createIosSubmission / createAndroidSubmission instead
*/
createSubmission: CreateSubmissionResult;
};
export type SubmissionMutationCancelSubmissionArgs = {
submissionId: Scalars['ID'];
submissionId?: InputMaybe<Scalars['ID']>;
};
@@ -3221,8 +3136,8 @@ export type SubmissionMutationCreateIosSubmissionArgs = {
};
export type SubmissionMutationRetrySubmissionArgs = {
parentSubmissionId: Scalars['ID'];
export type SubmissionMutationCreateSubmissionArgs = {
input: CreateSubmissionInput;
};
export type SubmissionQuery = {
@@ -3249,7 +3164,6 @@ export type SubscriptionDetails = {
__typename?: 'SubscriptionDetails';
addons: Array<AddonDetails>;
cancelledAt?: Maybe<Scalars['DateTime']>;
concurrencies?: Maybe<Concurrencies>;
endedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['ID'];
isDowngrading?: Maybe<Scalars['Boolean']>;
@@ -3435,34 +3349,6 @@ export enum UploadSessionType {
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
}
export type UsageMetricTotal = {
__typename?: 'UsageMetricTotal';
billingPeriod: BillingPeriod;
id: Scalars['ID'];
overageMetrics: Array<AccountUsageMetricAndCost>;
planMetrics: Array<AccountUsageMetricAndCost>;
/** Total cost of overages, in cents */
totalCost: Scalars['Float'];
};
export enum UsageMetricType {
Bandwidth = 'BANDWIDTH',
Request = 'REQUEST',
User = 'USER'
}
export enum UsageMetricsGranularity {
Day = 'DAY',
Hour = 'HOUR',
Minute = 'MINUTE',
Total = 'TOTAL'
}
export type UsageMetricsTimespan = {
end: Scalars['DateTime'];
start: Scalars['DateTime'];
};
/** Represents a human (not robot) actor. */
export type User = Actor & {
__typename?: 'User';
@@ -3584,7 +3470,6 @@ export type UserInvitation = {
created: Scalars['DateTime'];
/** Email to which this invitation was sent */
email: Scalars['String'];
expires: Scalars['DateTime'];
id: Scalars['ID'];
/** Account permissions to be granted upon acceptance of this invitation */
permissions: Array<Permission>;
@@ -3656,7 +3541,6 @@ export type UserInvitationPublicData = {
accountName: Scalars['String'];
created: Scalars['DateTime'];
email: Scalars['String'];
expires: Scalars['DateTime'];
/** Email to which this invitation was sent */
id: Scalars['ID'];
};
@@ -4309,7 +4193,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
@@ -4318,7 +4202,7 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -4431,7 +4315,7 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type GetAllBuildsForAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -4441,7 +4325,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
}>;
export type GetAllBuildsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type GetAllBuildsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type GetChannelByNameForAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -4537,7 +4421,7 @@ export type WebhookByIdQuery = { __typename?: 'RootQuery', webhook: { __typename
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, createdAt?: any | null, updatedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, createdAt: any };
@@ -41,10 +41,6 @@ export const BuildFragmentNode = gql`
appBuildVersion
runtimeVersion
gitCommitHash
initialQueuePosition
queuePosition
estimatedWaitTimeLeftSeconds
priority
createdAt
updatedAt
}
@@ -1,5 +1,6 @@
import { vol } from 'memfs';
import { asMock } from '../../__tests__/utils';
import { confirmAsync } from '../../prompts';
import { Actor, getUserAsync } from '../../user/User';
import {
@@ -122,58 +123,52 @@ describe(getProjectAccountNameAsync, () => {
const expWithoutOwner: any = {};
beforeEach(() => {
jest.mocked(getUserAsync).mockReset();
asMock(getUserAsync).mockReset();
});
it(`returns the owner field's value from app.json / app.config.js`, async () => {
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
})
);
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
}));
const projectAccountName = await getProjectAccountNameAsync(expWithOwner);
expect(projectAccountName).toBe('dominik');
});
it(`returns the username if owner field is not set in app.json / app.config.js`, async () => {
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
})
);
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
}));
const projectAccountName = await getProjectAccountNameAsync(expWithoutOwner);
expect(projectAccountName).toBe('notnotbrent');
});
it(`throws when project owner is undefined for robot actors`, async () => {
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'Robot',
id: 'user_id',
firstName: 'GLaDOS',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
})
);
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'Robot',
id: 'user_id',
firstName: 'GLaDOS',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
}));
await expect(getProjectAccountNameAsync(expWithoutOwner)).rejects.toThrow(
'manifest property is required'
);
@@ -182,67 +177,57 @@ describe(getProjectAccountNameAsync, () => {
describe(promptToCreateProjectIfNotExistsAsync, () => {
beforeEach(() => {
jest.mocked(getUserAsync).mockReset();
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
asMock(getUserAsync).mockReset();
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
});
it('returns the existing project', async () => {
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
})
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(
() => 'already-existing-project-id'
);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockImplementation(async () => 'already-existing-project-id');
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('already-existing-project-id');
});
it('makes a new project if none exists', async () => {
jest.mock('../../prompts');
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
})
);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockImplementation(async () => null);
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => 'new-project-id');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user agrees to make new project
jest.mocked(confirmAsync).mockImplementation(async () => true);
asMock(confirmAsync).mockImplementation(() => true);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('new-project-id');
});
it('aborts making a new project if the user declines', async () => {
jest.mock('../../prompts');
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
})
);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockImplementation(async () => null);
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => 'new-project-id');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
// user doesnt agree to make new project
jest.mocked(confirmAsync).mockImplementation(async () => false);
asMock(confirmAsync).mockImplementation(() => false);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe(null);
@@ -412,9 +412,9 @@ describe(uploadAssetsAsync, () => {
return { specifications: ['{}', '{}', '{}'] };
});
// beforeEach(() => {
// jest.useFakeTimers();
// });
beforeEach(() => {
jest.useFakeTimers();
});
it('throws an error if the upload exceeds TIMEOUT_LIMIT', async () => {
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
const status = AssetMetadataStatus.DoesNotExist;
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
@@ -20,7 +21,7 @@ beforeEach(async () => {
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
jest.mocked(promptAsync).mockReset();
asMock(promptAsync).mockReset();
});
describe(getApplicationIdAsync, () => {
@@ -121,7 +122,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
'/app'
);
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
packageName: 'com.expo.notdominik',
}));
@@ -138,7 +139,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
'/app'
);
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
asMock(promptAsync).mockImplementationOnce(() => ({
packageName: 'com.expo.notdominik',
}));
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import { resolveGradleBuildContextAsync } from '../gradle';
@@ -16,7 +17,7 @@ beforeEach(async () => {
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
jest.mocked(promptAsync).mockReset();
asMock(promptAsync).mockReset();
});
describe(resolveGradleBuildContextAsync, () => {
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more