mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c94f80f68 | ||
|
|
95f308737e | ||
|
|
2a2a0392cd | ||
|
|
bcbdae8278 | ||
|
|
805aee67ee | ||
|
|
0477984d7a | ||
|
|
4e96196e07 | ||
|
|
25ee2abdaa | ||
|
|
75b71ae457 | ||
|
|
7548fc35fd |
No files matched your search
@@ -0,0 +1,47 @@
|
||||
name: "\U0001F41B Bug Report in EAS CLI"
|
||||
labels: 'bug'
|
||||
description: 'You want to report a reproducible bug or regression in EAS CLI.'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the issue in 1 or 2 sentences
|
||||
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
placeholder: Run `eas diagnostics` and paste the output here
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: Error output
|
||||
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Reproducible demo or steps to reproduce from a blank project
|
||||
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
|
||||
@@ -0,0 +1,14 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Do you understand that any feature requests opened in this repository will be closed?
|
||||
options:
|
||||
- 'Yes'
|
||||
+19
-2
@@ -6,6 +6,23 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require `eas submit` to be configured with eas.json submit profiles. Drop support for CLI params. ([#590](https://github.com/expo/eas-cli/pull/590) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas diagnostics` command. ([#594](https://github.com/expo/eas-cli/pull/594) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas build` from throwing an exception when detecting the git `core.ignorecase` setting. ([#592](https://github.com/expo/eas-cli/pull/592) by [@mwillbanks](https://github.com/mwillbanks))
|
||||
- Support app names that consist only of unicode characters. ([#596](https://github.com/expo/eas-cli/pull/596) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.25.0](https://github.com/expo/eas-cli/releases/tag/v0.25.0) - 2021-09-01
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for node < 12. ([#581](https://github.com/expo/eas-cli/pull/581) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
@@ -15,7 +32,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix wrong warning for `metro.config.js` check on Windows ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
@@ -46,7 +63,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Add `changesNotSentForReview` option to android submissions. ([#560](https://github.com/expo/eas-cli/pull/560) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support `--json` flag in build commands. ([#567](https://github.com/expo/eas-cli/pull/567) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add link to https://expo.fyi/eas-build-archive in `eas build` output to make it easier to understand what is going on in the archive/upload phase of the build. ([#562](https://github.com/expo/eas-cli/pull/562) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Update QR code url for Android internal distribution build and generate smaller QR code. ([#573](https://github.com/expo/eas-cli/pull/573) by [@axeldelafosse](https://github.com/axeldelafosse))
|
||||
- Update QR code URL for Android internal distribution build and generate smaller QR code. ([#573](https://github.com/expo/eas-cli/pull/573) by [@axeldelafosse](https://github.com/axeldelafosse))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Community Code of Conduct
|
||||
|
||||
The Expo team is committed to fostering an open and welcoming environment.
|
||||
|
||||
**Our Community Guidelines can be found here:**
|
||||
|
||||
https://expo.dev/guidelines
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.25.0"
|
||||
"version": "0.26.0"
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.15.0",
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/node": "16.7.8",
|
||||
|
||||
+55
-105
@@ -47,13 +47,14 @@ eas --help COMMAND
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas diagnostics`](#eas-diagnostics)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
@@ -72,7 +73,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -86,7 +87,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -100,22 +101,22 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
View or change analytics settings
|
||||
view or change analytics settings
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
Start a build
|
||||
start a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -132,7 +133,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -143,7 +144,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -157,7 +158,7 @@ OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -190,7 +191,7 @@ OPTIONS
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -204,11 +205,11 @@ OPTIONS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
Show the eas.json config
|
||||
show the eas.json config
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -219,18 +220,18 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
Manage your credentials
|
||||
manage your credentials
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -241,7 +242,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -255,7 +256,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -266,7 +267,18 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
log environment info to the console
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -294,7 +306,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -308,7 +320,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -325,7 +337,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -342,7 +354,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -353,96 +365,34 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
## `eas submit`
|
||||
|
||||
Submits build artifact to app store
|
||||
submit build archive to app store
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas submit --platform=(android|ios)
|
||||
$ eas submit
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios) For which platform you want to submit a build
|
||||
-p, --platform=(android|ios)
|
||||
--id=id ID of the build to submit
|
||||
--latest Submit the latest build for specified platform
|
||||
--path=path Path to the .apk/.aab/.ipa file
|
||||
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.
|
||||
--url=url App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
Android specific options=android-package Android package name (default: expo.android.package
|
||||
from app config)
|
||||
|
||||
iOS specific options=app-name The name of your app as it will appear on the App
|
||||
Store (default: expo.name from app config)
|
||||
|
||||
iOS specific options=apple-id Your Apple ID username (you can also set
|
||||
EXPO_APPLE_ID env variable)
|
||||
|
||||
iOS specific options=apple-team-id Your Apple Developer Team ID
|
||||
|
||||
iOS specific options=asc-app-id App Store Connect unique application Apple ID
|
||||
number. Providing this param results in skipping
|
||||
app creation step. Learn more:
|
||||
https://expo.fyi/asc-app-id
|
||||
|
||||
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
|
||||
expo.ios.bundleIdentifier from app config)
|
||||
|
||||
Android specific options [default: false] Indicates that the changes sent
|
||||
with this submission will not be reviewed until
|
||||
they are explicitly sent for review from the Google
|
||||
Play Console UI
|
||||
|
||||
iOS specific options=company-name The name of your company, needed only for the first
|
||||
upload of any app to App Store
|
||||
|
||||
--id=id ID of the build to submit
|
||||
|
||||
Android specific options=key Path to the JSON file with service account key used
|
||||
to authenticate with Google Play
|
||||
|
||||
iOS specific options=language [default: en-US] Primary language (e.g. English,
|
||||
German, ...)
|
||||
|
||||
--latest Submit the latest build for specified platform
|
||||
|
||||
--path=path Path to the .apk/.aab file
|
||||
|
||||
--profile=profile Name of the submit profile from eas.json
|
||||
|
||||
Android specific options=(completed|draft|halted|inProgress) [default: completed] Release status (used when
|
||||
uploading new APKs/AABs)
|
||||
|
||||
iOS specific options=sku An unique ID for your app that is not visible on
|
||||
the App Store, will be generated unless provided
|
||||
|
||||
Android specific options=(production|beta|alpha|internal|rollout) [default: internal] The track of the application to
|
||||
use
|
||||
|
||||
Android specific options=(apk|aab) Android archive type
|
||||
|
||||
--url=url App archive url
|
||||
|
||||
--verbose Always print logs from Submission Service
|
||||
|
||||
--[no-]wait Wait for submission to complete
|
||||
DESCRIPTION
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
|
||||
EXAMPLES
|
||||
$ eas submit --platform=ios
|
||||
- Fully interactive iOS submission
|
||||
|
||||
$ eas submit --platform=android
|
||||
- Fully interactive Android submission
|
||||
|
||||
$ eas submit -p android --latest --key=/path/to/google-services.json
|
||||
- Minimal non-interactive Android submission, however it can ask you for other params if not specified
|
||||
|
||||
$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
|
||||
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -461,7 +411,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -475,7 +425,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -489,7 +439,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -509,7 +459,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -523,5 +473,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -12,9 +12,9 @@
|
||||
"@amplitude/node": "1.7.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.25",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "3.0.3",
|
||||
"@expo/config-plugins": "3.1.0",
|
||||
"@expo/eas-build-job": "0.2.46",
|
||||
"@expo/eas-json": "^0.25.0",
|
||||
"@expo/eas-json": "^0.26.0",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
@@ -33,6 +33,7 @@
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "4.5.1",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-glob": "3.2.7",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "3.0.0",
|
||||
@@ -76,6 +77,7 @@
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"@types/getenv": "^1.0.0",
|
||||
@@ -89,7 +91,7 @@
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.21.1",
|
||||
"axios": "0.21.4",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"memfs": "3.2.2",
|
||||
"mockdate": "^3.0.2",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
@@ -19,7 +19,7 @@ import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { MetadataContext, collectMetadata } from './metadata';
|
||||
import { Platform, TrackingContext } from './types';
|
||||
import { TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { commitPromptAsync, makeProjectTarballAsync } from './utils/repository';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { EasJson, EasJsonReader } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
@@ -95,25 +95,35 @@ export async function configureAsync(options: {
|
||||
}
|
||||
}
|
||||
|
||||
const MANAGED_DEFAULTS = {
|
||||
release: {},
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
build: {
|
||||
release: {},
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
release: {},
|
||||
},
|
||||
};
|
||||
|
||||
const GENERIC_DEFAULTS = {
|
||||
release: {},
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
|
||||
build: {
|
||||
release: {},
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
},
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
release: {},
|
||||
},
|
||||
};
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
@@ -129,8 +139,8 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
|
||||
|
||||
const easJson =
|
||||
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
|
||||
? { build: GENERIC_DEFAULTS }
|
||||
: { build: MANAGED_DEFAULTS };
|
||||
? EAS_JSON_GENERIC_DEFAULT
|
||||
: EAS_JSON_MANAGED_DEFAULT;
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await vcs.trackFileAsync(easJsonPath);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -11,7 +11,7 @@ import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { Platform, RequestedPlatform, TrackingContext } from './types';
|
||||
import { RequestedPlatform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
|
||||
export interface ConfigureContext {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
@@ -10,7 +10,6 @@ import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { IosMetadataContext } from '../metadata';
|
||||
import { Platform } from '../types';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
@@ -5,7 +7,6 @@ import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/ent
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArchiveSource, Ios, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import vcs from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from './ios/UpdatesModule';
|
||||
import { readBuildNumberAsync, readShortVersionAsync } from './ios/version';
|
||||
import { Platform } from './types';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
Ios = 'ios',
|
||||
|
||||
@@ -18,6 +18,8 @@ export enum Event {
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import vcs from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { Platform } from './types';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Log from '../log';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
|
||||
export default class AnalyticsView extends Command {
|
||||
static description = 'View or change analytics settings';
|
||||
static description = 'view or change analytics settings';
|
||||
|
||||
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
hasMismatchedExtendsAsync,
|
||||
@@ -13,7 +13,7 @@ import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { Platform, RequestedPlatform } from '../../build/types';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -53,7 +53,7 @@ interface BuildFlags {
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'Start a build';
|
||||
static description = 'start a build';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
|
||||
@@ -117,7 +117,7 @@ export default class Build extends EasCommand {
|
||||
const ctx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
|
||||
buildProfile: await easJsonReader.readBuildProfileAsync(platform, flags.profile),
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
@@ -157,10 +157,10 @@ export default class Build extends EasCommand {
|
||||
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('--platform is required when building in non-interactive mode');
|
||||
error('--platform is required when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
if (flags.json && !nonInteractive) {
|
||||
throw new Error('--json is allowed only when building in non-interactive mode');
|
||||
error('--json is allowed only when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
const requestedPlatform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
|
||||
|
||||
@@ -8,10 +8,9 @@ import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { selectAsync } from '../prompts';
|
||||
import { handleDeprecatedEasJsonAsync } from './build';
|
||||
export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export default class Config extends Command {
|
||||
static description = 'Show the eas.json config';
|
||||
static description = 'show the eas.json config';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
@@ -53,7 +52,7 @@ export default class Config extends Command {
|
||||
},
|
||||
]));
|
||||
|
||||
const profile = await reader.readBuildProfileAsync(profileName, platform);
|
||||
const profile = await reader.readBuildProfileAsync(platform, profileName);
|
||||
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
|
||||
|
||||
Log.log(getProjectConfigDescription(projectDir));
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { createCredentialsContextAsync } from '../credentials/context';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
|
||||
export default class Credentials extends Command {
|
||||
static description = 'Manage your credentials';
|
||||
export default class Credentials extends EasCommand {
|
||||
static description = 'manage your credentials';
|
||||
|
||||
async run() {
|
||||
const ctx = await createCredentialsContextAsync(process.cwd(), {});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Command } from '@oclif/command';
|
||||
import envinfo from 'envinfo';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
export default class Diagnostics extends Command {
|
||||
static description = 'log environment info to the console';
|
||||
|
||||
async run() {
|
||||
const info = await envinfo.run(
|
||||
{
|
||||
System: ['OS', 'Shell'],
|
||||
Binaries: ['Node', 'Yarn', 'npm'],
|
||||
Utilities: ['Git'],
|
||||
npmPackages: [
|
||||
'expo',
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-native',
|
||||
'react-native-web',
|
||||
'react-navigation',
|
||||
'@expo/webpack-config',
|
||||
],
|
||||
npmGlobalPackages: ['eas-cli'],
|
||||
},
|
||||
{
|
||||
title: `EAS CLI ${packageJSON.version} environment info`,
|
||||
}
|
||||
);
|
||||
Log.log(info);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { EasJsonReader, IosSubmitProfile } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { exit } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
@@ -14,156 +14,60 @@ import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils
|
||||
import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
|
||||
import { SubmitArchiveFlags } from '../submissions/types';
|
||||
import { displayLogsAsync } from '../submissions/utils/logs';
|
||||
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
|
||||
import { waitForSubmissionsEndAsync } from '../submissions/utils/wait';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
const ANDROID_FLAGS = 'Android specific options';
|
||||
const IOS_FLAGS = 'iOS specific options';
|
||||
|
||||
interface Flags {
|
||||
verbose: boolean;
|
||||
wait: boolean;
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
iosOptions: Partial<IosSubmitCommandFlags>;
|
||||
androidOptions: Partial<AndroidSubmitCommandFlags>;
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
}
|
||||
|
||||
export default class BuildSubmit extends EasCommand {
|
||||
static description = 'Submits build artifact to app store';
|
||||
static usage = 'submit --platform=(android|ios)';
|
||||
static description = `submit build archive to app store
|
||||
See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/submit/eas-json/', {
|
||||
learnMoreMessage: '',
|
||||
})}`;
|
||||
static aliases = ['build:submit'];
|
||||
|
||||
static examples = [
|
||||
`$ eas submit --platform=ios
|
||||
- Fully interactive iOS submission\n`,
|
||||
`$ eas submit --platform=android
|
||||
- Fully interactive Android submission\n`,
|
||||
`$ eas submit -p android --latest --key=/path/to/google-services.json
|
||||
- Minimal non-interactive Android submission, however it can ask you for other params if not specified\n`,
|
||||
`$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
|
||||
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
|
||||
and provide its App ID`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
description: 'For which platform you want to submit a build',
|
||||
options: ['android', 'ios'],
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
|
||||
/* Common flags for both platforms */
|
||||
profile: flags.string({
|
||||
description: 'Name of the submit profile from eas.json',
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
|
||||
}),
|
||||
latest: flags.boolean({
|
||||
description: 'Submit the latest build for specified platform',
|
||||
exclusive: ['id', 'path', 'url'],
|
||||
default: false,
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
id: flags.string({
|
||||
description: 'ID of the build to submit',
|
||||
exclusive: ['latest, path, url'],
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
path: flags.string({
|
||||
description: 'Path to the .apk/.aab file',
|
||||
description: 'Path to the .apk/.aab/.ipa file',
|
||||
exclusive: ['latest', 'id', 'url'],
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'App archive url',
|
||||
exclusive: ['latest', 'id', 'path'],
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
|
||||
verbose: flags.boolean({
|
||||
description: 'Always print logs from Submission Service',
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
|
||||
/* Android specific flags */
|
||||
type: flags.enum<'apk' | 'aab'>({
|
||||
description: 'Android archive type',
|
||||
options: ['apk', 'aab'],
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
|
||||
key: flags.string({
|
||||
description:
|
||||
'Path to the JSON file with service account key used to authenticate with Google Play',
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
'android-package': flags.string({
|
||||
description: 'Android package name (default: expo.android.package from app config)',
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
|
||||
track: flags.enum({
|
||||
description: '[default: internal] The track of the application to use',
|
||||
options: ['production', 'beta', 'alpha', 'internal', 'rollout'],
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
'changes-not-sent-for-review': flags.boolean({
|
||||
description:
|
||||
'[default: false] Indicates that the changes sent with this submission will not be reviewed until they are explicitly sent for review from the Google Play Console UI',
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
'release-status': flags.enum({
|
||||
description: '[default: completed] Release status (used when uploading new APKs/AABs)',
|
||||
options: ['completed', 'draft', 'halted', 'inProgress'],
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
|
||||
/* iOS specific flags */
|
||||
'apple-id': flags.string({
|
||||
description: 'Your Apple ID username (you can also set EXPO_APPLE_ID env variable)',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
'asc-app-id': flags.string({
|
||||
description: `App Store Connect unique application Apple ID number. Providing this param results in skipping app creation step. ${learnMore(
|
||||
'https://expo.fyi/asc-app-id'
|
||||
)}`,
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
'apple-team-id': flags.string({
|
||||
description: 'Your Apple Developer Team ID',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
'app-name': flags.string({
|
||||
description:
|
||||
'The name of your app as it will appear on the App Store (default: expo.name from app config)',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
'bundle-identifier': flags.string({
|
||||
description:
|
||||
'Your iOS Bundle Identifier (default: expo.ios.bundleIdentifier from app config)',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
sku: flags.string({
|
||||
description:
|
||||
'An unique ID for your app that is not visible on the App Store, will be generated unless provided',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
language: flags.string({
|
||||
description: '[default: en-US] Primary language (e.g. English, German, ...)',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
'company-name': flags.string({
|
||||
description:
|
||||
'The name of your company, needed only for the first upload of any app to App Store',
|
||||
helpLabel: IOS_FLAGS,
|
||||
default: false,
|
||||
}),
|
||||
wait: flags.boolean({
|
||||
description: 'Wait for submission to complete',
|
||||
default: true,
|
||||
allowNo: true,
|
||||
description: 'Wait for submission to complete',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -186,37 +90,27 @@ export default class BuildSubmit extends EasCommand {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
let iosSubmitProfile: IosSubmitProfile | null = null;
|
||||
if (platform === AppPlatform.Android) {
|
||||
const submitProfile = flags.profile
|
||||
? await easJsonReader.readSubmitProfileAsync(flags.profile, Platform.ANDROID)
|
||||
: {};
|
||||
const commandFlags: AndroidSubmitCommandFlags = {
|
||||
releaseStatus: 'completed',
|
||||
track: 'internal',
|
||||
changesNotSentForReview: false,
|
||||
...submitProfile,
|
||||
...flags.androidOptions,
|
||||
};
|
||||
const submitProfile = await easJsonReader.readSubmitProfileAsync(
|
||||
Platform.ANDROID,
|
||||
flags.profile
|
||||
);
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
profile: submitProfile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
submissions.push(await command.runAsync());
|
||||
} else {
|
||||
const submitProfile = flags.profile
|
||||
? await easJsonReader.readSubmitProfileAsync(flags.profile, Platform.IOS)
|
||||
: {};
|
||||
const commandFlags: IosSubmitCommandFlags = {
|
||||
language: 'en-US',
|
||||
...submitProfile,
|
||||
...flags.iosOptions,
|
||||
};
|
||||
iosSubmitProfile = await easJsonReader.readSubmitProfileAsync(Platform.IOS, flags.profile);
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
profile: iosSubmitProfile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
submissions.push(await command.runAsync());
|
||||
@@ -240,19 +134,19 @@ export default class BuildSubmit extends EasCommand {
|
||||
submission.platform === AppPlatform.Ios &&
|
||||
submission.status === SubmissionStatus.Finished
|
||||
) {
|
||||
const logMsg = [
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!'),
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.',
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.',
|
||||
// ascAppIdentifier should be always available for ios submissions but check it anyway
|
||||
submission.iosConfig?.ascAppIdentifier &&
|
||||
`- When it’s done, you can see your build here: ${learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/appstore/ios`,
|
||||
{ learnMoreMessage: '' }
|
||||
)}`,
|
||||
].join('\n');
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!\n') +
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.\n' +
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.\n' +
|
||||
'- When it’s done, you can see your build here' +
|
||||
learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${flags.iosOptions.ascAppId}/appstore/ios`,
|
||||
{
|
||||
learnMoreMessage: '',
|
||||
}
|
||||
)
|
||||
);
|
||||
Log.log(logMsg);
|
||||
}
|
||||
await displayLogsAsync(submission, { verbose: flags.verbose });
|
||||
if (completedSubmissions.length > 1) {
|
||||
@@ -265,54 +159,15 @@ export default class BuildSubmit extends EasCommand {
|
||||
|
||||
private parseFlags(): Flags {
|
||||
const {
|
||||
flags: {
|
||||
platform,
|
||||
profile,
|
||||
verbose,
|
||||
wait,
|
||||
|
||||
// android
|
||||
'android-package': androidPackage,
|
||||
'release-status': releaseStatus,
|
||||
'changes-not-sent-for-review': changesNotSentForReview,
|
||||
track,
|
||||
type,
|
||||
key: serviceAccountKeyPath,
|
||||
|
||||
// ios
|
||||
'apple-id': appleId,
|
||||
'asc-app-id': ascAppId,
|
||||
'apple-team-id': appleTeamId,
|
||||
'app-name': appName,
|
||||
'bundle-identifier': bundleIdentifier,
|
||||
'company-name': companyName,
|
||||
|
||||
// common
|
||||
...flags
|
||||
},
|
||||
flags: { platform, profile, verbose, wait, ...archiveFlags },
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
return {
|
||||
platform: platform as Platform,
|
||||
profile,
|
||||
androidOptions: {
|
||||
...(androidPackage && { androidPackage }),
|
||||
...(releaseStatus && { releaseStatus }),
|
||||
...(track && { track }),
|
||||
...(changesNotSentForReview && { changesNotSentForReview }),
|
||||
...(serviceAccountKeyPath && { serviceAccountKeyPath }),
|
||||
...flags,
|
||||
},
|
||||
iosOptions: {
|
||||
...(appleId && { appleId }),
|
||||
...(ascAppId && { ascAppId }),
|
||||
...(appleTeamId && { appleTeamId }),
|
||||
...(appName && { appName }),
|
||||
...(bundleIdentifier && { bundleIdentifier }),
|
||||
...(companyName && { companyName }),
|
||||
...flags,
|
||||
},
|
||||
verbose,
|
||||
wait,
|
||||
archiveFlags,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AndroidKeystoreFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
|
||||
@@ -14,19 +15,21 @@ export class CreateKeystore {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`New keystore cannot be created in non-interactive mode.`);
|
||||
}
|
||||
const keystore = await this.provideOrGenerateAsync();
|
||||
|
||||
const projectId = await getProjectIdAsync(ctx.exp);
|
||||
const keystore = await this.provideOrGenerateAsync(projectId);
|
||||
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
|
||||
Log.succeed('Created keystore');
|
||||
return keystoreFragment;
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(): Promise<KeystoreWithType> {
|
||||
private async provideOrGenerateAsync(projectId: string): Promise<KeystoreWithType> {
|
||||
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
|
||||
if (providedKeystore) {
|
||||
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
|
||||
validateKeystore(providedKeystoreWithType);
|
||||
return providedKeystoreWithType;
|
||||
}
|
||||
return await generateRandomKeystoreAsync();
|
||||
return await generateRandomKeystoreAsync(projectId);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
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';
|
||||
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from '../CreateKeystore';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
it('creates a keystore in Interactive Mode', async () => {
|
||||
@@ -21,7 +22,7 @@ describe('CreateKeystore', () => {
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('creates a keystore by uploading', async () => {
|
||||
(askForUserProvidedAsync as jest.Mock).mockImplementation(() => testKeystore);
|
||||
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
|
||||
|
||||
+4
-5
@@ -1,16 +1,17 @@
|
||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { jester as mockJester } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from '../SetupBuildCredentials';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
|
||||
describe('SetupBuildCredentials', () => {
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
@@ -31,7 +32,6 @@ describe('SetupBuildCredentials', () => {
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('sets up credentials when there are no prior credentials', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
android: {
|
||||
@@ -47,7 +47,6 @@ describe('SetupBuildCredentials', () => {
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
android: {
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export const keytoolCommandExistsAsync = jest.fn();
|
||||
export const exportCertificateAsync = jest.fn();
|
||||
export const logKeystoreHashesAsync = jest.fn();
|
||||
export const generateRandomKeystoreAsync = jest.fn();
|
||||
@@ -2,13 +2,13 @@ import spawnAsync from '@expo/spawn-async';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import tempy from 'tempy';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Analytics, { Event } from '../../../build/utils/analytics';
|
||||
import { AndroidKeystoreType } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { getTmpDirectory } from '../../../utils/paths';
|
||||
import { Keystore, KeystoreWithType } from '../credentials';
|
||||
import { KeystoreWithType } from '../credentials';
|
||||
|
||||
export async function keytoolCommandExistsAsync(): Promise<boolean> {
|
||||
try {
|
||||
@@ -28,63 +28,47 @@ async function ensureKeytoolCommandExistsAsync(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function writeFileToTmpAsync(base64Data: string, nameSuffix: string = ''): Promise<string> {
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const filePath = path.join(getTmpDirectory(), `${uuidv4()}${nameSuffix}`);
|
||||
await fs.writeFile(filePath, base64Data, 'base64');
|
||||
return filePath;
|
||||
enum KeystoreCreateStep {
|
||||
Attempt = 'attempt',
|
||||
Fail = 'fail',
|
||||
Success = 'success',
|
||||
}
|
||||
|
||||
export async function exportCertificateAsync(
|
||||
keystore: Keystore,
|
||||
options?: { rfcFormat: boolean }
|
||||
): Promise<string> {
|
||||
await ensureKeytoolCommandExistsAsync();
|
||||
const keystorePath = await writeFileToTmpAsync(keystore.keystore, '-keystore.jks');
|
||||
const certPath = path.join(getTmpDirectory(), `${uuidv4()}.cer`);
|
||||
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
|
||||
const keystoreData = {
|
||||
keystorePassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyPassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyAlias: crypto.randomBytes(16).toString('hex'),
|
||||
};
|
||||
return await createKeystoreAsync(keystoreData, projectId);
|
||||
}
|
||||
|
||||
async function createKeystoreAsync(
|
||||
credentials: {
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
},
|
||||
projectId: string
|
||||
): Promise<KeystoreWithType> {
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Attempt,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
|
||||
try {
|
||||
await spawnAsync('keytool', [
|
||||
'-exportcert',
|
||||
...(options?.rfcFormat ? ['-rfc'] : []),
|
||||
'-keystore',
|
||||
keystorePath,
|
||||
'-storepass',
|
||||
keystore.keystorePassword,
|
||||
'-alias',
|
||||
keystore.keyAlias,
|
||||
'-file',
|
||||
certPath,
|
||||
'-noprompt',
|
||||
'-storetype',
|
||||
'JKS',
|
||||
]);
|
||||
return await fs.readFile(certPath, 'base64');
|
||||
} finally {
|
||||
await fs.remove(keystorePath);
|
||||
await fs.remove(certPath);
|
||||
await ensureKeytoolCommandExistsAsync();
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Fail,
|
||||
reason: error.message,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logKeystoreHashesAsync(keystore: Keystore, linePrefix: string = '') {
|
||||
const base64Cert = await exportCertificateAsync(keystore);
|
||||
const data = Buffer.from(base64Cert, 'base64');
|
||||
const googleHash = crypto.createHash('sha1').update(data).digest('hex').toUpperCase();
|
||||
const googleHash256 = crypto.createHash('sha256').update(data).digest('hex').toUpperCase();
|
||||
const fbHash = crypto.createHash('sha1').update(data).digest('base64');
|
||||
Log.log(
|
||||
`${linePrefix}Google Certificate Fingerprint: ${googleHash.replace(/(.{2}(?!$))/g, '$1:')}`
|
||||
);
|
||||
Log.log(`${linePrefix}Google Certificate Hash (SHA-1): ${googleHash}`);
|
||||
Log.log(`${linePrefix}Google Certificate Hash (SHA-256): ${googleHash256}`);
|
||||
Log.log(`${linePrefix}Facebook Key Hash: ${fbHash}`);
|
||||
}
|
||||
|
||||
async function createKeystoreAsync(credentials: {
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
}): Promise<KeystoreWithType> {
|
||||
await ensureKeytoolCommandExistsAsync();
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
|
||||
try {
|
||||
@@ -110,42 +94,27 @@ async function createKeystoreAsync(credentials: {
|
||||
'-dname',
|
||||
`CN=,OU=,O=,L=,S=,C=US`,
|
||||
]);
|
||||
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Success,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
keystore: await fs.readFile(keystorePath, 'base64'),
|
||||
type: AndroidKeystoreType.Jks,
|
||||
};
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
|
||||
project_id: projectId,
|
||||
step: KeystoreCreateStep.Fail,
|
||||
reason: error.message,
|
||||
type: AndroidKeystoreType.Jks,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await fs.remove(keystorePath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateRandomKeystoreAsync(): Promise<KeystoreWithType> {
|
||||
const keystoreData = {
|
||||
keystorePassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyPassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyAlias: crypto.randomBytes(16).toString('hex'),
|
||||
};
|
||||
return await createKeystoreAsync(keystoreData);
|
||||
}
|
||||
|
||||
export async function validateKeystoreAsync(keystore: Keystore): Promise<void> {
|
||||
await ensureKeytoolCommandExistsAsync();
|
||||
try {
|
||||
await tempy.write.task(Buffer.from(keystore.keystore, 'base64'), async keystorePath => {
|
||||
await spawnAsync('keytool', [
|
||||
'-list',
|
||||
'-keystore',
|
||||
keystorePath,
|
||||
'-storepass',
|
||||
keystore.keystorePassword,
|
||||
'-alias',
|
||||
keystore.keyAlias,
|
||||
]);
|
||||
});
|
||||
} catch (e: any) {
|
||||
throw new Error(
|
||||
`An error occurred when validating the Android keystore: ${e.stdout ?? e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
|
||||
async runAsync(ctx: Context): Promise<BuildProfile<T>> {
|
||||
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
|
||||
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(profileName, this.platform);
|
||||
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(this.platform, profileName);
|
||||
Log.succeed(`Using build profile: ${profileName}`);
|
||||
return easConfig;
|
||||
}
|
||||
|
||||
+33
-6
@@ -4851,19 +4851,40 @@ export type UpdatePublishMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateSubmissionMutationVariables = Exact<{
|
||||
export type CreateAndroidSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
platform: AppPlatform;
|
||||
config: Scalars['JSONObject'];
|
||||
config: AndroidSubmissionConfigInput;
|
||||
submittedBuildId?: Maybe<Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateSubmissionMutation = (
|
||||
export type CreateAndroidSubmissionMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { submission: (
|
||||
{ __typename?: 'SubmissionMutation' }
|
||||
& { createSubmission: (
|
||||
& { createAndroidSubmission: (
|
||||
{ __typename?: 'CreateSubmissionResult' }
|
||||
& { submission: (
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id'>
|
||||
& SubmissionFragment
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateIosSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
config: IosSubmissionConfigInput;
|
||||
submittedBuildId?: Maybe<Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateIosSubmissionMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { submission: (
|
||||
{ __typename?: 'SubmissionMutation' }
|
||||
& { createIosSubmission: (
|
||||
{ __typename?: 'CreateSubmissionResult' }
|
||||
& { submission: (
|
||||
{ __typename?: 'Submission' }
|
||||
@@ -5209,7 +5230,13 @@ export type SubmissionFragment = (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id' | 'name'>
|
||||
) }
|
||||
), error?: Maybe<(
|
||||
), androidConfig?: Maybe<(
|
||||
{ __typename?: 'AndroidSubmissionConfig' }
|
||||
& Pick<AndroidSubmissionConfig, 'applicationIdentifier' | 'track' | 'releaseStatus'>
|
||||
)>, iosConfig?: Maybe<(
|
||||
{ __typename?: 'IosSubmissionConfig' }
|
||||
& Pick<IosSubmissionConfig, 'ascAppIdentifier' | 'appleIdUsername'>
|
||||
)>, error?: Maybe<(
|
||||
{ __typename?: 'SubmissionError' }
|
||||
& Pick<SubmissionError, 'errorCode' | 'message'>
|
||||
)> }
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AppPlatform,
|
||||
CreateSubmissionMutation,
|
||||
CreateSubmissionMutationVariables,
|
||||
CreateAndroidSubmissionInput,
|
||||
CreateAndroidSubmissionMutation,
|
||||
CreateAndroidSubmissionMutationVariables,
|
||||
CreateIosSubmissionInput,
|
||||
CreateIosSubmissionMutation,
|
||||
CreateIosSubmissionMutationVariables,
|
||||
SubmissionFragment,
|
||||
} from '../generated';
|
||||
import { SubmissionFragmentNode } from '../types/Submission';
|
||||
|
||||
export const SubmissionMutation = {
|
||||
async createSubmissionAsync(input: {
|
||||
appId: string;
|
||||
submittedBuildId?: string;
|
||||
platform: AppPlatform;
|
||||
config: JSONObject;
|
||||
}): Promise<SubmissionFragment> {
|
||||
async createAndroidSubmissionAsync(
|
||||
input: CreateAndroidSubmissionInput
|
||||
): Promise<SubmissionFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateSubmissionMutation, CreateSubmissionMutationVariables>(
|
||||
.mutation<CreateAndroidSubmissionMutation, CreateAndroidSubmissionMutationVariables>(
|
||||
gql`
|
||||
mutation CreateSubmissionMutation(
|
||||
mutation CreateAndroidSubmissionMutation(
|
||||
$appId: ID!
|
||||
$platform: AppPlatform!
|
||||
$config: JSONObject!
|
||||
$config: AndroidSubmissionConfigInput!
|
||||
$submittedBuildId: ID
|
||||
) {
|
||||
submission {
|
||||
createSubmission(
|
||||
input: {
|
||||
appId: $appId
|
||||
platform: $platform
|
||||
config: $config
|
||||
submittedBuildId: $submittedBuildId
|
||||
}
|
||||
createAndroidSubmission(
|
||||
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
|
||||
) {
|
||||
submission {
|
||||
id
|
||||
@@ -51,6 +44,35 @@ export const SubmissionMutation = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return nullthrows(data.submission?.createSubmission.submission);
|
||||
return nullthrows(data.submission.createAndroidSubmission.submission);
|
||||
},
|
||||
async createIosSubmissionAsync(input: CreateIosSubmissionInput): Promise<SubmissionFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateIosSubmissionMutation, CreateIosSubmissionMutationVariables>(
|
||||
gql`
|
||||
mutation CreateIosSubmissionMutation(
|
||||
$appId: ID!
|
||||
$config: IosSubmissionConfigInput!
|
||||
$submittedBuildId: ID
|
||||
) {
|
||||
submission {
|
||||
createIosSubmission(
|
||||
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
|
||||
) {
|
||||
submission {
|
||||
id
|
||||
...SubmissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(SubmissionFragmentNode)}
|
||||
`,
|
||||
input
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return nullthrows(data.submission.createIosSubmission.submission);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { AppPlatform } from '../generated';
|
||||
|
||||
export function toAppPlatform(platform: Platform): AppPlatform {
|
||||
if (platform === Platform.ANDROID) {
|
||||
return AppPlatform.Android;
|
||||
} else if (platform === Platform.IOS) {
|
||||
return AppPlatform.Ios;
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,15 @@ export const SubmissionFragmentNode = gql`
|
||||
name
|
||||
}
|
||||
}
|
||||
androidConfig {
|
||||
applicationIdentifier
|
||||
track
|
||||
releaseStatus
|
||||
}
|
||||
iosConfig {
|
||||
ascAppIdentifier
|
||||
appleIdUsername
|
||||
}
|
||||
error {
|
||||
errorCode
|
||||
message
|
||||
|
||||
@@ -90,13 +90,19 @@ export default class Log {
|
||||
*/
|
||||
export function learnMore(
|
||||
url: string,
|
||||
{ learnMoreMessage, dim = true }: { learnMoreMessage?: string; dim?: boolean } = {}
|
||||
{
|
||||
learnMoreMessage: maybeLearnMoreMessage,
|
||||
dim = true,
|
||||
}: { learnMoreMessage?: string; dim?: boolean } = {}
|
||||
): string {
|
||||
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
|
||||
if (terminalLink.isSupported) {
|
||||
const text = terminalLink(learnMoreMessage ?? 'Learn more.', url);
|
||||
const text = terminalLink(maybeLearnMoreMessage ?? 'Learn more.', url);
|
||||
return dim ? chalk.dim(text) : text;
|
||||
}
|
||||
const text = `${learnMoreMessage ?? 'Learn more'}: ${chalk.underline(url)}`;
|
||||
const learnMoreMessage = maybeLearnMoreMessage ?? 'Learn more';
|
||||
const text = `${
|
||||
learnMoreMessage === '' ? learnMoreMessage : `${learnMoreMessage}: `
|
||||
}${chalk.underline(url)}`;
|
||||
return dim ? chalk.dim(text) : text;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import once from 'lodash/once';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getProjectConfigDescription, getUsername, sanitizedProjectName } from '../projectUtils';
|
||||
import { getProjectConfigDescription, getUsername } from '../projectUtils';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
|
||||
const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
|
||||
@@ -58,7 +58,7 @@ export async function getBundleIdentifierAsync(
|
||||
} else {
|
||||
// TODO: the following asserts are only temporary until we support app extensions in managed projects
|
||||
assert(
|
||||
!targetName || targetName === sanitizedProjectName(exp.name),
|
||||
!targetName || targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
|
||||
'targetName cannot be set to an arbitrary value for managed projects'
|
||||
);
|
||||
assert(!buildConfiguration, 'buildConfiguration cannot be passed for managed projects');
|
||||
|
||||
@@ -7,7 +7,6 @@ import sortBy from 'lodash/sortBy';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { sanitizedProjectName } from '../projectUtils';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
|
||||
export interface XcodeBuildContext {
|
||||
@@ -45,7 +44,7 @@ export async function resolveXcodeBuildContextAsync(
|
||||
if (!expoName) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
const sanitizedExpoName = sanitizedProjectName(expoName);
|
||||
const sanitizedExpoName = IOSConfig.XcodeUtils.sanitizedName(expoName);
|
||||
if (!sanitizedExpoName) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { exit } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import type MetroConfig from 'metro-config';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { BuildContext } from '../build/context';
|
||||
import { Platform } from '../build/types';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
|
||||
|
||||
@@ -224,15 +224,6 @@ export async function getBranchByNameAsync({
|
||||
return data.app.byId.updateBranchByName;
|
||||
}
|
||||
|
||||
// copy-pasted from expo-cli
|
||||
// https://github.com/expo/expo-cli/blob/master/packages/expo-cli/src/utils/extractTemplateAppAsync.ts#L15
|
||||
export function sanitizedProjectName(name: string) {
|
||||
return name
|
||||
.replace(/[\W_]+/g, '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
// return project id of existing/newly created project, or null if user declines
|
||||
export async function promptToCreateProjectIfNotExistsAsync(
|
||||
exp: ExpoConfig
|
||||
|
||||
+84
-86
@@ -1,14 +1,16 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import { URL, parse as parseUrl } from 'url';
|
||||
import { URL } from 'url';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getBuildByIdForSubmissionAsync, getLatestBuildForSubmissionAsync } from '../utils/builds';
|
||||
import { isExistingFile, uploadAppArchiveAsync } from '../utils/files';
|
||||
import { BuildFragment } from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { getBuildByIdForSubmissionAsync, getLatestBuildForSubmissionAsync } from './utils/builds';
|
||||
import { isExistingFile, uploadAppArchiveAsync } from './utils/files';
|
||||
|
||||
export enum ArchiveFileSourceType {
|
||||
export enum ArchiveSourceType {
|
||||
url,
|
||||
latest,
|
||||
path,
|
||||
@@ -16,88 +18,88 @@ export enum ArchiveFileSourceType {
|
||||
prompt,
|
||||
}
|
||||
|
||||
interface ArchiveFileSourceBase {
|
||||
sourceType: ArchiveFileSourceType;
|
||||
projectDir: string;
|
||||
platform: AppPlatform;
|
||||
interface ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType;
|
||||
platform: Platform;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
interface ArchiveFileUrlSource extends ArchiveFileSourceBase {
|
||||
sourceType: ArchiveFileSourceType.url;
|
||||
interface ArchiveUrlSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.url;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ArchiveFileLatestSource extends ArchiveFileSourceBase {
|
||||
sourceType: ArchiveFileSourceType.latest;
|
||||
interface ArchiveLatestSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.latest;
|
||||
}
|
||||
|
||||
interface ArchiveFilePathSource extends ArchiveFileSourceBase {
|
||||
sourceType: ArchiveFileSourceType.path;
|
||||
interface ArchivePathSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.path;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface ArchiveFileBuildIdSource extends ArchiveFileSourceBase {
|
||||
sourceType: ArchiveFileSourceType.buildId;
|
||||
interface ArchiveBuildIdSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.buildId;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ArchiveFilePromptSource extends ArchiveFileSourceBase {
|
||||
sourceType: ArchiveFileSourceType.prompt;
|
||||
interface ArchivePromptSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.prompt;
|
||||
}
|
||||
|
||||
export interface ResolvedArchive {
|
||||
location: string;
|
||||
realSource: ArchiveFileSource;
|
||||
export interface Archive {
|
||||
build?: BuildFragment;
|
||||
source: ArchiveSource;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type ArchiveFileSource =
|
||||
| ArchiveFileUrlSource
|
||||
| ArchiveFileLatestSource
|
||||
| ArchiveFilePathSource
|
||||
| ArchiveFileBuildIdSource
|
||||
| ArchiveFilePromptSource;
|
||||
export type ArchiveSource =
|
||||
| ArchiveUrlSource
|
||||
| ArchiveLatestSource
|
||||
| ArchivePathSource
|
||||
| ArchiveBuildIdSource
|
||||
| ArchivePromptSource;
|
||||
|
||||
export async function getArchiveFileLocationAsync(
|
||||
source: ArchiveFileSource
|
||||
): Promise<ResolvedArchive> {
|
||||
export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
|
||||
switch (source.sourceType) {
|
||||
case ArchiveFileSourceType.prompt:
|
||||
case ArchiveSourceType.prompt:
|
||||
return await handlePromptSourceAsync(source);
|
||||
case ArchiveFileSourceType.url: {
|
||||
case ArchiveSourceType.url: {
|
||||
return await handleUrlSourceAsync(source);
|
||||
}
|
||||
case ArchiveFileSourceType.latest: {
|
||||
case ArchiveSourceType.latest: {
|
||||
return await handleLatestSourceAsync(source);
|
||||
}
|
||||
case ArchiveFileSourceType.path: {
|
||||
case ArchiveSourceType.path: {
|
||||
return await handlePathSourceAsync(source);
|
||||
}
|
||||
case ArchiveFileSourceType.buildId: {
|
||||
case ArchiveSourceType.buildId: {
|
||||
return await handleBuildIdSourceAsync(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUrlSourceAsync(source: ArchiveFileUrlSource): Promise<ResolvedArchive> {
|
||||
async function handleUrlSourceAsync(source: ArchiveUrlSource): Promise<Archive> {
|
||||
if (!validateUrl(source.url)) {
|
||||
Log.error(chalk.bold(`The URL you provided is invalid: ${source.url}`));
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
location: source.url,
|
||||
realSource: source,
|
||||
url: source.url,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise<ResolvedArchive> {
|
||||
async function handleLatestSourceAsync(source: ArchiveLatestSource): Promise<Archive> {
|
||||
try {
|
||||
const latestBuild = await getLatestBuildForSubmissionAsync(source.platform, source.projectId);
|
||||
const latestBuild = await getLatestBuildForSubmissionAsync(
|
||||
toAppPlatform(source.platform),
|
||||
source.projectId
|
||||
);
|
||||
|
||||
if (!latestBuild) {
|
||||
Log.error(
|
||||
@@ -105,16 +107,16 @@ async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise
|
||||
"Couldn't find any builds for this project on EAS servers. It looks like you haven't run 'eas build' yet."
|
||||
)
|
||||
);
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
location: latestBuild.artifacts.buildUrl,
|
||||
realSource: source,
|
||||
build: latestBuild,
|
||||
url: latestBuild.artifacts.buildUrl,
|
||||
source,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
@@ -122,44 +124,42 @@ async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<ResolvedArchive> {
|
||||
async function handlePathSourceAsync(source: ArchivePathSource): Promise<Archive> {
|
||||
if (!(await isExistingFile(source.path))) {
|
||||
Log.error(chalk.bold(`${source.path} doesn't exist`));
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
Log.log('Uploading your app archive to the Expo Submission Service');
|
||||
const uploadUrl = await uploadAppArchiveAsync(source.path);
|
||||
return {
|
||||
location: uploadUrl,
|
||||
realSource: source,
|
||||
url: uploadUrl,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleBuildIdSourceAsync(
|
||||
source: ArchiveFileBuildIdSource
|
||||
): Promise<ResolvedArchive> {
|
||||
async function handleBuildIdSourceAsync(source: ArchiveBuildIdSource): Promise<Archive> {
|
||||
try {
|
||||
const build = await getBuildByIdForSubmissionAsync(source.platform, source.id);
|
||||
const build = await getBuildByIdForSubmissionAsync(toAppPlatform(source.platform), source.id);
|
||||
return {
|
||||
location: build.artifacts.buildUrl,
|
||||
realSource: source,
|
||||
build,
|
||||
source,
|
||||
url: build.artifacts.buildUrl,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(chalk.bold(`Couldn't find build for id ${source.id}`));
|
||||
Log.error(err);
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise<ResolvedArchive> {
|
||||
async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Archive> {
|
||||
const { sourceType: sourceTypeRaw } = await promptAsync({
|
||||
name: 'sourceType',
|
||||
type: 'select',
|
||||
@@ -167,52 +167,52 @@ async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise
|
||||
choices: [
|
||||
{
|
||||
title: 'Latest build from EAS',
|
||||
value: ArchiveFileSourceType.latest,
|
||||
value: ArchiveSourceType.latest,
|
||||
},
|
||||
{ title: 'I have a url to the app archive', value: ArchiveFileSourceType.url },
|
||||
{ title: 'I have a url to the app archive', value: ArchiveSourceType.url },
|
||||
{
|
||||
title: 'Local app binary file',
|
||||
value: ArchiveFileSourceType.path,
|
||||
value: ArchiveSourceType.path,
|
||||
},
|
||||
{
|
||||
title: 'A build identified by a build id',
|
||||
value: ArchiveFileSourceType.buildId,
|
||||
value: ArchiveSourceType.buildId,
|
||||
},
|
||||
],
|
||||
});
|
||||
const sourceType = sourceTypeRaw as ArchiveFileSourceType;
|
||||
const sourceType = sourceTypeRaw as ArchiveSourceType;
|
||||
switch (sourceType) {
|
||||
case ArchiveFileSourceType.url: {
|
||||
case ArchiveSourceType.url: {
|
||||
const url = await askForArchiveUrlAsync();
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.url,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url,
|
||||
});
|
||||
}
|
||||
case ArchiveFileSourceType.path: {
|
||||
case ArchiveSourceType.path: {
|
||||
const path = await askForArchivePathAsync(source.platform);
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.path,
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
});
|
||||
}
|
||||
case ArchiveFileSourceType.latest: {
|
||||
return getArchiveFileLocationAsync({
|
||||
case ArchiveSourceType.latest: {
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.latest,
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
});
|
||||
}
|
||||
case ArchiveFileSourceType.buildId: {
|
||||
case ArchiveSourceType.buildId: {
|
||||
const id = await askForBuildIdAsync();
|
||||
return getArchiveFileLocationAsync({
|
||||
return getArchiveAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.buildId,
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id,
|
||||
});
|
||||
}
|
||||
case ArchiveFileSourceType.prompt:
|
||||
case ArchiveSourceType.prompt:
|
||||
throw new Error('This should never happen');
|
||||
}
|
||||
}
|
||||
@@ -239,8 +239,8 @@ async function askForArchiveUrlAsync(): Promise<string> {
|
||||
return url;
|
||||
}
|
||||
|
||||
async function askForArchivePathAsync(platform: AppPlatform): Promise<string> {
|
||||
const isIos = platform === AppPlatform.Ios;
|
||||
async function askForArchivePathAsync(platform: Platform): Promise<string> {
|
||||
const isIos = platform === Platform.IOS;
|
||||
const defaultArchivePath = `/path/to/your/archive.${isIos ? 'ipa' : 'aab'}`;
|
||||
const { path } = await promptAsync({
|
||||
name: 'path',
|
||||
@@ -279,9 +279,7 @@ async function askForBuildIdAsync(): Promise<string> {
|
||||
function validateUrl(url: string): boolean {
|
||||
const protocols = ['http', 'https'];
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
const parsed = parseUrl(url);
|
||||
const parsed = new URL(url);
|
||||
return protocols
|
||||
? parsed.protocol
|
||||
? protocols.map(x => `${x.toLowerCase()}:`).includes(parsed.protocol)
|
||||
@@ -1,38 +1,33 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AppPlatform, SubmissionFragment } from '../graphql/generated';
|
||||
import { SubmissionMutation } from '../graphql/mutations/SubmissionMutation';
|
||||
import {
|
||||
AndroidSubmissionConfigInput,
|
||||
IosSubmissionConfigInput,
|
||||
SubmissionFragment,
|
||||
} from '../graphql/generated';
|
||||
import Log from '../log';
|
||||
import { AndroidSubmissionConfig } from './android/AndroidSubmissionConfig';
|
||||
import { IosSubmissionConfig } from './ios/IosSubmissionConfig';
|
||||
import { BaseSubmissionContext } from './types';
|
||||
import { SubmissionContext } from './types';
|
||||
|
||||
export default abstract class BaseSubmitter<
|
||||
SubmissionContext extends BaseSubmissionContext,
|
||||
SubmissionOptions
|
||||
> {
|
||||
protected constructor(
|
||||
private platform: AppPlatform,
|
||||
protected ctx: SubmissionContext,
|
||||
protected options: SubmissionOptions
|
||||
) {}
|
||||
export interface SubmissionInput<P extends Platform> {
|
||||
projectId: string;
|
||||
submissionConfig: P extends Platform.ANDROID
|
||||
? AndroidSubmissionConfigInput
|
||||
: IosSubmissionConfigInput;
|
||||
buildId?: string;
|
||||
}
|
||||
export default abstract class BaseSubmitter<P extends Platform, SubmissionOptions> {
|
||||
constructor(protected ctx: SubmissionContext<P>, protected options: SubmissionOptions) {}
|
||||
|
||||
public abstract submitAsync(): Promise<SubmissionFragment>;
|
||||
|
||||
protected async createSubmissionAsync(
|
||||
submissionConfig: AndroidSubmissionConfig | IosSubmissionConfig,
|
||||
buildId?: string
|
||||
submissionInput: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
const scheduleSpinner = ora('Scheduling submission').start();
|
||||
try {
|
||||
const submission = await SubmissionMutation.createSubmissionAsync({
|
||||
appId: submissionConfig.projectId,
|
||||
platform: this.platform,
|
||||
config: submissionConfig as unknown as JSONObject,
|
||||
submittedBuildId: buildId,
|
||||
});
|
||||
const submission = this.createPlatformSubmissionAsync(submissionInput);
|
||||
scheduleSpinner.succeed();
|
||||
return submission;
|
||||
} catch (err) {
|
||||
@@ -40,4 +35,8 @@ export default abstract class BaseSubmitter<
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract createPlatformSubmissionAsync(
|
||||
input: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment>;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated';
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { uploadAsync } from '../../uploads';
|
||||
import { Archive, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource';
|
||||
import { getBuildByIdForSubmissionAsync, getLatestBuildForSubmissionAsync } from '../utils/builds';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../log');
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('../utils/builds');
|
||||
jest.mock('../../uploads');
|
||||
|
||||
const ARCHIVE_URL = 'https://url.to/archive.tar.gz';
|
||||
|
||||
const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
|
||||
id: uuidv4(),
|
||||
artifacts: {
|
||||
buildUrl: ARCHIVE_URL,
|
||||
},
|
||||
appVersion: '1.2.3',
|
||||
platform: AppPlatform.Android,
|
||||
};
|
||||
|
||||
const SOURCE_STUB_INPUT = {
|
||||
projectId: uuidv4(),
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: '.',
|
||||
};
|
||||
|
||||
describe(getArchiveAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
|
||||
asMock(promptAsync).mockReset();
|
||||
asMock(promptAsync).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles URL source', async () => {
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: ARCHIVE_URL,
|
||||
});
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('prompts again if provided URL is invalid', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: 'invalid',
|
||||
});
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledTimes(2);
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles prompt source', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles Build ID source', async () => {
|
||||
const buildId = uuidv4();
|
||||
asMock(getBuildByIdForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: buildId,
|
||||
});
|
||||
|
||||
expect(getBuildByIdForSubmissionAsync).toBeCalledWith(
|
||||
toAppPlatform(SOURCE_STUB_INPUT.platform),
|
||||
buildId
|
||||
);
|
||||
assertArchiveResult(archive, ArchiveSourceType.buildId);
|
||||
});
|
||||
|
||||
it('prompts again if build with provided ID doesnt exist', async () => {
|
||||
asMock(getBuildByIdForSubmissionAsync).mockRejectedValue(new Error('Build doesnt exist'));
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: uuidv4(),
|
||||
});
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles latest build source', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
projectId,
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
});
|
||||
|
||||
expect(getLatestBuildForSubmissionAsync).toBeCalledWith(
|
||||
toAppPlatform(SOURCE_STUB_INPUT.platform),
|
||||
projectId
|
||||
);
|
||||
assertArchiveResult(archive, ArchiveSourceType.latest);
|
||||
});
|
||||
|
||||
it('prompts again if no builds exists when selected latest', async () => {
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(null);
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
});
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles path source', async () => {
|
||||
const path = '/archive.apk';
|
||||
await fs.writeFile(path, 'some content');
|
||||
asMock(uploadAsync).mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
});
|
||||
|
||||
expect(uploadAsync).toBeCalledWith(
|
||||
UploadSessionType.EasSubmitAppArchive,
|
||||
path,
|
||||
expect.anything()
|
||||
);
|
||||
assertArchiveResult(archive, ArchiveSourceType.path);
|
||||
});
|
||||
|
||||
it('prompts again if provided path doesnt exist', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path: './doesnt/exist.aab',
|
||||
});
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
});
|
||||
});
|
||||
|
||||
function assertArchiveResult(
|
||||
archive: Archive,
|
||||
expectedSourceType: ArchiveSourceType,
|
||||
expectedUrl: string = ARCHIVE_URL
|
||||
) {
|
||||
expect(archive.source.sourceType).toBe(expectedSourceType);
|
||||
expect(archive.url).toBe(expectedUrl);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { AndroidArchiveType } from '../types';
|
||||
|
||||
export interface AndroidSubmissionConfig {
|
||||
projectId: string;
|
||||
archiveUrl: string;
|
||||
archiveType: AndroidArchiveType;
|
||||
androidPackage: string;
|
||||
track: ReleaseTrack;
|
||||
serviceAccount: string;
|
||||
releaseStatus?: ReleaseStatus;
|
||||
changesNotSentForReview: boolean;
|
||||
}
|
||||
|
||||
export enum ReleaseStatus {
|
||||
completed = 'completed',
|
||||
draft = 'draft',
|
||||
halted = 'halted',
|
||||
inProgress = 'inProgress',
|
||||
}
|
||||
|
||||
export enum ReleaseTrack {
|
||||
production = 'production',
|
||||
beta = 'beta',
|
||||
alpha = 'alpha',
|
||||
internal = 'internal',
|
||||
}
|
||||
@@ -1,35 +1,45 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack, AndroidSubmitProfile } from '@expo/eas-json';
|
||||
import { Result, result } from '@expo/results';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import {
|
||||
SubmissionAndroidReleaseStatus,
|
||||
SubmissionAndroidTrack,
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getApplicationIdAsync } from '../../project/android/applicationId';
|
||||
import { ArchiveSource, ArchiveTypeSource, ArchiveTypeSourceType } from '../archiveSource';
|
||||
import { resolveArchiveFileSource } from '../commons';
|
||||
import { AndroidArchiveType, AndroidSubmissionContext, AndroidSubmitCommandFlags } from '../types';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext, SubmitArchiveFlags } from '../types';
|
||||
import { AndroidPackageSource, AndroidPackageSourceType } from './AndroidPackageSource';
|
||||
import { ReleaseStatus, ReleaseTrack } from './AndroidSubmissionConfig';
|
||||
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
|
||||
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
|
||||
|
||||
class AndroidSubmitCommand {
|
||||
export default class AndroidSubmitCommand {
|
||||
static createContext({
|
||||
archiveFlags,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
}: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: AndroidSubmitProfile;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
commandFlags: AndroidSubmitCommandFlags;
|
||||
}): AndroidSubmissionContext {
|
||||
}): SubmissionContext<Platform.ANDROID> {
|
||||
return {
|
||||
archiveFlags,
|
||||
platform: Platform.ANDROID,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private ctx: AndroidSubmissionContext) {}
|
||||
constructor(private ctx: SubmissionContext<Platform.ANDROID>) {}
|
||||
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -42,7 +52,7 @@ class AndroidSubmitCommand {
|
||||
const androidPackageSource = await this.resolveAndroidPackageSourceAsync();
|
||||
const track = this.resolveTrack();
|
||||
const releaseStatus = this.resolveReleaseStatus();
|
||||
const archiveSource = this.resolveArchiveSource(this.ctx.projectId);
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const serviceAccountSource = this.resolveServiceAccountSource();
|
||||
|
||||
const errored = [
|
||||
@@ -65,7 +75,7 @@ class AndroidSubmitCommand {
|
||||
releaseStatus: releaseStatus.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
serviceAccountSource: serviceAccountSource.enforceValue(),
|
||||
changesNotSentForReview: this.ctx.commandFlags.changesNotSentForReview,
|
||||
changesNotSentForReview: this.ctx.profile.changesNotSentForReview,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,12 +89,7 @@ class AndroidSubmitCommand {
|
||||
}
|
||||
|
||||
private async resolveAndroidPackageSourceAsync(): Promise<Result<AndroidPackageSource>> {
|
||||
let androidPackage: string | undefined;
|
||||
if (this.ctx.commandFlags.androidPackage) {
|
||||
androidPackage = this.ctx.commandFlags.androidPackage;
|
||||
} else {
|
||||
androidPackage = await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
}
|
||||
const androidPackage = await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
if (androidPackage) {
|
||||
return result({
|
||||
sourceType: AndroidPackageSourceType.userDefined,
|
||||
@@ -97,71 +102,56 @@ class AndroidSubmitCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveTrack(): Result<ReleaseTrack> {
|
||||
const { track } = this.ctx.commandFlags;
|
||||
private resolveTrack(): Result<SubmissionAndroidTrack> {
|
||||
const { track } = this.ctx.profile;
|
||||
if (!track) {
|
||||
return result(ReleaseTrack.internal);
|
||||
return result(SubmissionAndroidTrack.Internal);
|
||||
}
|
||||
if (track in ReleaseTrack) {
|
||||
return result(ReleaseTrack[track as keyof typeof ReleaseTrack]);
|
||||
const capitalizedTrack = capitalize(track);
|
||||
if (capitalizedTrack in SubmissionAndroidTrack) {
|
||||
return result(
|
||||
SubmissionAndroidTrack[capitalizedTrack as keyof typeof SubmissionAndroidTrack]
|
||||
);
|
||||
} else {
|
||||
return result(
|
||||
new Error(
|
||||
`Unsupported track: ${track} (valid options: ${Object.keys(ReleaseTrack).join(', ')})`
|
||||
`Unsupported track: ${track} (valid options: ${Object.keys(AndroidReleaseTrack).join(
|
||||
', '
|
||||
)})`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveReleaseStatus(): Result<ReleaseStatus> {
|
||||
const { releaseStatus } = this.ctx.commandFlags;
|
||||
private resolveReleaseStatus(): Result<SubmissionAndroidReleaseStatus> {
|
||||
const { releaseStatus } = this.ctx.profile;
|
||||
if (!releaseStatus) {
|
||||
return result(ReleaseStatus.completed);
|
||||
return result(SubmissionAndroidReleaseStatus.Completed);
|
||||
}
|
||||
if (releaseStatus in ReleaseStatus) {
|
||||
return result(ReleaseStatus[releaseStatus as keyof typeof ReleaseStatus]);
|
||||
const capitalizedReleaseStatus = capitalize(releaseStatus);
|
||||
if (capitalizedReleaseStatus in SubmissionAndroidReleaseStatus) {
|
||||
return result(
|
||||
SubmissionAndroidReleaseStatus[
|
||||
capitalizedReleaseStatus as keyof typeof SubmissionAndroidReleaseStatus
|
||||
]
|
||||
);
|
||||
} else {
|
||||
return result(
|
||||
new Error(
|
||||
`Unsupported release status: ${releaseStatus} (valid options: ${Object.keys(
|
||||
ReleaseStatus
|
||||
AndroidReleaseStatus
|
||||
).join(', ')})`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveArchiveSource(projectId: string): Result<ArchiveSource> {
|
||||
return result({
|
||||
archiveFile: resolveArchiveFileSource(AppPlatform.Android, this.ctx, projectId),
|
||||
archiveType: this.resolveArchiveTypeSource(),
|
||||
});
|
||||
}
|
||||
|
||||
private resolveArchiveTypeSource(): ArchiveTypeSource {
|
||||
const { type: rawArchiveType } = this.ctx.commandFlags;
|
||||
if (rawArchiveType) {
|
||||
if (!(rawArchiveType in AndroidArchiveType)) {
|
||||
throw new Error(
|
||||
`Unsupported archive type: ${rawArchiveType} (valid options: ${Object.keys(
|
||||
AndroidArchiveType
|
||||
).join(', ')})`
|
||||
);
|
||||
}
|
||||
const archiveType = rawArchiveType as AndroidArchiveType;
|
||||
return {
|
||||
sourceType: ArchiveTypeSourceType.parameter,
|
||||
archiveType,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
sourceType: ArchiveTypeSourceType.infer,
|
||||
};
|
||||
}
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.ANDROID));
|
||||
}
|
||||
|
||||
private resolveServiceAccountSource(): Result<ServiceAccountSource> {
|
||||
const { serviceAccountKeyPath } = this.ctx.commandFlags;
|
||||
const { serviceAccountKeyPath } = this.ctx.profile;
|
||||
if (serviceAccountKeyPath) {
|
||||
return result({
|
||||
sourceType: ServiceAccountSourceType.path,
|
||||
@@ -174,5 +164,3 @@ class AndroidSubmitCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AndroidSubmitCommand;
|
||||
@@ -1,23 +1,29 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { AndroidArchiveType, AndroidSubmissionContext, ArchiveType } from '../types';
|
||||
import {
|
||||
AndroidSubmissionConfigInput,
|
||||
SubmissionAndroidReleaseStatus,
|
||||
SubmissionAndroidTrack,
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
|
||||
import {
|
||||
ArchiveSourceSummaryFields,
|
||||
formatArchiveSourceSummary,
|
||||
printSummary,
|
||||
} from '../utils/summary';
|
||||
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
|
||||
import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from './AndroidSubmissionConfig';
|
||||
import { ServiceAccountSource, getServiceAccountAsync } from './ServiceAccountSource';
|
||||
|
||||
export interface AndroidSubmissionOptions
|
||||
extends Pick<
|
||||
AndroidSubmissionConfig,
|
||||
'track' | 'releaseStatus' | 'projectId' | 'changesNotSentForReview'
|
||||
AndroidSubmissionConfigInput,
|
||||
'track' | 'releaseStatus' | 'changesNotSentForReview'
|
||||
> {
|
||||
projectId: string;
|
||||
androidPackageSource: AndroidPackageSource;
|
||||
archiveSource: ArchiveSource;
|
||||
serviceAccountSource: ServiceAccountSource;
|
||||
@@ -29,30 +35,41 @@ interface ResolvedSourceOptions {
|
||||
serviceAccountPath: string;
|
||||
}
|
||||
|
||||
class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSubmissionOptions> {
|
||||
constructor(ctx: AndroidSubmissionContext, options: AndroidSubmissionOptions) {
|
||||
super(AppPlatform.Android, ctx, options);
|
||||
}
|
||||
|
||||
export default class AndroidSubmitter extends BaseSubmitter<
|
||||
Platform.ANDROID,
|
||||
AndroidSubmissionOptions
|
||||
> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptions();
|
||||
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
SummaryHumanReadableKeys
|
||||
);
|
||||
|
||||
return await this.createSubmissionAsync(
|
||||
return await this.createSubmissionAsync({
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id
|
||||
);
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
});
|
||||
}
|
||||
|
||||
protected async createPlatformSubmissionAsync({
|
||||
projectId,
|
||||
submissionConfig,
|
||||
buildId,
|
||||
}: SubmissionInput<Platform.ANDROID>): Promise<SubmissionFragment> {
|
||||
return await SubmissionMutation.createAndroidSubmissionAsync({
|
||||
appId: projectId,
|
||||
config: submissionConfig,
|
||||
submittedBuildId: buildId,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveSourceOptions(): Promise<ResolvedSourceOptions> {
|
||||
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
|
||||
const archive = await getArchiveAsync(AppPlatform.Android, this.options.archiveSource);
|
||||
const archive = await getArchiveAsync(this.options.archiveSource);
|
||||
const serviceAccountPath = await getServiceAccountAsync(this.options.serviceAccountSource);
|
||||
return {
|
||||
androidPackage,
|
||||
@@ -64,26 +81,23 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
private async formatSubmissionConfig(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
|
||||
): Promise<AndroidSubmissionConfig> {
|
||||
): Promise<AndroidSubmissionConfigInput> {
|
||||
const serviceAccount = await fs.readFile(serviceAccountPath, 'utf-8');
|
||||
const { track, releaseStatus, projectId, changesNotSentForReview } = options;
|
||||
|
||||
const { track, releaseStatus, changesNotSentForReview } = options;
|
||||
return {
|
||||
androidPackage,
|
||||
archiveUrl: archive.location,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
applicationIdentifier: androidPackage,
|
||||
archiveUrl: archive.url,
|
||||
track,
|
||||
changesNotSentForReview,
|
||||
releaseStatus,
|
||||
projectId,
|
||||
serviceAccount,
|
||||
googleServiceAccountKeyJson: serviceAccount,
|
||||
};
|
||||
}
|
||||
|
||||
private prepareSummaryData(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
|
||||
): Summary {
|
||||
): SummaryData {
|
||||
const { projectId, track, releaseStatus, changesNotSentForReview } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
@@ -91,40 +105,31 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
projectId,
|
||||
androidPackage,
|
||||
track,
|
||||
changesNotSentForReview,
|
||||
releaseStatus,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
changesNotSentForReview: changesNotSentForReview ?? undefined,
|
||||
releaseStatus: releaseStatus ?? undefined,
|
||||
serviceAccountPath,
|
||||
...formatArchiveSourceSummary(archive),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type Summary = {
|
||||
type SummaryData = {
|
||||
androidPackage: string;
|
||||
archiveType: ArchiveType;
|
||||
serviceAccountPath: string;
|
||||
track: ReleaseTrack;
|
||||
releaseStatus?: ReleaseStatus;
|
||||
projectId?: string;
|
||||
changesNotSentForReview?: boolean;
|
||||
projectId: string;
|
||||
releaseStatus?: SubmissionAndroidReleaseStatus;
|
||||
serviceAccountPath: string;
|
||||
track: SubmissionAndroidTrack;
|
||||
} & ArchiveSourceSummaryFields;
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
|
||||
androidPackage: 'Android package',
|
||||
archivePath: 'Archive path',
|
||||
archiveUrl: 'Download URL',
|
||||
archiveType: 'Archive type',
|
||||
serviceAccountPath: 'Google Service Key',
|
||||
changesNotSentForReview: 'Changes not sent for a review',
|
||||
track: 'Release track',
|
||||
releaseStatus: 'Release status',
|
||||
projectId: 'Project ID',
|
||||
formattedBuild: 'Build',
|
||||
projectId: 'Project ID',
|
||||
releaseStatus: 'Release status',
|
||||
serviceAccountPath: 'Google Service Key',
|
||||
track: 'Release track',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {
|
||||
archiveType: (type: string) => type.toUpperCase(),
|
||||
};
|
||||
|
||||
export default AndroidSubmitter;
|
||||
@@ -1,22 +1,26 @@
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
|
||||
import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppPlatform, BuildFragment } from '../../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
SubmissionAndroidReleaseStatus,
|
||||
SubmissionAndroidTrack,
|
||||
} from '../../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import { AndroidArchiveType, AndroidSubmitCommandFlags } from '../../types';
|
||||
import { getLatestBuildForSubmissionAsync } from '../../utils/builds';
|
||||
import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from '../AndroidSubmissionConfig';
|
||||
import AndroidSubmitCommand from '../AndroidSubmitCommand';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('ora');
|
||||
jest.mock('../../../graphql/mutations/SubmissionMutation', () => ({
|
||||
SubmissionMutation: {
|
||||
createSubmissionAsync: jest.fn(),
|
||||
createAndroidSubmissionAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
@@ -61,8 +65,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
});
|
||||
afterAll(() => {
|
||||
vol.reset();
|
||||
|
||||
jest.unmock('@expo/config');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -73,38 +75,32 @@ describe(AndroidSubmitCommand, () => {
|
||||
it('sends a request to Submission Service', async () => {
|
||||
const projectId = uuidv4();
|
||||
|
||||
const commandFlags: AndroidSubmitCommandFlags = {
|
||||
latest: false,
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
type: 'apk',
|
||||
serviceAccountKeyPath: '/google-service-account.json',
|
||||
track: 'internal',
|
||||
releaseStatus: 'draft',
|
||||
changesNotSentForReview: false,
|
||||
};
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
commandFlags,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
profile: {
|
||||
serviceAccountKeyPath: '/google-service-account.json',
|
||||
track: AndroidReleaseTrack.internal,
|
||||
releaseStatus: AndroidReleaseStatus.draft,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const androidSubmissionConfig: AndroidSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
archiveType: AndroidArchiveType.apk,
|
||||
androidPackage: testProject.appJSON.expo.android?.package,
|
||||
serviceAccount: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: ReleaseStatus.draft,
|
||||
track: ReleaseTrack.internal,
|
||||
changesNotSentForReview: false,
|
||||
projectId,
|
||||
};
|
||||
|
||||
expect(SubmissionMutation.createSubmissionAsync).toHaveBeenCalledWith({
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: androidSubmissionConfig,
|
||||
platform: AppPlatform.Android,
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
applicationIdentifier: testProject.appJSON.expo.android?.package,
|
||||
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
|
||||
track: SubmissionAndroidTrack.Internal,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,37 +108,32 @@ describe(AndroidSubmitCommand, () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
|
||||
|
||||
const commandFlags: AndroidSubmitCommandFlags = {
|
||||
latest: true,
|
||||
type: 'apk',
|
||||
serviceAccountKeyPath: '/google-service-account.json',
|
||||
track: 'internal',
|
||||
releaseStatus: 'draft',
|
||||
changesNotSentForReview: false,
|
||||
};
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
commandFlags,
|
||||
archiveFlags: {
|
||||
latest: true,
|
||||
},
|
||||
profile: {
|
||||
serviceAccountKeyPath: '/google-service-account.json',
|
||||
track: AndroidReleaseTrack.internal,
|
||||
releaseStatus: AndroidReleaseStatus.draft,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const androidSubmissionConfig: AndroidSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
archiveType: AndroidArchiveType.apk,
|
||||
androidPackage: testProject.appJSON.expo.android?.package,
|
||||
serviceAccount: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: ReleaseStatus.draft,
|
||||
track: ReleaseTrack.internal,
|
||||
projectId,
|
||||
changesNotSentForReview: false,
|
||||
};
|
||||
|
||||
expect(SubmissionMutation.createSubmissionAsync).toHaveBeenCalledWith({
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: androidSubmissionConfig,
|
||||
platform: AppPlatform.Android,
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
applicationIdentifier: testProject.appJSON.expo.android?.package,
|
||||
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
|
||||
track: SubmissionAndroidTrack.Internal,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
submittedBuildId: fakeBuildFragment.id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { AndroidArchiveType, ArchiveType, IosArchiveType } from '../types';
|
||||
|
||||
export enum ArchiveTypeSourceType {
|
||||
infer,
|
||||
parameter,
|
||||
prompt,
|
||||
}
|
||||
|
||||
interface ArchiveTypeSourceBase {
|
||||
sourceType: ArchiveTypeSourceType;
|
||||
}
|
||||
|
||||
interface ArchiveTypeInferSource extends ArchiveTypeSourceBase {
|
||||
sourceType: ArchiveTypeSourceType.infer;
|
||||
}
|
||||
|
||||
interface ArchiveTypeParameterSource extends ArchiveTypeSourceBase {
|
||||
sourceType: ArchiveTypeSourceType.parameter;
|
||||
archiveType: ArchiveType;
|
||||
}
|
||||
|
||||
interface ArchiveTypePromptSource extends ArchiveTypeSourceBase {
|
||||
sourceType: ArchiveTypeSourceType.prompt;
|
||||
}
|
||||
|
||||
export type ArchiveTypeSource =
|
||||
| ArchiveTypeInferSource
|
||||
| ArchiveTypeParameterSource
|
||||
| ArchiveTypePromptSource;
|
||||
|
||||
export async function getArchiveTypeAsync(
|
||||
platform: AppPlatform,
|
||||
source: ArchiveTypeSource,
|
||||
location: string
|
||||
): Promise<ArchiveType> {
|
||||
// for iOS we have only one archive type
|
||||
if (platform === AppPlatform.Ios) {
|
||||
return IosArchiveType.ipa;
|
||||
}
|
||||
|
||||
switch (source.sourceType) {
|
||||
case ArchiveTypeSourceType.infer:
|
||||
return handleInferSourceAsync(platform, source, location);
|
||||
case ArchiveTypeSourceType.parameter:
|
||||
return handleParameterSourceAsync(platform, source, location);
|
||||
case ArchiveTypeSourceType.prompt:
|
||||
return handlePromptSourceAsync(platform, source, location);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInferSourceAsync(
|
||||
platform: AppPlatform,
|
||||
_source: ArchiveTypeInferSource,
|
||||
location: string
|
||||
): Promise<ArchiveType> {
|
||||
const inferredArchiveType = inferArchiveTypeFromLocation(platform, location);
|
||||
if (inferredArchiveType) {
|
||||
return inferredArchiveType;
|
||||
} else {
|
||||
Log.warn("We couldn't auto detect the archive type");
|
||||
return getArchiveTypeAsync(platform, { sourceType: ArchiveTypeSourceType.prompt }, location);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleParameterSourceAsync(
|
||||
platform: AppPlatform,
|
||||
source: ArchiveTypeParameterSource,
|
||||
location: string
|
||||
): Promise<ArchiveType> {
|
||||
const inferredArchiveType = inferArchiveTypeFromLocation(platform, location);
|
||||
if (inferredArchiveType) {
|
||||
if (source.archiveType === inferredArchiveType) {
|
||||
return source.archiveType;
|
||||
} else {
|
||||
Log.warn(
|
||||
`The archive seems to be .${inferredArchiveType} and you passed: --type ${source.archiveType}`
|
||||
);
|
||||
return getArchiveTypeAsync(platform, { sourceType: ArchiveTypeSourceType.prompt }, location);
|
||||
}
|
||||
} else {
|
||||
return source.archiveType;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePromptSourceAsync(
|
||||
platform: AppPlatform,
|
||||
_source: ArchiveTypePromptSource,
|
||||
location: string
|
||||
): Promise<ArchiveType> {
|
||||
const inferredArchiveType = inferArchiveTypeFromLocation(platform, location);
|
||||
const { archiveType: archiveTypeRaw } = await promptAsync({
|
||||
name: 'archiveType',
|
||||
type: 'select',
|
||||
message: "What's the archive type?",
|
||||
choices: [
|
||||
{ title: 'APK', value: AndroidArchiveType.apk },
|
||||
{ title: 'AAB', value: AndroidArchiveType.aab },
|
||||
],
|
||||
...(inferredArchiveType && { default: inferredArchiveType }),
|
||||
});
|
||||
return archiveTypeRaw as ArchiveType;
|
||||
}
|
||||
|
||||
type ArchiveInferredType = ArchiveType | null;
|
||||
|
||||
function inferArchiveTypeFromLocation(
|
||||
platform: AppPlatform,
|
||||
location: string
|
||||
): ArchiveInferredType {
|
||||
if (platform === AppPlatform.Ios) {
|
||||
return IosArchiveType.ipa;
|
||||
} else {
|
||||
if (location.endsWith('.apk')) {
|
||||
return AndroidArchiveType.apk;
|
||||
} else if (location.endsWith('.aab')) {
|
||||
return AndroidArchiveType.aab;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { AppPlatform, BuildFragment, UploadSessionType } from '../../../graphql/generated';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { uploadAsync } from '../../../uploads';
|
||||
import {
|
||||
getBuildByIdForSubmissionAsync,
|
||||
getLatestBuildForSubmissionAsync,
|
||||
} from '../../utils/builds';
|
||||
import {
|
||||
ArchiveFileSourceType,
|
||||
ResolvedArchive,
|
||||
getArchiveFileLocationAsync,
|
||||
} from '../ArchiveFileSource';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../utils/builds');
|
||||
jest.mock('../../../uploads');
|
||||
|
||||
const ARCHIVE_URL = 'https://url.to/archive.tar.gz';
|
||||
|
||||
const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
|
||||
id: uuidv4(),
|
||||
artifacts: {
|
||||
buildUrl: ARCHIVE_URL,
|
||||
},
|
||||
appVersion: '1.2.3',
|
||||
platform: AppPlatform.Android,
|
||||
};
|
||||
|
||||
const SOURCE_STUB_INPUT = {
|
||||
projectId: uuidv4(),
|
||||
platform: AppPlatform.Android,
|
||||
projectDir: '.',
|
||||
};
|
||||
|
||||
describe(getArchiveFileLocationAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
|
||||
asMock(promptAsync).mockReset();
|
||||
asMock(promptAsync).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles URL source', async () => {
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.url,
|
||||
url: ARCHIVE_URL,
|
||||
});
|
||||
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.url);
|
||||
});
|
||||
|
||||
it('prompts again if provided URL is invalid', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.url,
|
||||
url: 'invalid',
|
||||
});
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledTimes(2);
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.url);
|
||||
});
|
||||
|
||||
it('handles prompt source', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
});
|
||||
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.url);
|
||||
});
|
||||
|
||||
it('handles Build ID source', async () => {
|
||||
const buildId = uuidv4();
|
||||
asMock(getBuildByIdForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.buildId,
|
||||
id: buildId,
|
||||
});
|
||||
|
||||
expect(getBuildByIdForSubmissionAsync).toBeCalledWith(SOURCE_STUB_INPUT.platform, buildId);
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.buildId);
|
||||
});
|
||||
|
||||
it('prompts again if build with provided ID doesnt exist', async () => {
|
||||
asMock(getBuildByIdForSubmissionAsync).mockRejectedValue(new Error('Build doesnt exist'));
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.buildId,
|
||||
id: uuidv4(),
|
||||
});
|
||||
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.url);
|
||||
});
|
||||
|
||||
it('handles latest build source', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
projectId,
|
||||
sourceType: ArchiveFileSourceType.latest,
|
||||
});
|
||||
|
||||
expect(getLatestBuildForSubmissionAsync).toBeCalledWith(SOURCE_STUB_INPUT.platform, projectId);
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.latest);
|
||||
});
|
||||
|
||||
it('prompts again if no builds exists when selected latest', async () => {
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(null);
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.latest,
|
||||
});
|
||||
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.url);
|
||||
});
|
||||
|
||||
it('handles path source', async () => {
|
||||
const path = '/archive.apk';
|
||||
await fs.writeFile(path, 'some content');
|
||||
asMock(uploadAsync).mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.path,
|
||||
path,
|
||||
});
|
||||
|
||||
expect(uploadAsync).toBeCalledWith(
|
||||
UploadSessionType.EasSubmitAppArchive,
|
||||
path,
|
||||
expect.anything()
|
||||
);
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.path);
|
||||
});
|
||||
|
||||
it('prompts again if provided path doesnt exist', async () => {
|
||||
asMock(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveFileSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
|
||||
const resolvedArchive = await getArchiveFileLocationAsync({
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveFileSourceType.path,
|
||||
path: './doesnt/exist.aab',
|
||||
});
|
||||
|
||||
assertArchiveResult(resolvedArchive, ArchiveFileSourceType.url);
|
||||
});
|
||||
});
|
||||
|
||||
function assertArchiveResult(
|
||||
resolvedArchive: ResolvedArchive,
|
||||
expectedSourceType: ArchiveFileSourceType,
|
||||
expectedLocation: string = ARCHIVE_URL
|
||||
) {
|
||||
expect(resolvedArchive.realSource.sourceType).toBe(expectedSourceType);
|
||||
expect(resolvedArchive.location).toBe(expectedLocation);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
import { ArchiveType } from '../types';
|
||||
import {
|
||||
ArchiveFileSource,
|
||||
ArchiveFileSourceType,
|
||||
getArchiveFileLocationAsync,
|
||||
} from './ArchiveFileSource';
|
||||
import { ArchiveTypeSource, ArchiveTypeSourceType, getArchiveTypeAsync } from './ArchiveTypeSource';
|
||||
|
||||
export interface ArchiveSource {
|
||||
archiveFile: ArchiveFileSource;
|
||||
archiveType: ArchiveTypeSource;
|
||||
}
|
||||
|
||||
export interface Archive {
|
||||
location: string;
|
||||
type: ArchiveType;
|
||||
realFileSource: ArchiveFileSource;
|
||||
build?: BuildFragment;
|
||||
}
|
||||
|
||||
export async function getArchiveAsync(
|
||||
platform: AppPlatform,
|
||||
source: ArchiveSource
|
||||
): Promise<Archive> {
|
||||
const { location, realSource, build } = await getArchiveFileLocationAsync(source.archiveFile);
|
||||
const type = await getArchiveTypeAsync(platform, source.archiveType, location);
|
||||
return {
|
||||
location,
|
||||
type,
|
||||
realFileSource: realSource,
|
||||
build,
|
||||
};
|
||||
}
|
||||
|
||||
export { ArchiveFileSource, ArchiveTypeSourceType, ArchiveTypeSource, ArchiveFileSourceType };
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { ArchiveFileSource, ArchiveFileSourceType } from './archiveSource';
|
||||
import { AndroidSubmissionContext, IosSubmissionContext } from './types';
|
||||
import { ArchiveSource, ArchiveSourceType } from './ArchiveSource';
|
||||
import { SubmissionContext } from './types';
|
||||
|
||||
export function resolveArchiveFileSource(
|
||||
platform: AppPlatform,
|
||||
ctx: AndroidSubmissionContext | IosSubmissionContext,
|
||||
projectId: string
|
||||
): ArchiveFileSource {
|
||||
const { url, path, id, latest } = ctx.commandFlags;
|
||||
export function resolveArchiveSource<T extends Platform>(
|
||||
ctx: SubmissionContext<T>,
|
||||
platform: T
|
||||
): ArchiveSource {
|
||||
const { url, path, id, latest } = ctx.archiveFlags;
|
||||
const chosenOptions = [url, path, id, latest];
|
||||
if (chosenOptions.filter(opt => opt).length > 1) {
|
||||
throw new Error(`Pass only one of: --url, --path, --id, --latest`);
|
||||
@@ -17,44 +16,39 @@ export function resolveArchiveFileSource(
|
||||
|
||||
if (url) {
|
||||
return {
|
||||
sourceType: ArchiveFileSourceType.url,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url,
|
||||
projectId,
|
||||
platform,
|
||||
projectDir: ctx.projectDir,
|
||||
projectId: ctx.projectId,
|
||||
};
|
||||
} else if (path) {
|
||||
return {
|
||||
sourceType: ArchiveFileSourceType.path,
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
projectId,
|
||||
platform,
|
||||
projectDir: ctx.projectDir,
|
||||
projectId: ctx.projectId,
|
||||
};
|
||||
} else if (id) {
|
||||
if (!uuid.validate(id)) {
|
||||
throw new Error(`${id} is not an ID`);
|
||||
}
|
||||
return {
|
||||
sourceType: ArchiveFileSourceType.buildId,
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id,
|
||||
projectId,
|
||||
platform,
|
||||
projectDir: ctx.projectDir,
|
||||
projectId: ctx.projectId,
|
||||
};
|
||||
} else if (latest) {
|
||||
return {
|
||||
sourceType: ArchiveFileSourceType.latest,
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
platform,
|
||||
projectDir: ctx.projectDir,
|
||||
projectId,
|
||||
projectId: ctx.projectId,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
platform,
|
||||
projectDir: ctx.projectDir,
|
||||
projectId,
|
||||
projectId: ctx.projectId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { App, RequestContext, Session, User } from '@expo/apple-utils';
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
import Log from '../../log';
|
||||
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { IosSubmissionContext } from '../types';
|
||||
import { SubmissionContext } from '../types';
|
||||
import { sanitizeLanguage } from './utils/language';
|
||||
|
||||
interface CreateAppOptions {
|
||||
@@ -24,28 +25,21 @@ interface CreateAppOptions {
|
||||
}
|
||||
|
||||
type AppStoreResult = {
|
||||
appleId: string;
|
||||
ascAppId: string;
|
||||
appleIdUsername: string;
|
||||
ascAppIdentifier: string;
|
||||
};
|
||||
|
||||
export async function ensureAppStoreConnectAppExistsAsync(
|
||||
ctx: IosSubmissionContext
|
||||
ctx: SubmissionContext<Platform.IOS>
|
||||
): Promise<AppStoreResult> {
|
||||
const projectConfig = getConfig(ctx.projectDir, { skipSDKVersionRequirement: true });
|
||||
const { exp } = projectConfig;
|
||||
|
||||
const { bundleIdentifier, appName, language } = ctx.commandFlags;
|
||||
|
||||
// TODO:
|
||||
// - for builds from the database, read bundled identifier from metadata
|
||||
// - for builds uploaded from file system, prompt for the bundle identifier
|
||||
// this is necessary to make submit work outside the project directory
|
||||
const resolvedBundleId =
|
||||
bundleIdentifier ?? (await getBundleIdentifierAsync(ctx.projectDir, exp));
|
||||
const { appName, language } = ctx.profile;
|
||||
|
||||
const options = {
|
||||
...ctx.commandFlags,
|
||||
bundleIdentifier: resolvedBundleId,
|
||||
...ctx.profile,
|
||||
bundleIdentifier: await getBundleIdentifierAsync(ctx.projectDir, exp),
|
||||
appName: appName ?? exp.name ?? (await promptForAppNameAsync()),
|
||||
language: sanitizeLanguage(language),
|
||||
};
|
||||
@@ -125,8 +119,8 @@ async function createAppStoreConnectAppAsync(options: CreateAppOptions): Promise
|
||||
}
|
||||
|
||||
return {
|
||||
appleId: authCtx.appleId,
|
||||
ascAppId: app.id,
|
||||
appleIdUsername: authCtx.appleId,
|
||||
ascAppIdentifier: app.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface IosSubmissionConfig {
|
||||
projectId: string;
|
||||
archiveUrl: string;
|
||||
appleId: string;
|
||||
appSpecificPassword: string;
|
||||
appAppleId: string; // the name for ASC App ID
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { IosSubmitProfile } from '@expo/eas-json';
|
||||
import { Result, result } from '@expo/results';
|
||||
import chalk from 'chalk';
|
||||
import getenv from 'getenv';
|
||||
import wrapAnsi from 'wrap-ansi';
|
||||
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import { SubmissionFragment } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import UserSettings from '../../user/UserSettings';
|
||||
import { ArchiveSource, ArchiveTypeSourceType } from '../archiveSource';
|
||||
import { resolveArchiveFileSource } from '../commons';
|
||||
import { IosSubmissionContext, IosSubmitCommandFlags } from '../types';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext, SubmitArchiveFlags } from '../types';
|
||||
import { ensureAppStoreConnectAppExistsAsync } from './AppProduce';
|
||||
import {
|
||||
AppSpecificPasswordSource,
|
||||
@@ -17,24 +19,28 @@ import {
|
||||
} from './AppSpecificPasswordSource';
|
||||
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
||||
|
||||
class IosSubmitCommand {
|
||||
export default class IosSubmitCommand {
|
||||
static createContext({
|
||||
archiveFlags,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
}: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: IosSubmitProfile;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
commandFlags: IosSubmitCommandFlags;
|
||||
}): IosSubmissionContext {
|
||||
}): SubmissionContext<Platform.IOS> {
|
||||
return {
|
||||
archiveFlags,
|
||||
platform: Platform.IOS,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private ctx: IosSubmissionContext) {}
|
||||
constructor(private ctx: SubmissionContext<Platform.IOS>) {}
|
||||
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -44,7 +50,7 @@ class IosSubmitCommand {
|
||||
}
|
||||
|
||||
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
|
||||
const archiveSource = this.resolveArchiveSource(this.ctx.projectId);
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||
|
||||
const errored = [archiveSource, appSpecificPasswordSource].filter(r => !r.ok);
|
||||
@@ -54,12 +60,12 @@ class IosSubmitCommand {
|
||||
throw new Error('Failed to submit the app');
|
||||
}
|
||||
|
||||
const { appleId, ascAppId } = await this.getAppStoreInfoAsync();
|
||||
const { appleIdUsername, ascAppIdentifier } = await this.getAppStoreInfoAsync();
|
||||
|
||||
return {
|
||||
projectId: this.ctx.projectId,
|
||||
appleId,
|
||||
ascAppId,
|
||||
appleIdUsername,
|
||||
ascAppIdentifier,
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
appSpecificPasswordSource: appSpecificPasswordSource.enforceValue(),
|
||||
};
|
||||
@@ -80,11 +86,8 @@ class IosSubmitCommand {
|
||||
});
|
||||
}
|
||||
|
||||
private resolveArchiveSource(projectId: string): Result<ArchiveSource> {
|
||||
return result({
|
||||
archiveFile: resolveArchiveFileSource(AppPlatform.Ios, this.ctx, projectId),
|
||||
archiveType: { sourceType: ArchiveTypeSourceType.infer },
|
||||
});
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.IOS));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,25 +97,24 @@ class IosSubmitCommand {
|
||||
* - App Store Connect app ID (appAppleId)
|
||||
*/
|
||||
private async getAppStoreInfoAsync(): Promise<{
|
||||
appleId: string;
|
||||
ascAppId: string;
|
||||
appleIdUsername: string;
|
||||
ascAppIdentifier: string;
|
||||
}> {
|
||||
const { ascAppId } = this.ctx.commandFlags;
|
||||
const { ascAppId } = this.ctx.profile;
|
||||
|
||||
if (ascAppId) {
|
||||
return {
|
||||
appleId: await this.getAppleIdAsync(),
|
||||
ascAppId,
|
||||
appleIdUsername: await this.getAppleIdAsync(),
|
||||
ascAppIdentifier: ascAppId,
|
||||
};
|
||||
}
|
||||
|
||||
Log.log(
|
||||
wrapAnsi(
|
||||
chalk.italic(
|
||||
'Ensuring your app exists on App Store Connect. ' +
|
||||
`This step can be skipped by providing the --asc-app-id param. ${learnMore(
|
||||
'https://expo.fyi/asc-app-id'
|
||||
)}`
|
||||
`Ensuring your app exists on App Store Connect. This step can be skipped by providing ascAppId in the submit profile. ${learnMore(
|
||||
'https://expo.fyi/asc-app-id'
|
||||
)}`
|
||||
),
|
||||
process.stdout.columns || 80
|
||||
)
|
||||
@@ -127,7 +129,7 @@ class IosSubmitCommand {
|
||||
* and we just need apple ID
|
||||
*/
|
||||
private async getAppleIdAsync(): Promise<string> {
|
||||
const { appleId } = this.ctx.commandFlags;
|
||||
const { appleId } = this.ctx.profile;
|
||||
const envAppleId = getenv.string('EXPO_APPLE_ID', '');
|
||||
|
||||
if (appleId) {
|
||||
@@ -151,5 +153,3 @@ class IosSubmitCommand {
|
||||
return promptAppleId;
|
||||
}
|
||||
}
|
||||
|
||||
export default IosSubmitCommand;
|
||||
@@ -1,7 +1,9 @@
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { IosSubmissionContext } from '../types';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
|
||||
import {
|
||||
ArchiveSourceSummaryFields,
|
||||
formatArchiveSourceSummary,
|
||||
@@ -11,10 +13,10 @@ import {
|
||||
AppSpecificPasswordSource,
|
||||
getAppSpecificPasswordAsync,
|
||||
} from './AppSpecificPasswordSource';
|
||||
import { IosSubmissionConfig } from './IosSubmissionConfig';
|
||||
|
||||
export interface IosSubmissionOptions extends Pick<IosSubmissionConfig, 'projectId' | 'appleId'> {
|
||||
ascAppId: string;
|
||||
export interface IosSubmissionOptions
|
||||
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
|
||||
projectId: string;
|
||||
archiveSource: ArchiveSource;
|
||||
appSpecificPasswordSource: AppSpecificPasswordSource;
|
||||
}
|
||||
@@ -24,14 +26,7 @@ interface ResolvedSourceOptions {
|
||||
appSpecificPassword: string;
|
||||
}
|
||||
|
||||
type SummaryData = Pick<IosSubmissionOptions, 'ascAppId' | 'appleId' | 'projectId'> &
|
||||
ArchiveSourceSummaryFields;
|
||||
|
||||
class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOptions> {
|
||||
constructor(ctx: IosSubmissionContext, options: IosSubmissionOptions) {
|
||||
super(AppPlatform.Ios, ctx, options);
|
||||
}
|
||||
|
||||
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptions();
|
||||
const submissionConfig = await this.formatSubmissionConfigAsync(
|
||||
@@ -41,18 +36,30 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
SummaryHumanReadableKeys
|
||||
);
|
||||
|
||||
return await this.createSubmissionAsync(
|
||||
return await this.createSubmissionAsync({
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id
|
||||
);
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
});
|
||||
}
|
||||
|
||||
protected async createPlatformSubmissionAsync({
|
||||
projectId,
|
||||
submissionConfig,
|
||||
buildId,
|
||||
}: SubmissionInput<Platform.IOS>): Promise<SubmissionFragment> {
|
||||
return await SubmissionMutation.createIosSubmissionAsync({
|
||||
appId: projectId,
|
||||
config: submissionConfig,
|
||||
submittedBuildId: buildId,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveSourceOptions(): Promise<ResolvedSourceOptions> {
|
||||
const archive = await getArchiveAsync(AppPlatform.Ios, this.options.archiveSource);
|
||||
const archive = await getArchiveAsync(this.options.archiveSource);
|
||||
const appSpecificPassword = await getAppSpecificPasswordAsync(
|
||||
this.options.appSpecificPasswordSource
|
||||
);
|
||||
@@ -66,43 +73,43 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
private async formatSubmissionConfigAsync(
|
||||
options: IosSubmissionOptions,
|
||||
{ archive, appSpecificPassword }: ResolvedSourceOptions
|
||||
): Promise<IosSubmissionConfig> {
|
||||
const { projectId, appleId, ascAppId } = options;
|
||||
const submissionConfig = {
|
||||
appAppleId: ascAppId, //ASC App ID is called "appAppleId" on server side
|
||||
appleId,
|
||||
projectId,
|
||||
archiveUrl: archive.location,
|
||||
appSpecificPassword,
|
||||
): Promise<IosSubmissionConfigInput> {
|
||||
const { appleIdUsername, ascAppIdentifier } = options;
|
||||
return {
|
||||
ascAppIdentifier,
|
||||
appleIdUsername,
|
||||
archiveUrl: archive.url,
|
||||
appleAppSpecificPassword: appSpecificPassword,
|
||||
};
|
||||
return submissionConfig;
|
||||
}
|
||||
|
||||
private prepareSummaryData(
|
||||
options: IosSubmissionOptions,
|
||||
{ archive }: ResolvedSourceOptions
|
||||
): SummaryData {
|
||||
const { appleId, ascAppId, projectId } = options;
|
||||
const { appleIdUsername, ascAppIdentifier, projectId } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
ascAppId,
|
||||
appleId,
|
||||
ascAppIdentifier,
|
||||
appleIdUsername,
|
||||
projectId,
|
||||
...formatArchiveSourceSummary(archive),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type SummaryData = {
|
||||
ascAppIdentifier: string;
|
||||
appleIdUsername: string;
|
||||
projectId: string;
|
||||
} & ArchiveSourceSummaryFields;
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
|
||||
ascAppId: 'ASC App ID',
|
||||
appleId: 'Apple ID',
|
||||
ascAppIdentifier: 'ASC App ID',
|
||||
appleIdUsername: 'Apple ID',
|
||||
projectId: 'Project ID',
|
||||
archiveUrl: 'Archive URL',
|
||||
archivePath: 'Archive Path',
|
||||
formattedBuild: 'Build',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof SummaryData, Function>> = {};
|
||||
|
||||
export default IosSubmitter;
|
||||
@@ -3,19 +3,16 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppPlatform } from '../../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import { IosSubmitCommandFlags } from '../../types';
|
||||
import { IosSubmissionConfig } from '../IosSubmissionConfig';
|
||||
import IosSubmitCommand from '../IosSubmitCommand';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('ora');
|
||||
jest.mock('../../../graphql/mutations/SubmissionMutation', () => ({
|
||||
SubmissionMutation: {
|
||||
createSubmissionAsync: jest.fn(),
|
||||
createIosSubmissionAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
@@ -60,32 +57,29 @@ describe(IosSubmitCommand, () => {
|
||||
|
||||
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
|
||||
|
||||
const commandFlags: IosSubmitCommandFlags = {
|
||||
latest: false,
|
||||
url: 'http://expo.dev/fake.ipa',
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '12345678',
|
||||
};
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
commandFlags,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.ipa',
|
||||
},
|
||||
profile: {
|
||||
language: 'en-US',
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '12345678',
|
||||
},
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const iosSubmissionConfig: IosSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.dev/fake.ipa',
|
||||
appleId: '[email protected]',
|
||||
appSpecificPassword: 'supersecret',
|
||||
appAppleId: '12345678',
|
||||
projectId,
|
||||
};
|
||||
|
||||
expect(SubmissionMutation.createSubmissionAsync).toHaveBeenCalledWith({
|
||||
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: iosSubmissionConfig,
|
||||
platform: AppPlatform.Ios,
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.ipa',
|
||||
appleIdUsername: '[email protected]',
|
||||
appleAppSpecificPassword: 'supersecret',
|
||||
ascAppIdentifier: '12345678',
|
||||
},
|
||||
});
|
||||
|
||||
delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
|
||||
|
||||
@@ -1,55 +1,17 @@
|
||||
export interface BaseSubmissionContext {
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
|
||||
export interface SubmissionContext<T extends Platform> {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
platform: T;
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export interface SubmissionContext<T extends SubmitCommandFlags> extends BaseSubmissionContext {
|
||||
commandFlags: T;
|
||||
}
|
||||
|
||||
export interface SubmitCommandFlags {
|
||||
export interface SubmitArchiveFlags {
|
||||
latest?: boolean;
|
||||
id?: string;
|
||||
path?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export enum AndroidArchiveType {
|
||||
apk = 'apk',
|
||||
aab = 'aab',
|
||||
}
|
||||
|
||||
export enum IosArchiveType {
|
||||
ipa = 'ipa',
|
||||
}
|
||||
|
||||
export type ArchiveType = AndroidArchiveType | IosArchiveType;
|
||||
|
||||
// Android specific types
|
||||
export interface AndroidSubmitCommandFlags extends SubmitCommandFlags {
|
||||
type?: 'apk' | 'aab';
|
||||
serviceAccountKeyPath?: string;
|
||||
androidPackage?: string;
|
||||
track: string;
|
||||
releaseStatus: string;
|
||||
changesNotSentForReview: boolean;
|
||||
}
|
||||
|
||||
export type AndroidSubmissionContext = SubmissionContext<AndroidSubmitCommandFlags>;
|
||||
|
||||
// iOS specific types
|
||||
export interface IosSubmitCommandFlags extends SubmitCommandFlags {
|
||||
// common for all flows
|
||||
appleId?: string;
|
||||
ascAppId?: string;
|
||||
|
||||
// used only when running produce
|
||||
appleTeamId?: string;
|
||||
appName?: string;
|
||||
bundleIdentifier?: string;
|
||||
sku?: string;
|
||||
language?: string;
|
||||
companyName?: string;
|
||||
}
|
||||
|
||||
export type IosSubmissionContext = SubmissionContext<IosSubmitCommandFlags>;
|
||||
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveFileSourceType } from '../archiveSource';
|
||||
import { Archive, ArchiveSourceType } from '../ArchiveSource';
|
||||
export interface ArchiveSourceSummaryFields {
|
||||
archiveUrl?: string;
|
||||
archivePath?: string;
|
||||
@@ -43,36 +43,28 @@ function formatSubmissionBuildSummary(build: BuildFragment) {
|
||||
);
|
||||
}
|
||||
|
||||
export function formatArchiveSourceSummary({
|
||||
realFileSource,
|
||||
build,
|
||||
}: Archive): ArchiveSourceSummaryFields {
|
||||
export function formatArchiveSourceSummary({ source, build }: Archive): ArchiveSourceSummaryFields {
|
||||
const summarySlice: ArchiveSourceSummaryFields = {};
|
||||
|
||||
switch (realFileSource.sourceType) {
|
||||
case ArchiveFileSourceType.path:
|
||||
summarySlice.archivePath = realFileSource.path;
|
||||
switch (source.sourceType) {
|
||||
case ArchiveSourceType.path:
|
||||
summarySlice.archivePath = source.path;
|
||||
break;
|
||||
case ArchiveFileSourceType.url:
|
||||
summarySlice.archiveUrl = realFileSource.url;
|
||||
case ArchiveSourceType.url:
|
||||
summarySlice.archiveUrl = source.url;
|
||||
break;
|
||||
case ArchiveFileSourceType.buildId:
|
||||
case ArchiveFileSourceType.latest:
|
||||
case ArchiveSourceType.buildId:
|
||||
case ArchiveSourceType.latest:
|
||||
summarySlice.formattedBuild = formatSubmissionBuildSummary(build!);
|
||||
break;
|
||||
}
|
||||
return summarySlice;
|
||||
}
|
||||
|
||||
export function printSummary<T>(
|
||||
summary: T,
|
||||
keyMap: Record<keyof T, string>,
|
||||
valueRemap: Partial<Record<keyof T, Function>>
|
||||
): void {
|
||||
export function printSummary<T>(summary: T, keyMap: Record<keyof T, string>): void {
|
||||
const fields = [];
|
||||
for (const [key, entryValue] of Object.entries(summary)) {
|
||||
for (const [key, value] of Object.entries(summary)) {
|
||||
const label = `${keyMap[key as keyof T]}:`;
|
||||
const value = valueRemap[key as keyof T]?.(entryValue) ?? entryValue;
|
||||
fields.push({ label, value });
|
||||
}
|
||||
|
||||
|
||||
@@ -207,14 +207,19 @@ async function isGitCaseSensitiveAsync(): Promise<boolean | undefined> {
|
||||
if (process.platform !== 'darwin') {
|
||||
return undefined;
|
||||
}
|
||||
const result = await spawnAsync('git', ['config', '--get', 'core.ignorecase']);
|
||||
const isIgnoreCaseEnabled = result.stdout.trim();
|
||||
if (isIgnoreCaseEnabled === '') {
|
||||
|
||||
try {
|
||||
const result = await spawnAsync('git', ['config', '--get', 'core.ignorecase']);
|
||||
const isIgnoreCaseEnabled = result.stdout.trim();
|
||||
if (isIgnoreCaseEnabled === '') {
|
||||
return undefined;
|
||||
} else if (isIgnoreCaseEnabled === 'true') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
} else if (isIgnoreCaseEnabled === 'true') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with the eas.json",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,11 +4,17 @@ import path from 'path';
|
||||
|
||||
import { BuildProfile } from './EasBuild.types';
|
||||
import { CredentialsSource, EasJson, RawBuildProfile } from './EasJson.types';
|
||||
import { EasJsonSchema, MinimalEasJsonSchema } from './EasJsonSchema';
|
||||
import {
|
||||
AndroidSubmitProfileSchema,
|
||||
EasJsonSchema,
|
||||
IosSubmitProfileSchema,
|
||||
MinimalEasJsonSchema,
|
||||
} from './EasJsonSchema';
|
||||
import { SubmitProfile } from './EasSubmit.types';
|
||||
|
||||
interface EasJsonPreValidation {
|
||||
build: { [profile: string]: object };
|
||||
submit?: { [profile: string]: object };
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
@@ -28,29 +34,32 @@ export class EasJsonReader {
|
||||
return Object.keys(easJson?.build ?? {});
|
||||
}
|
||||
|
||||
public async readSubmitProfileAsync<T extends Platform>(
|
||||
profileName: string,
|
||||
platform: T
|
||||
): Promise<SubmitProfile<T>> {
|
||||
const easJson = await this.readAndValidateAsync();
|
||||
const profile = easJson?.submit?.[profileName]?.[platform];
|
||||
if (!profile) {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json for ${platform}.`);
|
||||
public async getSubmitProfileNamesAsync({ throwIfEasJsonDoesNotExist = true } = {}): Promise<
|
||||
string[]
|
||||
> {
|
||||
try {
|
||||
const easJson = await this.readRawAsync();
|
||||
return Object.keys(easJson?.submit ?? {});
|
||||
} catch (err: any) {
|
||||
if (!throwIfEasJsonDoesNotExist && err.code === 'ENOENT') {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return profile as SubmitProfile<T>;
|
||||
}
|
||||
|
||||
public async readBuildProfileAsync<T extends Platform>(
|
||||
buildProfileName: string,
|
||||
platform: T
|
||||
platform: T,
|
||||
profileName: string
|
||||
): Promise<BuildProfile<T>> {
|
||||
const easJson = await this.readAndValidateAsync();
|
||||
this.ensureBuildProfileExists(easJson, buildProfileName);
|
||||
this.ensureBuildProfileExists(easJson, profileName);
|
||||
const {
|
||||
android: resolvedAndroidSpecificValues,
|
||||
ios: resolvedIosSpecificValues,
|
||||
...resolvedProfile
|
||||
} = this.resolveBuildProfile(easJson, buildProfileName);
|
||||
} = this.resolveBuildProfile(easJson, profileName);
|
||||
if (platform === Platform.ANDROID) {
|
||||
const profileWithoutDefaults = profileMerge(
|
||||
resolvedProfile,
|
||||
@@ -65,6 +74,28 @@ export class EasJsonReader {
|
||||
}
|
||||
}
|
||||
|
||||
public async readSubmitProfileAsync<T extends Platform>(
|
||||
platform: T,
|
||||
profileName?: string
|
||||
): Promise<SubmitProfile<T>> {
|
||||
if (!profileName) {
|
||||
const profileNames = await this.getSubmitProfileNamesAsync({
|
||||
throwIfEasJsonDoesNotExist: false,
|
||||
});
|
||||
if (profileNames.includes('release')) {
|
||||
return await this.readSubmitProfileAsync(platform, 'release');
|
||||
} else {
|
||||
return getDefaultSubmitProfile(platform);
|
||||
}
|
||||
}
|
||||
const easJson = await this.readAndValidateAsync();
|
||||
const profile = easJson?.submit?.[profileName]?.[platform];
|
||||
if (!profile) {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json for ${platform}.`);
|
||||
}
|
||||
return profile as SubmitProfile<T>;
|
||||
}
|
||||
|
||||
public async readAndValidateAsync(): Promise<EasJson> {
|
||||
const easJson = await this.readRawAsync();
|
||||
const { value, error } = EasJsonSchema.validate(easJson, {
|
||||
@@ -83,10 +114,7 @@ export class EasJsonReader {
|
||||
const rawFile = await fs.readFile(EasJsonReader.formatEasJsonPath(this.projectDir), 'utf8');
|
||||
const json = JSON.parse(rawFile);
|
||||
|
||||
const { value, error } = MinimalEasJsonSchema.validate(json, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
const { value, error } = MinimalEasJsonSchema.validate(json, { abortEarly: false });
|
||||
if (error) {
|
||||
throw new Error(`eas.json is not valid [${error.toString()}]`);
|
||||
}
|
||||
@@ -144,3 +172,9 @@ export function profileMerge(base: RawBuildProfile, update: RawBuildProfile): Ra
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getDefaultSubmitProfile<T extends Platform>(platform: T): SubmitProfile<T> {
|
||||
const Schema =
|
||||
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
|
||||
return Schema.validate({}, { convert: true }).value;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Android, Ios } from '@expo/eas-build-job';
|
||||
import Joi from 'joi';
|
||||
|
||||
import { ReleaseStatus, ReleaseTrack } from './EasSubmit.types';
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from './EasSubmit.types';
|
||||
|
||||
const semverSchemaCheck = (value: any) => {
|
||||
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {
|
||||
@@ -74,23 +74,25 @@ const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
})
|
||||
);
|
||||
|
||||
const AndroidSubmitProfileSchema = Joi.object({
|
||||
export const AndroidSubmitProfileSchema = Joi.object({
|
||||
serviceAccountKeyPath: Joi.string(),
|
||||
track: Joi.string().valid(...Object.values(ReleaseTrack)),
|
||||
releaseStatus: Joi.string().valid(...Object.values(ReleaseStatus)),
|
||||
changesNotSentForReview: Joi.boolean(),
|
||||
verbose: Joi.boolean(),
|
||||
track: Joi.string()
|
||||
.valid(...Object.values(AndroidReleaseTrack))
|
||||
.default(AndroidReleaseTrack.internal),
|
||||
releaseStatus: Joi.string()
|
||||
.valid(...Object.values(AndroidReleaseStatus))
|
||||
.default(AndroidReleaseStatus.completed),
|
||||
changesNotSentForReview: Joi.boolean().default(false),
|
||||
});
|
||||
|
||||
const IosSubmitProfileSchema = Joi.object({
|
||||
export const IosSubmitProfileSchema = Joi.object({
|
||||
appleId: Joi.string(),
|
||||
ascAppId: Joi.string(),
|
||||
|
||||
appleTeamId: Joi.string(),
|
||||
sku: Joi.string(),
|
||||
language: Joi.string(),
|
||||
language: Joi.string().default('en-US'),
|
||||
companyName: Joi.string(),
|
||||
verbose: Joi.boolean(),
|
||||
appName: Joi.string(),
|
||||
});
|
||||
|
||||
const EasJsonSubmitConfigurationSchema = Joi.object({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export enum ReleaseStatus {
|
||||
export enum AndroidReleaseStatus {
|
||||
completed = 'completed',
|
||||
draft = 'draft',
|
||||
halted = 'halted',
|
||||
inProgress = 'inProgress',
|
||||
}
|
||||
|
||||
export enum ReleaseTrack {
|
||||
export enum AndroidReleaseTrack {
|
||||
production = 'production',
|
||||
beta = 'beta',
|
||||
alpha = 'alpha',
|
||||
@@ -16,21 +16,19 @@ export enum ReleaseTrack {
|
||||
|
||||
export interface AndroidSubmitProfile {
|
||||
serviceAccountKeyPath?: string;
|
||||
track?: ReleaseTrack;
|
||||
releaseStatus?: ReleaseStatus;
|
||||
changesNotSentForReview?: boolean;
|
||||
verbose?: boolean;
|
||||
track: AndroidReleaseTrack;
|
||||
releaseStatus: AndroidReleaseStatus;
|
||||
changesNotSentForReview: boolean;
|
||||
}
|
||||
|
||||
export interface IosSubmitProfile {
|
||||
appleId?: string;
|
||||
ascAppId?: string;
|
||||
|
||||
appleTeamId?: string;
|
||||
sku?: string;
|
||||
language?: string;
|
||||
language: string;
|
||||
companyName?: string;
|
||||
verbose?: boolean;
|
||||
appName?: string;
|
||||
}
|
||||
|
||||
export type SubmitProfile<TPlatform extends Platform = Platform> =
|
||||
|
||||
@@ -19,8 +19,8 @@ test('minimal valid eas.json for both platforms', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readBuildProfileAsync('release', Platform.IOS);
|
||||
const androidProfile = await reader.readBuildProfileAsync('release', Platform.ANDROID);
|
||||
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'release');
|
||||
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'release');
|
||||
|
||||
expect({
|
||||
distribution: 'store',
|
||||
@@ -47,8 +47,8 @@ test('valid eas.json for development client builds', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readBuildProfileAsync('debug', Platform.IOS);
|
||||
const androidProfile = await reader.readBuildProfileAsync('debug', Platform.ANDROID);
|
||||
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'debug');
|
||||
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
expect({
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
@@ -73,7 +73,7 @@ test('valid profile for internal distribution on Android', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const profile = await reader.readBuildProfileAsync('internal', Platform.ANDROID);
|
||||
const profile = await reader.readBuildProfileAsync(Platform.ANDROID, 'internal');
|
||||
expect({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
@@ -95,8 +95,8 @@ test('valid profile extending other profile', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
|
||||
const extendedProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
|
||||
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
expect({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
@@ -134,9 +134,9 @@ test('valid profile extending other profile with platform specific envs', async
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
|
||||
const extendedAndroidProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
|
||||
const extendedIosProfile = await reader.readBuildProfileAsync('extension', Platform.IOS);
|
||||
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
const extendedIosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'extension');
|
||||
expect({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
@@ -188,9 +188,9 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
|
||||
const extendedAndroidProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
|
||||
const extendedIosProfile = await reader.readBuildProfileAsync('extension', Platform.IOS);
|
||||
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
const extendedIosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'extension');
|
||||
expect({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
@@ -224,7 +224,7 @@ test('valid eas.json with missing profile', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync('debug', Platform.ANDROID);
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
await expect(promise).rejects.toThrowError('There is no profile named debug in eas.json.');
|
||||
});
|
||||
|
||||
@@ -236,7 +236,7 @@ test('invalid eas.json when using wrong buildType', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'release');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'eas.json is not valid [ValidationError: "build.release.android.buildType" must be one of [apk, app-bundle]]'
|
||||
);
|
||||
@@ -246,7 +246,7 @@ test('empty json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'release');
|
||||
await expect(promise).rejects.toThrowError('There is no profile named release in eas.json.');
|
||||
});
|
||||
|
||||
@@ -258,7 +258,7 @@ test('invalid semver value', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'release');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'eas.json is not valid [ValidationError: "build.release.node" failed custom validation because 12.0.0-alpha is not a valid version]'
|
||||
);
|
||||
|
||||
@@ -22,11 +22,17 @@ test('minimal allowed eas.json for both platforms', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readSubmitProfileAsync('release', Platform.IOS);
|
||||
const androidProfile = await reader.readSubmitProfileAsync('release', Platform.ANDROID);
|
||||
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
|
||||
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'release');
|
||||
|
||||
expect(androidProfile).toEqual({});
|
||||
expect(iosProfile).toEqual({});
|
||||
expect(androidProfile).toEqual({
|
||||
changesNotSentForReview: false,
|
||||
releaseStatus: 'completed',
|
||||
track: 'internal',
|
||||
});
|
||||
expect(iosProfile).toEqual({
|
||||
language: 'en-US',
|
||||
});
|
||||
});
|
||||
|
||||
test('android config with all required values', async () => {
|
||||
@@ -43,12 +49,13 @@ test('android config with all required values', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const androidProfile = await reader.readSubmitProfileAsync('release', Platform.ANDROID);
|
||||
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'release');
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
serviceAccountKeyPath: './path.json',
|
||||
track: 'beta',
|
||||
releaseStatus: 'completed',
|
||||
changesNotSentForReview: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,12 +73,13 @@ test('ios config with all required values', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readSubmitProfileAsync('release', Platform.IOS);
|
||||
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
appleId: '[email protected]',
|
||||
appleTeamId: 'QWERTY',
|
||||
ascAppId: '1223423523',
|
||||
language: 'en-US',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +97,7 @@ test('missing ios profile', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readSubmitProfileAsync('release', Platform.IOS);
|
||||
const promise = reader.readSubmitProfileAsync(Platform.IOS, 'release');
|
||||
|
||||
expect(promise).rejects.toThrow('There is no profile named release in eas.json for ios.');
|
||||
});
|
||||
@@ -1,4 +1,10 @@
|
||||
export { AndroidSubmitProfile, IosSubmitProfile } from './EasSubmit.types';
|
||||
export {
|
||||
AndroidSubmitProfile,
|
||||
AndroidReleaseStatus,
|
||||
AndroidReleaseTrack,
|
||||
IosSubmitProfile,
|
||||
SubmitProfile,
|
||||
} from './EasSubmit.types';
|
||||
export { EasJson } from './EasJson.types';
|
||||
export {
|
||||
AndroidBuildProfile,
|
||||
|
||||
@@ -79,27 +79,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176"
|
||||
integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==
|
||||
|
||||
"@babel/[email protected]", "@babel/core@^7.7.2":
|
||||
version "7.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8"
|
||||
integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.5"
|
||||
"@babel/generator" "^7.15.0"
|
||||
"@babel/helper-compilation-targets" "^7.15.0"
|
||||
"@babel/helper-module-transforms" "^7.15.0"
|
||||
"@babel/helpers" "^7.14.8"
|
||||
"@babel/parser" "^7.15.0"
|
||||
"@babel/template" "^7.14.5"
|
||||
"@babel/traverse" "^7.15.0"
|
||||
"@babel/types" "^7.15.0"
|
||||
convert-source-map "^1.7.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
json5 "^2.1.2"
|
||||
semver "^6.3.0"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/[email protected]":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
|
||||
@@ -144,6 +123,27 @@
|
||||
semver "^5.4.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/core@^7.7.2":
|
||||
version "7.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8"
|
||||
integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.5"
|
||||
"@babel/generator" "^7.15.0"
|
||||
"@babel/helper-compilation-targets" "^7.15.0"
|
||||
"@babel/helper-module-transforms" "^7.15.0"
|
||||
"@babel/helpers" "^7.14.8"
|
||||
"@babel/parser" "^7.15.0"
|
||||
"@babel/template" "^7.14.5"
|
||||
"@babel/traverse" "^7.15.0"
|
||||
"@babel/types" "^7.15.0"
|
||||
convert-source-map "^1.7.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
json5 "^2.1.2"
|
||||
semver "^6.3.0"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/eslint-parser@^7.11.5":
|
||||
version "7.14.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.14.7.tgz#91be59a4f7dd60d02a3ef772d156976465596bda"
|
||||
@@ -1668,20 +1668,22 @@
|
||||
xcode "^3.0.1"
|
||||
xml2js "^0.4.23"
|
||||
|
||||
"@expo/config-plugins@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-3.0.3.tgz#4aff484fd2adedbd12a1cd73daabf07b1a5af0e5"
|
||||
integrity sha512-+DWHd/2L3O+njVercxGSzcBUXMm5CSzdvIK+7dU+FRhoiF57y2NzBhfNQcReYx/EY1coBrYOsMtENPhV1tWoWQ==
|
||||
"@expo/config-plugins@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-3.1.0.tgz#0752ff33c5eab21cf42034a44e79df97f0f867f8"
|
||||
integrity sha512-V5qxaxCAExBM0TXmbU1QKiZcAGP3ecu7KXede8vByT15cro5PkcWu2sSdJCYbHQ/gw6Vf/i8sr8gKlN8V8TSLg==
|
||||
dependencies:
|
||||
"@expo/config-types" "^41.0.0"
|
||||
"@expo/json-file" "8.2.30"
|
||||
"@expo/plist" "0.0.13"
|
||||
"@expo/config-types" "^42.0.0"
|
||||
"@expo/json-file" "8.2.33"
|
||||
"@expo/plist" "0.0.14"
|
||||
chalk "^4.1.2"
|
||||
debug "^4.3.1"
|
||||
find-up "~5.0.0"
|
||||
fs-extra "9.0.0"
|
||||
getenv "^1.0.0"
|
||||
glob "7.1.6"
|
||||
resolve-from "^5.0.0"
|
||||
semver "^7.3.5"
|
||||
slash "^3.0.0"
|
||||
xcode "^3.0.1"
|
||||
xml2js "^0.4.23"
|
||||
@@ -1691,10 +1693,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-40.0.0-beta.2.tgz#4fea4ef5654d02218b02b0b3772529a9ce5b0471"
|
||||
integrity sha512-t9pHCQMXOP4nwd7LGXuHkLlFy0JdfknRSCAeVF4Kw2/y+5OBbR9hW9ZVnetpBf0kORrekgiI7K/qDaa3hh5+Qg==
|
||||
|
||||
"@expo/config-types@^41.0.0":
|
||||
version "41.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-41.0.0.tgz#ffe1444c6c26e0e3a8f7149b4afe486e357536d1"
|
||||
integrity sha512-Ax0pHuY5OQaSrzplOkT9DdpdmNzaVDnq9VySb4Ujq7UJ4U4jriLy8u93W98zunOXpcu0iiKubPsqD6lCiq0pig==
|
||||
"@expo/config-types@^42.0.0":
|
||||
version "42.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-42.0.0.tgz#3e3e125ec092c0c34dbfaf19be5480402de3d677"
|
||||
integrity sha512-Rj02OMZke2MrGa/1Y/EScmR7VuWbDEHPJyvfFyyLbadUt+Yv6isCdeFzDt71I7gJlPR9T4fzixeYLrtXXOTq0w==
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "3.3.42"
|
||||
@@ -1775,6 +1777,15 @@
|
||||
json5 "^1.0.1"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "8.2.33"
|
||||
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.33.tgz#78f56f33a2cfb807b23c81e00237a33159aa1f32"
|
||||
integrity sha512-CDnhjdirUs6OdN5hOSTJ2y3i9EiJMk7Z5iDljC5xyCHCrUex7oyI8vbRsZEojAahxZccgL/PrO+CjakiFFWurg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "~7.10.4"
|
||||
json5 "^1.0.1"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.24.0-expo"
|
||||
resolved "https://registry.yarnpkg.com/@expo/oclif-dev-cli/-/oclif-dev-cli-1.24.0-expo.tgz#4e1f766e79b4d99e3da29318a577952793b143bf"
|
||||
@@ -3877,6 +3888,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
|
||||
integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
|
||||
|
||||
"@types/[email protected]":
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/envinfo/-/envinfo-7.8.1.tgz#1915df82c16d637e92146645c70db9360eb099c6"
|
||||
integrity sha512-pTyshpmGxqB9lRwG75v2YR0oqKYpCrklOYlZWQ88z/JB0fimT8EVmYekuIwpU3IxPZDHSXCqXKzkCrtAcKY25g==
|
||||
|
||||
"@types/form-data@^2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.5.0.tgz#5025f7433016f923348434c40006d9a797c1b0e8"
|
||||
@@ -4581,7 +4597,14 @@ axios-retry@^3.0.2:
|
||||
dependencies:
|
||||
is-retry-allowed "^1.1.0"
|
||||
|
||||
[email protected].1, axios@^0.21.1:
|
||||
[email protected].4:
|
||||
version "0.21.4"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
||||
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.0"
|
||||
|
||||
axios@^0.21.1:
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||
@@ -4996,7 +5019,7 @@ caseless@~0.12.0:
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
||||
|
||||
[email protected]:
|
||||
[email protected], chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -6006,7 +6029,7 @@ [email protected], env-paths@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
|
||||
|
||||
envinfo@^7.7.4:
|
||||
[email protected], envinfo@^7.7.4:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
|
||||
integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
|
||||
@@ -6658,6 +6681,11 @@ follow-redirects@^1.10.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e"
|
||||
integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||
|
||||
Reference in new issue
Block a user