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 | ||
|
|
5bc14b5b30 | ||
|
|
bee98ce25e | ||
|
|
0e6a5315ab | ||
|
|
91b51528ef | ||
|
|
5e3962afc2 | ||
|
|
f07c52d2fb | ||
|
|
50846a1a96 | ||
|
|
4cb726f374 | ||
|
|
da9d1b4984 | ||
|
|
64ad52dc45 | ||
|
|
6169e5ff5b | ||
|
|
ef33973f7f |
No files matched your search
+3
-2
@@ -10,7 +10,7 @@ module.exports = {
|
||||
jasmine: false,
|
||||
},
|
||||
rules: {
|
||||
'no-console':'warn',
|
||||
'no-console': 'warn',
|
||||
'no-constant-condition': ['warn', { checkLoops: false }],
|
||||
'sort-imports': [
|
||||
'warn',
|
||||
@@ -19,7 +19,8 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
'curly': 'warn',
|
||||
curly: 'warn',
|
||||
'import/no-relative-packages': 'error',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -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'
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: sudo apt-get install nsis
|
||||
- run: yarn build
|
||||
@@ -112,4 +112,4 @@ jobs:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win/${{ steps.x86.outputs.filename }}
|
||||
asset_name: ${{ steps.x86.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
asset_content_type: application/gzip
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
build-script: 'build:tarballs:linux'
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['12', '14']
|
||||
node: ['12', '14', '16']
|
||||
include:
|
||||
- node: '14'
|
||||
- node: '16'
|
||||
coverage: true
|
||||
name: Test with Node ${{ matrix.node }}
|
||||
steps:
|
||||
|
||||
+36
-3
@@ -6,14 +6,47 @@ 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
|
||||
|
||||
- Support `--no-wait` in `eas submit`. ([#578](https://github.com/expo/eas-cli/pull/578) by [@dsokal](https://github.com/dsokal))
|
||||
- Detect changes when `core.ignorecase` is set to true. ([#570](https://github.com/expo/eas-cli/pull/570) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 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 detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace `@hapi/joi` with `joi`. Upgrade typescript to 4.4.2. Upgrade dependencies. ([#582](https://github.com/expo/eas-cli/pull/582) by [@dsokal](https://github.com/dsokal))
|
||||
- Change typescript target from `ES2017` to `ES2019`. ([#584](https://github.com/expo/eas-cli/pull/584) by [@dsokal](https://github.com/dsokal))
|
||||
- Use `ts-jest` instead of `babel-jest`. ([#585](https://github.com/expo/eas-cli/pull/585) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.24.1](https://github.com/expo/eas-cli/releases/tag/v0.24.1) - 2021-08-25
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix wrong `EasJsonReader` import causing `eas submit` to fail. ([951ee](https://github.com/expo/eas-cli/commit/951eea2396591bd612aa7cb3038da35e45c33bb8) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.24.0](https://github.com/expo/eas-cli/releases/tag/v0.24.0) - 2021-08-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
@@ -30,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,6 +0,0 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['@expo/babel-preset-cli'],
|
||||
};
|
||||
};
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': ['babel-jest', { configFile: require.resolve('./babel.config.js') }],
|
||||
},
|
||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.24.1"
|
||||
"version": "0.26.0"
|
||||
}
|
||||
+14
-8
@@ -10,19 +10,25 @@
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint": "eslint . --ext .ts --ext .js",
|
||||
"release": "lerna version",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "^26.0.13",
|
||||
"eslint": "7.31.0",
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/node": "16.7.8",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-universe": "7.0.1",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"jest": "^26.4.2",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
"lerna": "^3.22.1",
|
||||
"prettier": "2.3.2"
|
||||
"jest": "27.1.0",
|
||||
"jest-watch-typeahead": "0.6.4",
|
||||
"lerna": "4.0.0",
|
||||
"prettier": "2.3.2",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.8.0",
|
||||
"yarn": "1.22.11"
|
||||
}
|
||||
}
|
||||
+56
-104
@@ -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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_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]`
|
||||
|
||||
@@ -283,7 +295,7 @@ OPTIONS
|
||||
--all see all commands in CLI
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -294,7 +306,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_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,94 +365,34 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_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
|
||||
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.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_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`
|
||||
|
||||
@@ -459,7 +411,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_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]`
|
||||
|
||||
@@ -473,7 +425,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_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`
|
||||
|
||||
@@ -487,7 +439,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_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`
|
||||
|
||||
@@ -507,7 +459,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_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`
|
||||
|
||||
@@ -521,5 +473,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.24.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_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 -->
|
||||
@@ -6,4 +6,7 @@ if (process.env.TMPDIR) {
|
||||
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
|
||||
}
|
||||
|
||||
// fs-extra@10 is not compatible with memfs, the following line fixes tests
|
||||
(fs.realpath as any).native = jest.fn();
|
||||
|
||||
module.exports = fs;
|
||||
Generated
+2018
-665
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.24.1",
|
||||
"version": "0.26.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -12,99 +12,93 @@
|
||||
"@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/eas-build-job": "0.2.45",
|
||||
"@expo/eas-json": "^0.24.0",
|
||||
"@expo/config-plugins": "3.1.0",
|
||||
"@expo/eas-build-job": "0.2.46",
|
||||
"@expo/eas-json": "^0.26.0",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.11",
|
||||
"@expo/plist": "0.0.14",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.0.7",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@hapi/joi": "17.1.1",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/errors": "1.3.3",
|
||||
"@oclif/plugin-help": "3.2.0",
|
||||
"@urql/core": "2.0.0",
|
||||
"@urql/exchange-retry": "0.2.0",
|
||||
"chalk": "4.1.0",
|
||||
"@oclif/errors": "1.3.5",
|
||||
"@oclif/plugin-help": "3.2.3",
|
||||
"@urql/core": "2.3.1",
|
||||
"@urql/exchange-retry": "0.3.0",
|
||||
"chalk": "4.1.2",
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "3.0.3",
|
||||
"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",
|
||||
"fs-extra": "9.0.1",
|
||||
"fs-extra": "10.0.0",
|
||||
"getenv": "1.0.0",
|
||||
"got": "11.5.2",
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"got": "11.8.2",
|
||||
"graphql": "15.5.1",
|
||||
"graphql-tag": "2.12.5",
|
||||
"ignore": "5.1.8",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.0",
|
||||
"jks-js": "1.0.1",
|
||||
"joi": "17.4.2",
|
||||
"keychain": "1.3.0",
|
||||
"lodash": "4.17.20",
|
||||
"mime": "2.4.7",
|
||||
"lodash": "4.17.21",
|
||||
"mime": "2.5.2",
|
||||
"minimatch": "3.0.4",
|
||||
"nanoid": "3.1.23",
|
||||
"nanoid": "3.1.25",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-forge": "0.10.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"progress": "2.0.3",
|
||||
"prompts": "2.3.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.4",
|
||||
"semver": "7.3.5",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.0.5",
|
||||
"tar": "6.1.11",
|
||||
"tempy": "0.7.0",
|
||||
"terminal-link": "2.1.1",
|
||||
"timeago.js": "4.0.2",
|
||||
"tslib": "2.3.0",
|
||||
"turndown": "7.0.0",
|
||||
"tslib": "2.3.1",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.0",
|
||||
"uuid": "8.3.2",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@expo/babel-preset-cli": "^0.2.17",
|
||||
"@graphql-codegen/cli": "^1.19.2",
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/node": "^12",
|
||||
"@types/lodash": "4.14.172",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
"@types/progress": "^2.0.3",
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/semver": "7.3.8",
|
||||
"@types/tar": "4.0.5",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/wrap-ansi": "^3.0.0",
|
||||
"axios": "0.21.1",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.21.4",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"memfs": "3.2.2",
|
||||
"mockdate": "^3.0.2",
|
||||
"nock": "^13.0.5",
|
||||
"typescript": "4.3.5"
|
||||
"nock": "^13.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
@@ -184,7 +178,5 @@
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
},
|
||||
"types": "build/index.d.ts",
|
||||
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ describe('apiClient', () => {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('apiClient', () => {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeInstanceOf(RequestError);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Identify } from '@amplitude/identify';
|
||||
import * as Amplitude from '@amplitude/node';
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from './user/UserSettings';
|
||||
|
||||
@@ -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';
|
||||
@@ -102,7 +102,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
@@ -221,7 +221,7 @@ async function withAnalyticsAsync<Result>(
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
reason: error.message,
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { RequestedPlatform } from './types';
|
||||
|
||||
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
|
||||
@@ -6,13 +5,3 @@ export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> =
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
|
||||
export const appPlatformDisplayNames: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Android]: 'Android',
|
||||
[AppPlatform.Ios]: 'iOS',
|
||||
};
|
||||
|
||||
export const appPlatformEmojis = {
|
||||
[AppPlatform.Ios]: '🍎',
|
||||
[AppPlatform.Android]: '🤖',
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
await spawnAsync(execNameOrPath, [arg], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
|
||||
Log.warn(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
BuildFragment,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { appPlatformDisplayNames } from '../constants';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
|
||||
export function formatGraphQLBuild(build: BuildFragment) {
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { printJsonOnlyOutput } from '../../utils/json';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
|
||||
@@ -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') {
|
||||
@@ -29,9 +28,9 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
const rootDir = await vcs.getRootPathAsync();
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(googleServicesFilePath)) &&
|
||||
((await vcs.isFileIgnoredAsync(googleServicesFilePath)) ||
|
||||
!isInsideDirectory(absGoogleServicesFilePath, rootDir))
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
) {
|
||||
Log.warn(
|
||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||
|
||||
@@ -5,35 +5,42 @@ import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand from '../EasCommand';
|
||||
import TestEasCommand from './TestEasCommand';
|
||||
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
|
||||
jest.mock('../../analytics', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(getUserAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
|
||||
(ensureLoggedInAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
|
||||
(initAsync as jest.Mock).mockReset();
|
||||
(flushAsync as jest.Mock).mockReset();
|
||||
(logEvent as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
beforeAll(() => {
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
jest.mock('../../user/User', () => ({ getUserAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../analytics', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(ensureLoggedInAsync as jest.Mock).mockClear();
|
||||
(initAsync as jest.Mock).mockClear();
|
||||
(flushAsync as jest.Mock).mockClear();
|
||||
(logEvent as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('without exceptions', () => {
|
||||
// The first test in this suite should have an increased timeout
|
||||
// because of the implementation of Command from @oclif/command.
|
||||
// It seems that loading config takes significant amount of time
|
||||
// and I'm not sure how to mock it.
|
||||
//
|
||||
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
|
||||
// and look for "Config.load"
|
||||
it('ensures the user is logged in', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(ensureLoggedInAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
expect(ensureLoggedInAsync).toHaveBeenCalled();
|
||||
}, 15_000);
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
|
||||
|
||||
@@ -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'] }];
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
import ora from 'ora';
|
||||
|
||||
import { appPlatformEmojis } from '../../build/constants';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
Build,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { appPlatformEmojis } from '../../platform';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
|
||||
@@ -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,156 +1,73 @@
|
||||
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';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { learnMore } from '../log';
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../platform';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
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';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
const ANDROID_FLAGS = 'Android specific options';
|
||||
const IOS_FLAGS = 'iOS specific options';
|
||||
import { SubmitArchiveFlags } from '../submissions/types';
|
||||
import { displayLogsAsync } from '../submissions/utils/logs';
|
||||
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
|
||||
import { waitForSubmissionsEndAsync } from '../submissions/utils/wait';
|
||||
|
||||
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,
|
||||
default: false,
|
||||
}),
|
||||
|
||||
/* 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,
|
||||
wait: flags.boolean({
|
||||
description: 'Wait for submission to complete',
|
||||
default: true,
|
||||
allowNo: true,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -158,7 +75,7 @@ export default class BuildSubmit extends EasCommand {
|
||||
const flags = this.parseFlags();
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
(await this.promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -170,112 +87,115 @@ export default class BuildSubmit extends EasCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Make this work outside project dir
|
||||
if (!projectDir) {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
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 options: AndroidSubmitCommandFlags = {
|
||||
releaseStatus: 'completed',
|
||||
track: 'internal',
|
||||
verbose: false,
|
||||
changesNotSentForReview: false,
|
||||
...submitProfile,
|
||||
...flags.androidOptions,
|
||||
};
|
||||
|
||||
const ctx = AndroidSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const submitProfile = await easJsonReader.readSubmitProfileAsync(
|
||||
Platform.ANDROID,
|
||||
flags.profile
|
||||
);
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
profile: submitProfile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
} else if (platform === AppPlatform.Ios) {
|
||||
const submitProfile = flags.profile
|
||||
? await easJsonReader.readSubmitProfileAsync(flags.profile, Platform.IOS)
|
||||
: {};
|
||||
const options: IosSubmitCommandFlags = {
|
||||
language: 'en-US',
|
||||
verbose: false,
|
||||
...submitProfile,
|
||||
...flags.iosOptions,
|
||||
};
|
||||
|
||||
const ctx = IosSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
submissions.push(await command.runAsync());
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${platform}!`);
|
||||
iosSubmitProfile = await easJsonReader.readSubmitProfileAsync(Platform.IOS, flags.profile);
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
profile: iosSubmitProfile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
submissions.push(await command.runAsync());
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
|
||||
if (flags.wait) {
|
||||
Log.newLine();
|
||||
const completedSubmissions = await waitForSubmissionsEndAsync(submissions);
|
||||
for (const submission of completedSubmissions) {
|
||||
if (completedSubmissions.length > 1) {
|
||||
Log.log(
|
||||
`${appPlatformEmojis[submission.platform]}${chalk.bold(
|
||||
`${appPlatformDisplayNames[submission.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (
|
||||
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(logMsg);
|
||||
}
|
||||
await displayLogsAsync(submission, { verbose: flags.verbose });
|
||||
if (completedSubmissions.length > 1) {
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
this.exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
}
|
||||
}
|
||||
|
||||
private parseFlags(): Flags {
|
||||
const {
|
||||
flags: {
|
||||
platform,
|
||||
profile,
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<AppPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Submit to platform',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
title: 'iOS',
|
||||
value: AppPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: AppPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
private async promptForPlatformAsync(): Promise<AppPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Submit to platform',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
title: 'iOS',
|
||||
value: AppPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: AppPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
|
||||
private exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(submissions: SubmissionFragment[]): void {
|
||||
const nonFinishedSubmissions = submissions.filter(
|
||||
({ status }) => status !== SubmissionStatus.Finished
|
||||
);
|
||||
if (nonFinishedSubmissions.length > 0) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
testPKCS12KeystoreEmptyPasswordBase64,
|
||||
} from './fixtures-base64-data';
|
||||
|
||||
const now = new Date();
|
||||
|
||||
export const testKeystore: Keystore = {
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: 'ae6777e9444a436dbe533d2be46c83ba',
|
||||
@@ -61,8 +63,8 @@ export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
|
||||
},
|
||||
credential: 'abcdxxxxxxefgh',
|
||||
version: AndroidFcmVersion.Legacy,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
|
||||
@@ -75,8 +77,8 @@ export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
|
||||
md5CertificateFingerprint: 'test-md5',
|
||||
sha1CertificateFingerprint: 'test-sha1',
|
||||
sha256CertificateFingerprint: 'test-sha256',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
|
||||
@@ -13,6 +13,8 @@ import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Cr
|
||||
import { Target } from '../ios/types';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
|
||||
const now = new Date();
|
||||
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
@@ -39,7 +41,7 @@ export const testPushKey: ApplePushKeyFragment = {
|
||||
id: 'test-push-key-id',
|
||||
keyIdentifier: 'test-key-identifier',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
updatedAt: new Date(),
|
||||
updatedAt: now,
|
||||
iosAppCredentialsList: [],
|
||||
};
|
||||
|
||||
@@ -47,10 +49,10 @@ export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier:
|
||||
|
||||
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
|
||||
id: 'test-prov-prof-id-1',
|
||||
expiration: new Date(),
|
||||
expiration: now,
|
||||
developerPortalIdentifier: 'test-developer-identifier',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: now,
|
||||
status: 'Active',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
appleDevices: [],
|
||||
@@ -62,9 +64,9 @@ export const testDistCertFragmentNoDependencies: AppleDistributionCertificateFra
|
||||
certificatePassword: 'test-password',
|
||||
serialNumber: 'test-serial',
|
||||
developerPortalIdentifier: 'test-developer-identifier',
|
||||
validityNotAfter: new Date(),
|
||||
validityNotBefore: new Date(),
|
||||
updatedAt: new Date(),
|
||||
validityNotAfter: now,
|
||||
validityNotBefore: now,
|
||||
updatedAt: now,
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
iosAppBuildCredentialsList: [],
|
||||
};
|
||||
@@ -75,9 +77,9 @@ export const testDistCertFragmentOneDependency: AppleDistributionCertificateFrag
|
||||
certificatePassword: 'test-password',
|
||||
serialNumber: 'test-serial',
|
||||
developerPortalIdentifier: 'test-developer-identifier',
|
||||
validityNotAfter: new Date(),
|
||||
validityNotBefore: new Date(),
|
||||
updatedAt: new Date(),
|
||||
validityNotAfter: now,
|
||||
validityNotBefore: now,
|
||||
updatedAt: now,
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
iosAppBuildCredentialsList: [
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
@@ -1,7 +1,10 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import Log from '../../../../log';
|
||||
import { testAndroidAppCredentialsFragment } from '../../../__tests__/fixtures-android';
|
||||
import {
|
||||
testAndroidAppCredentialsFragment,
|
||||
testLegacyAndroidFcmFragment,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentialsUtils';
|
||||
import { displayAndroidAppCredentials } from '../printCredentials';
|
||||
@@ -9,7 +12,7 @@ import { displayAndroidAppCredentials } from '../printCredentials';
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
|
||||
|
||||
mockdate.set(new Date('4/20/2021'));
|
||||
mockdate.set(testLegacyAndroidFcmFragment.updatedAt);
|
||||
|
||||
describe('print credentials', () => {
|
||||
it('prints the AndroidAppCredentials fragment', async () => {
|
||||
|
||||
@@ -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) {
|
||||
throw new Error(
|
||||
`An error occurred when validating the Android keystore: ${e.stdout ?? e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ function getPemFromJksKeystore(keystore: Keystore): string {
|
||||
const { protectedPrivateKey } = keystoreEntry;
|
||||
try {
|
||||
return jks.decrypt(protectedPrivateKey, keystore.keyPassword);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
throw new Error(`${e.message}: Check that your key password is correct`);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ function getX509Asn1FromPKCS12Keystore(
|
||||
try {
|
||||
const p12 = parsePKCS12(p12BufferOrBase64String, maybePassword);
|
||||
x509Asn1 = getX509Asn1ByFriendlyName(p12, keyAlias);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
throw new Error(`Invalid PKCS#12 (.p12) keystore: ${e.message}`);
|
||||
}
|
||||
if (!x509Asn1) {
|
||||
|
||||
@@ -22,15 +22,15 @@ import { updateAndroidCredentialsAsync, updateIosCredentialsAsync } from '../upd
|
||||
jest.mock('fs');
|
||||
jest.mock('prompts');
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update credentials.json', () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(updateAndroidCredentialsAsync, () => {
|
||||
it('should update keystore in credentials.json if www returns valid credentials', async () => {
|
||||
const ctx = createCtxMock();
|
||||
@@ -102,7 +102,7 @@ describe('update credentials.json', () => {
|
||||
delete buildCredentialsNoKeystore.androidKeystore;
|
||||
await updateAndroidCredentialsAsync(ctx, buildCredentialsNoKeystore);
|
||||
throw new Error('updateAndroidCredentialsAsync should throw an error');
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
expect(e.message).toMatch(
|
||||
'There are no credentials configured for this project on EAS servers'
|
||||
);
|
||||
@@ -278,7 +278,7 @@ describe('update credentials.json', () => {
|
||||
try {
|
||||
await updateIosCredentialsAsync(ctx, app, targets, IosDistributionType.AppStore);
|
||||
throw new Error('updateIosCredentialsAsync should throw na error');
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
expect(e.message).toMatch('There are no credentials configured');
|
||||
}
|
||||
const certP12 = await fs.readFile('./cert.p12', 'base64');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Joi from '@hapi/joi';
|
||||
import Joi from 'joi';
|
||||
|
||||
import { Keystore } from '../android/credentials';
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
try {
|
||||
return await ctx.appStore.createPushKeyAsync();
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
if (e.message === APPLE_KEYS_TOO_MANY_GENERATED_ERROR) {
|
||||
const pushKeys = await ctx.appStore.listPushKeysAsync();
|
||||
Log.warn('Maximum number of Push Notifications Keys generated on Apple Developer Portal.');
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testTargets } from '../../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
||||
import { CreatePushKey } from '../CreatePushKey';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
exports[`select credentials select an AppleDistributionCertificate fragment 1`] = `
|
||||
"Cert ID: 5URSS525PL, Serial number: 3F9CDD6CFC6E52C1, Team ID: 77KQ969CHE
|
||||
Created: 6 months ago, Updated: 10 months ago,
|
||||
Expires: Wed, 13 Oct 2021 18:28:44 GMT+0000Cert ID: JRH7292L8R, Serial number: 5544BE191B9F949E, Team ID: 77KQ969CHE
|
||||
Expires: Wed, 13 Oct 2021 18:28:44 UTCCert ID: JRH7292L8R, Serial number: 5544BE191B9F949E, Team ID: 77KQ969CHE
|
||||
Created: 10 months ago, Updated: 10 months ago,
|
||||
Expires: Tue, 22 Jun 2021 20:46:07 GMT+0000"
|
||||
Expires: Tue, 22 Jun 2021 20:46:07 UTC"
|
||||
`;
|
||||
@@ -178,7 +178,7 @@ export async function authenticateAsync(options: Options = {}): Promise<AuthCtx>
|
||||
// Defined for legacy usage in Turtle V1 or any other places where Fastlane is used in the servers.
|
||||
fastlaneSession,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.message === 'ABORTED') {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function getProfilesForBundleIdAsync(
|
||||
try {
|
||||
await profile.getBundleIdAsync();
|
||||
validProfileIds.add(profile.id);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.name === 'UnexpectedAppleResponse' &&
|
||||
e.message.includes('The specified resource does not exist - There is no resource of type')
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function syncCapabilityIdentifiersForEntitlementsAsync(
|
||||
remoteIdModel = await CapabilityModel.createAsync(bundleId.context, {
|
||||
identifier: localId,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Add a more helpful error message.
|
||||
error.message += `\n\nRemove the value '${localId}' from the array '${classifier.entitlement}' in the iOS project entitlements.\nIf you know that the ID is registered to one of your apps, try again with a different Apple account.`;
|
||||
throw error;
|
||||
|
||||
@@ -11,7 +11,7 @@ async function getContractStatusAsync(
|
||||
try {
|
||||
const capabilities = await ITCAgreements.getCapabilitiesAsync(context);
|
||||
return capabilities?.contractStatus ?? null;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
Log.warn(`Failed to get iTunes contract status: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ async function getContractStatusAsync(
|
||||
async function getContractMessagesAsync(context: RequestContext) {
|
||||
try {
|
||||
return await ITCAgreements.getContractMessagesAsync(context);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
Log.warn(`Failed to get iTunes contract messages: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function createDistributionCertificateAsync(
|
||||
teamId: authCtx.team.id,
|
||||
teamName: authCtx.team.name,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
spinner.fail('Failed to create Apple distribution certificate');
|
||||
// TODO: Move check into apple-utils
|
||||
if (
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
});
|
||||
}
|
||||
spinner.succeed(`Bundle identifier registered ${chalk.dim(bundleIdentifier)}`);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
spinner.fail(
|
||||
`The bundle identifier ${chalk.bold(bundleIdentifier)} is not available to team "${
|
||||
@@ -179,7 +179,7 @@ export async function ensureAppExistsAsync(
|
||||
companyName,
|
||||
sku,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
const providerName = authCtx.authState?.session.provider.name;
|
||||
throw new Error(
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function createPushKeyAsync(
|
||||
teamId: authCtx.team.id,
|
||||
teamName: authCtx.team.name,
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
spinner.fail('Failed to create Apple push key');
|
||||
const resultString = err.rawDump?.resultString;
|
||||
if (
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`print credentials prints the IosAppCredentials map 1`] = `"iOS Credentials Project: @quinlanj/test52 Bundle Identifier: com.quinlanj.test52 Apple Team: test-apple-identifier Push Key: Developer Portal ID: 9839M6AY8W Apple Team: 77KQ969CHE Updated 6 months ago App Specific Password: Apple ID Username: [email protected] Password Label: description for super secret password Updated 6 months ago Configuration: App Store Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 5 months ago Configuration: Ad Hoc Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 5 months ago"`;
|
||||
exports[`print credentials prints the IosAppCredentials map 1`] = `"iOS Credentials Project: @quinlanj/test52 Bundle Identifier: com.quinlanj.test52 Apple Team: test-apple-identifier Push Key: Developer Portal ID: 9839M6AY8W Apple Team: 77KQ969CHE Updated 6 months ago App Specific Password: Apple ID Username: [email protected] Password Label: description for super secret password Updated 6 months ago Configuration: App Store Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 5 months ago Configuration: Ad Hoc Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 5 months ago"`;
|
||||
@@ -47,7 +47,7 @@ function getRawCertData(p12Buffer: Buffer | string, passwordRaw: string | null):
|
||||
} else {
|
||||
p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1);
|
||||
}
|
||||
} catch (_error) {
|
||||
} catch (_error: any) {
|
||||
const error: Error = _error;
|
||||
if (/Invalid password/.exec(error.message)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -55,7 +55,7 @@ function validateProvisioningProfileWithoutApple(
|
||||
nullthrows(distributionCertificate?.certificateP12),
|
||||
nullthrows(distributionCertificate?.certificatePassword)
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
Log.warn(`Failed to calculate fingerprint for Distribution Certificate: ${e.toString()}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+73
-21
@@ -485,6 +485,7 @@ export type App = Project & {
|
||||
users?: Maybe<Array<Maybe<User>>>;
|
||||
/** @deprecated Field no longer supported */
|
||||
releases: Array<Maybe<AppRelease>>;
|
||||
latestReleaseForReleaseChannel?: Maybe<AppRelease>;
|
||||
};
|
||||
|
||||
|
||||
@@ -609,6 +610,13 @@ export type AppReleasesArgs = {
|
||||
limit?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppLatestReleaseForReleaseChannelArgs = {
|
||||
platform: AppPlatform;
|
||||
releaseChannel: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AppIcon = {
|
||||
__typename?: 'AppIcon';
|
||||
url: Scalars['String'];
|
||||
@@ -893,7 +901,7 @@ export type Submission = ActivityTimelineProjectActivity & {
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
app?: Maybe<App>;
|
||||
app: App;
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
submittedBuild?: Maybe<Build>;
|
||||
platform: AppPlatform;
|
||||
@@ -907,16 +915,18 @@ export type Submission = ActivityTimelineProjectActivity & {
|
||||
};
|
||||
|
||||
export enum SubmissionStatus {
|
||||
AwaitingBuild = 'AWAITING_BUILD',
|
||||
InQueue = 'IN_QUEUE',
|
||||
InProgress = 'IN_PROGRESS',
|
||||
Finished = 'FINISHED',
|
||||
Errored = 'ERRORED'
|
||||
Errored = 'ERRORED',
|
||||
Canceled = 'CANCELED'
|
||||
}
|
||||
|
||||
export type AndroidSubmissionConfig = {
|
||||
__typename?: 'AndroidSubmissionConfig';
|
||||
applicationIdentifier: Scalars['String'];
|
||||
archiveType: SubmissionAndroidArchiveType;
|
||||
archiveType?: Maybe<SubmissionAndroidArchiveType>;
|
||||
track: SubmissionAndroidTrack;
|
||||
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
|
||||
};
|
||||
@@ -978,14 +988,15 @@ export type BuildJob = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
export type AppRelease = {
|
||||
__typename?: 'AppRelease';
|
||||
id: Scalars['ID'];
|
||||
hash?: Maybe<Scalars['String']>;
|
||||
publishedTime?: Maybe<Scalars['DateTime']>;
|
||||
publishingUsername?: Maybe<Scalars['String']>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
version?: Maybe<Scalars['String']>;
|
||||
s3Key?: Maybe<Scalars['String']>;
|
||||
s3Url?: Maybe<Scalars['String']>;
|
||||
manifest?: Maybe<Scalars['JSON']>;
|
||||
hash: Scalars['String'];
|
||||
publishedTime: Scalars['DateTime'];
|
||||
publishingUsername: Scalars['String'];
|
||||
sdkVersion: Scalars['String'];
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
version: Scalars['String'];
|
||||
s3Key: Scalars['String'];
|
||||
s3Url: Scalars['String'];
|
||||
manifest: Scalars['JSON'];
|
||||
};
|
||||
|
||||
export type BuildArtifact = {
|
||||
@@ -1032,6 +1043,7 @@ export type Deployment = {
|
||||
channel: Scalars['String'];
|
||||
recentBuilds: Array<Build>;
|
||||
branchMapping?: Maybe<UpdateBranch>;
|
||||
mostRecentlyUpdatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type UpdateBranch = {
|
||||
@@ -2905,7 +2917,10 @@ export type DeleteRobotResult = {
|
||||
|
||||
export type SubmissionMutation = {
|
||||
__typename?: 'SubmissionMutation';
|
||||
/** Create an EAS Submit submission */
|
||||
/**
|
||||
* Create an EAS Submit submission
|
||||
* @deprecated Use createIosSubmission / createAndroidSubmission instead
|
||||
*/
|
||||
createSubmission: CreateSubmissionResult;
|
||||
/** Create an iOS EAS Submit submission */
|
||||
createIosSubmission: CreateSubmissionResult;
|
||||
@@ -2950,7 +2965,7 @@ export type CreateIosSubmissionInput = {
|
||||
export type IosSubmissionConfigInput = {
|
||||
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
|
||||
appleAppSpecificPassword?: Maybe<Scalars['String']>;
|
||||
archiveUrl: Scalars['String'];
|
||||
archiveUrl?: Maybe<Scalars['String']>;
|
||||
appleIdUsername: Scalars['String'];
|
||||
ascAppIdentifier: Scalars['String'];
|
||||
};
|
||||
@@ -2964,11 +2979,12 @@ export type CreateAndroidSubmissionInput = {
|
||||
export type AndroidSubmissionConfigInput = {
|
||||
googleServiceAccountKeyId?: Maybe<Scalars['String']>;
|
||||
googleServiceAccountKeyJson?: Maybe<Scalars['String']>;
|
||||
archiveUrl: Scalars['String'];
|
||||
archiveType: SubmissionAndroidArchiveType;
|
||||
archiveUrl?: Maybe<Scalars['String']>;
|
||||
archiveType?: Maybe<SubmissionAndroidArchiveType>;
|
||||
applicationIdentifier: Scalars['String'];
|
||||
track: SubmissionAndroidTrack;
|
||||
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
|
||||
changesNotSentForReview?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type UpdateChannelMutation = {
|
||||
@@ -3271,6 +3287,7 @@ export type MeMutationLeaveAccountArgs = {
|
||||
|
||||
export type MeMutationInitiateSecondFactorAuthenticationArgs = {
|
||||
deviceConfigurations: Array<Maybe<SecondFactorDeviceConfiguration>>;
|
||||
recaptchaResponseToken?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -4834,23 +4851,45 @@ 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' }
|
||||
& Pick<Submission, 'id'>
|
||||
& SubmissionFragment
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
@@ -5184,7 +5223,20 @@ export type EnvironmentSecretFragment = (
|
||||
export type SubmissionFragment = (
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id' | 'status' | 'platform' | 'logsUrl'>
|
||||
& { error?: Maybe<(
|
||||
& { app: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id' | 'name'>
|
||||
& { ownerAccount: (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id' | 'name'>
|
||||
) }
|
||||
), 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,56 +1,78 @@
|
||||
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,
|
||||
Submission,
|
||||
CreateAndroidSubmissionInput,
|
||||
CreateAndroidSubmissionMutation,
|
||||
CreateAndroidSubmissionMutationVariables,
|
||||
CreateIosSubmissionInput,
|
||||
CreateIosSubmissionMutation,
|
||||
CreateIosSubmissionMutationVariables,
|
||||
SubmissionFragment,
|
||||
} from '../generated';
|
||||
|
||||
interface CreateSubmissionResult {
|
||||
submission: Pick<Submission, 'id'>;
|
||||
}
|
||||
import { SubmissionFragmentNode } from '../types/Submission';
|
||||
|
||||
export const SubmissionMutation = {
|
||||
async createSubmissionAsync(input: {
|
||||
appId: string;
|
||||
submittedBuildId?: string;
|
||||
platform: AppPlatform;
|
||||
config: JSONObject;
|
||||
}): Promise<CreateSubmissionResult> {
|
||||
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
|
||||
...SubmissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(SubmissionFragmentNode)}
|
||||
`,
|
||||
input
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return nullthrows(data.submission?.createSubmission);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,23 @@ export const SubmissionFragmentNode = gql`
|
||||
id
|
||||
status
|
||||
platform
|
||||
app {
|
||||
id
|
||||
name
|
||||
ownerAccount {
|
||||
id
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AppPlatform } from './graphql/generated';
|
||||
|
||||
export const appPlatformDisplayNames: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Android]: 'Android',
|
||||
[AppPlatform.Ios]: 'iOS',
|
||||
};
|
||||
|
||||
export const appPlatformEmojis = {
|
||||
[AppPlatform.Ios]: '🍎',
|
||||
[AppPlatform.Android]: '🤖',
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('fs');
|
||||
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../ensureProjectExists');
|
||||
|
||||
@@ -140,14 +141,6 @@ describe(getProjectAccountNameAsync, () => {
|
||||
expect(projectAccountName).toBe('notnotbrent');
|
||||
});
|
||||
|
||||
it(`throws an error if the user is not logged in`, async () => {
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => undefined);
|
||||
|
||||
await expect(getProjectAccountNameAsync(expWithOwner)).rejects.toThrow(
|
||||
/Failed to access user data/
|
||||
);
|
||||
});
|
||||
|
||||
it(`throws when project owner is undefined for robot actors`, async () => {
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
|
||||
__typename: 'Robot',
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
return await findProjectIdByAccountNameAndSlugAsync(accountName, slug);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function isEasEnabledForProjectAsync(projectId: string): Promise<bo
|
||||
data: { enabled },
|
||||
} = await apiClient.get(`projects/${projectId}/eas-enabled`).json();
|
||||
return enabled;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 404) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildConte
|
||||
|
||||
const metroConfig = await loadConfigAsync(ctx.projectDir);
|
||||
const hasHashAssetFilesPlugin = metroConfig.transformer?.assetPlugins?.find((plugin: string) =>
|
||||
plugin.match(/expo-asset\/tools\/hashAssetFiles/)
|
||||
plugin.match(/expo-asset[/|\\]tools[/|\\]hashAssetFiles/)
|
||||
);
|
||||
if (!hasHashAssetFilesPlugin) {
|
||||
Log.warn(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ExpoConfig, Platform } from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import Joi from '@hapi/joi';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import Joi from 'joi';
|
||||
import { uniqBy } from 'lodash';
|
||||
import mime from 'mime';
|
||||
import path from 'path';
|
||||
|
||||
+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,113 +1,42 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import ora from 'ora';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
|
||||
import {
|
||||
AndroidSubmissionConfigInput,
|
||||
IosSubmissionConfigInput,
|
||||
SubmissionFragment,
|
||||
} from '../graphql/generated';
|
||||
import Log from '../log';
|
||||
import { getProjectAccountNameAsync } from '../project/projectUtils';
|
||||
import { sleep } from '../utils/promise';
|
||||
import SubmissionService, {
|
||||
DEFAULT_CHECK_INTERVAL_MS,
|
||||
SubmissionConfig,
|
||||
} from './SubmissionService';
|
||||
import { BaseSubmissionContext } from './types';
|
||||
import { displayLogs } from './utils/logs';
|
||||
import { SubmissionContext } from './types';
|
||||
|
||||
abstract class BaseSubmitter<SubmissionContext extends BaseSubmissionContext, SubmissionOptions> {
|
||||
protected abstract readonly appStoreName: string;
|
||||
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) {}
|
||||
|
||||
protected constructor(
|
||||
private platform: AppPlatform,
|
||||
protected ctx: SubmissionContext,
|
||||
protected options: SubmissionOptions
|
||||
) {}
|
||||
public abstract submitAsync(): Promise<SubmissionFragment>;
|
||||
|
||||
public abstract submitAsync(): Promise<void>;
|
||||
|
||||
protected async startSubmissionAsync(
|
||||
submissionConfig: SubmissionConfig,
|
||||
buildId?: string,
|
||||
verbose: boolean = false
|
||||
): Promise<SubmissionStatus> {
|
||||
protected async createSubmissionAsync(
|
||||
submissionInput: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
const scheduleSpinner = ora('Scheduling submission').start();
|
||||
let submissionId: string;
|
||||
try {
|
||||
submissionId = await SubmissionService.startSubmissionAsync(
|
||||
this.platform,
|
||||
submissionConfig.projectId,
|
||||
submissionConfig,
|
||||
buildId
|
||||
);
|
||||
const submission = this.createPlatformSubmissionAsync(submissionInput);
|
||||
scheduleSpinner.succeed();
|
||||
return submission;
|
||||
} catch (err) {
|
||||
scheduleSpinner.fail('Failed to schedule submission');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const projectUrl = await this.getProjectUrlAsync();
|
||||
const submissionUrl = `${projectUrl}/submissions/${submissionId}`;
|
||||
|
||||
Log.newLine();
|
||||
Log.log(`Submission details: ${chalk.underline(submissionUrl)}`);
|
||||
Log.log(`Waiting for submission to finish. You can press Ctrl + C to exit`);
|
||||
Log.newLine();
|
||||
|
||||
let submissionCompleted = false;
|
||||
let submissionStatus: SubmissionStatus | null = null;
|
||||
let submission: SubmissionFragment | null = null;
|
||||
const submissionSpinner = ora(`Submitting your app to ${this.appStoreName}`).start();
|
||||
|
||||
try {
|
||||
while (!submissionCompleted) {
|
||||
await sleep(DEFAULT_CHECK_INTERVAL_MS);
|
||||
submission = await SubmissionService.getSubmissionAsync(submissionId);
|
||||
submissionStatus = submission.status;
|
||||
|
||||
submissionSpinner.text = this.getStatusText(submissionStatus);
|
||||
|
||||
if (submissionStatus === SubmissionStatus.Errored) {
|
||||
submissionCompleted = true;
|
||||
process.exitCode = 1;
|
||||
submissionSpinner.fail();
|
||||
} else if (submissionStatus === SubmissionStatus.Finished) {
|
||||
submissionCompleted = true;
|
||||
submissionSpinner.succeed();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
submissionSpinner.fail(this.getStatusText(SubmissionStatus.Errored));
|
||||
throw err;
|
||||
}
|
||||
|
||||
await displayLogs(submission, submissionStatus, verbose);
|
||||
return submissionStatus ?? SubmissionStatus.Errored;
|
||||
}
|
||||
|
||||
private getStatusText(status: SubmissionStatus): string {
|
||||
if (status === SubmissionStatus.InQueue) {
|
||||
return `Submitting your app to ${this.appStoreName}: waiting for an available submitter`;
|
||||
} else if (status === SubmissionStatus.InProgress) {
|
||||
return `Submitting your app to ${this.appStoreName}: submission in progress`;
|
||||
} else if (status === SubmissionStatus.Finished) {
|
||||
return `Submitted your app to ${this.appStoreName}!`;
|
||||
} else if (status === SubmissionStatus.Errored) {
|
||||
return `Something went wrong when submitting your app to ${this.appStoreName}.`;
|
||||
} else {
|
||||
throw new Error('This should never happen');
|
||||
}
|
||||
}
|
||||
|
||||
private async getProjectUrlAsync(): Promise<string> {
|
||||
const projectDir = this.ctx.projectDir;
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const projectName = exp.slug;
|
||||
|
||||
const submissionUrl = `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
|
||||
return submissionUrl;
|
||||
}
|
||||
protected abstract createPlatformSubmissionAsync(
|
||||
input: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment>;
|
||||
}
|
||||
|
||||
export default BaseSubmitter;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
|
||||
import { AppPlatform, SubmissionFragment } from '../graphql/generated';
|
||||
import { SubmissionMutation } from '../graphql/mutations/SubmissionMutation';
|
||||
import { SubmissionQuery } from '../graphql/queries/SubmissionQuery';
|
||||
import { AndroidSubmissionConfig } from './android/AndroidSubmissionConfig';
|
||||
import { IosSubmissionConfig } from './ios/IosSubmissionConfig';
|
||||
|
||||
export type SubmissionConfig = AndroidSubmissionConfig | IosSubmissionConfig;
|
||||
|
||||
export type StartSubmissionResult = string;
|
||||
|
||||
const SubmissionService = {
|
||||
startSubmissionAsync,
|
||||
getSubmissionAsync,
|
||||
};
|
||||
|
||||
export const DEFAULT_CHECK_INTERVAL_MS = 5 * 1000; // 5 secs
|
||||
|
||||
async function startSubmissionAsync(
|
||||
platform: AppPlatform,
|
||||
projectId: string,
|
||||
config: SubmissionConfig,
|
||||
buildId?: string
|
||||
): Promise<StartSubmissionResult> {
|
||||
const { submission } = await SubmissionMutation.createSubmissionAsync({
|
||||
appId: projectId,
|
||||
platform,
|
||||
config: config as unknown as JSONObject,
|
||||
submittedBuildId: buildId,
|
||||
});
|
||||
return submission.id;
|
||||
}
|
||||
|
||||
async function getSubmissionAsync(submissionId: string): Promise<SubmissionFragment> {
|
||||
return await SubmissionQuery.byIdAsync(submissionId, { useCache: false });
|
||||
}
|
||||
|
||||
export default SubmissionService;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../../graphql/generated';
|
||||
import { StartSubmissionResult, SubmissionConfig } from '../SubmissionService';
|
||||
|
||||
const SubmissionService = {
|
||||
startSubmissionAsync,
|
||||
getSubmissionAsync,
|
||||
};
|
||||
|
||||
export const DEFAULT_CHECK_INTERVAL_MS = 0;
|
||||
|
||||
const submissionStore: Record<string, SubmissionFragment> = {};
|
||||
|
||||
async function startSubmissionAsync(
|
||||
platform: AppPlatform,
|
||||
_projectId: string,
|
||||
_config: SubmissionConfig
|
||||
): Promise<StartSubmissionResult> {
|
||||
const id = uuid();
|
||||
const submission: SubmissionFragment = {
|
||||
id,
|
||||
platform,
|
||||
status: SubmissionStatus.InQueue,
|
||||
};
|
||||
submissionStore[id] = submission;
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
|
||||
async function getSubmissionAsync(submissionId: string): Promise<SubmissionFragment> {
|
||||
return submissionStore[submissionId];
|
||||
}
|
||||
|
||||
export default SubmissionService;
|
||||
@@ -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,44 +1,58 @@
|
||||
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 } 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 {
|
||||
static createContext(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
commandFlags: AndroidSubmitCommandFlags
|
||||
): AndroidSubmissionContext {
|
||||
export default class AndroidSubmitCommand {
|
||||
static createContext({
|
||||
archiveFlags,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
}: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: AndroidSubmitProfile;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}): 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<void> {
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
const submissionOptions = await this.getAndroidSubmissionOptionsAsync();
|
||||
const submitter = new AndroidSubmitter(this.ctx, submissionOptions);
|
||||
await submitter.submitAsync();
|
||||
return await submitter.submitAsync();
|
||||
}
|
||||
|
||||
private async getAndroidSubmissionOptionsAsync(): Promise<AndroidSubmissionOptions> {
|
||||
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 = [
|
||||
@@ -61,7 +75,7 @@ class AndroidSubmitCommand {
|
||||
releaseStatus: releaseStatus.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
serviceAccountSource: serviceAccountSource.enforceValue(),
|
||||
changesNotSentForReview: this.ctx.commandFlags.changesNotSentForReview,
|
||||
changesNotSentForReview: this.ctx.profile.changesNotSentForReview,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,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,
|
||||
@@ -93,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,
|
||||
@@ -170,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 } 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,33 +35,41 @@ interface ResolvedSourceOptions {
|
||||
serviceAccountPath: string;
|
||||
}
|
||||
|
||||
class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSubmissionOptions> {
|
||||
protected readonly appStoreName = 'Google Play Store';
|
||||
|
||||
constructor(ctx: AndroidSubmissionContext, options: AndroidSubmissionOptions) {
|
||||
super(AppPlatform.Android, ctx, options);
|
||||
}
|
||||
|
||||
async submitAsync(): Promise<void> {
|
||||
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
|
||||
);
|
||||
|
||||
await this.startSubmissionAsync(
|
||||
return await this.createSubmissionAsync({
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id,
|
||||
this.ctx.commandFlags.verbose
|
||||
);
|
||||
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,
|
||||
@@ -67,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
|
||||
@@ -94,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,3 +1,4 @@
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
|
||||
import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -6,20 +7,22 @@ import { jester as mockJester } from '../../../credentials/__tests__/fixtures-co
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
SubmissionFragment,
|
||||
SubmissionStatus,
|
||||
SubmissionAndroidReleaseStatus,
|
||||
SubmissionAndroidTrack,
|
||||
} from '../../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import SubmissionService from '../../SubmissionService';
|
||||
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('../../SubmissionService');
|
||||
jest.mock('../../../graphql/mutations/SubmissionMutation', () => ({
|
||||
SubmissionMutation: {
|
||||
createAndroidSubmissionAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
jest.mock('../../utils/builds');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
@@ -62,8 +65,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
});
|
||||
afterAll(() => {
|
||||
vol.reset();
|
||||
|
||||
jest.unmock('@expo/config');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -71,111 +72,70 @@ describe(AndroidSubmitCommand, () => {
|
||||
});
|
||||
|
||||
describe('sending submission', () => {
|
||||
const originalStartSubmissionAsync = SubmissionService.startSubmissionAsync;
|
||||
const originalGetSubmissionAsync = SubmissionService.getSubmissionAsync;
|
||||
beforeAll(() => {
|
||||
SubmissionService.startSubmissionAsync = jest.fn(SubmissionService.startSubmissionAsync);
|
||||
SubmissionService.getSubmissionAsync = jest.fn(SubmissionService.getSubmissionAsync);
|
||||
});
|
||||
afterAll(() => {
|
||||
SubmissionService.startSubmissionAsync = originalStartSubmissionAsync;
|
||||
SubmissionService.getSubmissionAsync = originalGetSubmissionAsync;
|
||||
});
|
||||
afterEach(() => {
|
||||
asMock(SubmissionService.startSubmissionAsync).mockClear();
|
||||
asMock(SubmissionService.getSubmissionAsync).mockClear();
|
||||
});
|
||||
|
||||
it('sends a request to Submission Service', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(SubmissionService.getSubmissionAsync).mockImplementationOnce(
|
||||
async (submissionId: string): Promise<SubmissionFragment> => {
|
||||
const actualSubmission = await originalGetSubmissionAsync(submissionId);
|
||||
return {
|
||||
...actualSubmission,
|
||||
status: SubmissionStatus.Finished,
|
||||
};
|
||||
}
|
||||
);
|
||||
asMock(getProjectIdAsync).mockImplementationOnce(() => projectId);
|
||||
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
latest: false,
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
type: 'apk',
|
||||
serviceAccountKeyPath: '/google-service-account.json',
|
||||
track: 'internal',
|
||||
releaseStatus: 'draft',
|
||||
verbose: false,
|
||||
changesNotSentForReview: false,
|
||||
};
|
||||
const ctx = AndroidSubmitCommand.createContext(testProject.projectRoot, projectId, options);
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
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(SubmissionService.startSubmissionAsync).toHaveBeenCalledWith(
|
||||
AppPlatform.Android,
|
||||
projectId,
|
||||
androidSubmissionConfig,
|
||||
undefined
|
||||
);
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('assigns the build ID to submission', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(SubmissionService.getSubmissionAsync).mockImplementationOnce(
|
||||
async (submissionId: string): Promise<SubmissionFragment> => {
|
||||
const actualSubmission = await originalGetSubmissionAsync(submissionId);
|
||||
return {
|
||||
...actualSubmission,
|
||||
status: SubmissionStatus.Finished,
|
||||
};
|
||||
}
|
||||
);
|
||||
asMock(getProjectIdAsync).mockImplementationOnce(() => projectId);
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
|
||||
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
latest: true,
|
||||
type: 'apk',
|
||||
serviceAccountKeyPath: '/google-service-account.json',
|
||||
track: 'internal',
|
||||
releaseStatus: 'draft',
|
||||
verbose: false,
|
||||
changesNotSentForReview: false,
|
||||
};
|
||||
const ctx = AndroidSubmitCommand.createContext(testProject.projectRoot, projectId, options);
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
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(SubmissionService.startSubmissionAsync).toHaveBeenCalledWith(
|
||||
AppPlatform.Android,
|
||||
projectId,
|
||||
androidSubmissionConfig,
|
||||
fakeBuildFragment.id
|
||||
);
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
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),
|
||||
};
|
||||
@@ -101,7 +95,7 @@ async function createAppStoreConnectAppAsync(options: CreateAppOptions): Promise
|
||||
bundleIdentifier: bundleId,
|
||||
sku,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (
|
||||
// Name is invalid
|
||||
error.message.match(
|
||||
@@ -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 } 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,30 +19,38 @@ import {
|
||||
} from './AppSpecificPasswordSource';
|
||||
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
||||
|
||||
class IosSubmitCommand {
|
||||
static createContext(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
commandFlags: IosSubmitCommandFlags
|
||||
): IosSubmissionContext {
|
||||
export default class IosSubmitCommand {
|
||||
static createContext({
|
||||
archiveFlags,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
}: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: IosSubmitProfile;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}): 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<void> {
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
const options = await this.resolveSubmissionOptionsAsync();
|
||||
const submitter = new IosSubmitter(this.ctx, options);
|
||||
await submitter.submitAsync();
|
||||
return await submitter.submitAsync();
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -50,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(),
|
||||
};
|
||||
@@ -76,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,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
|
||||
)
|
||||
@@ -123,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) {
|
||||
@@ -147,5 +153,3 @@ class IosSubmitCommand {
|
||||
return promptAppleId;
|
||||
}
|
||||
}
|
||||
|
||||
export default IosSubmitCommand;
|
||||
@@ -1,10 +1,9 @@
|
||||
import chalk from 'chalk';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { AppPlatform, SubmissionStatus } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { IosSubmissionContext } from '../types';
|
||||
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,
|
||||
@@ -14,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;
|
||||
}
|
||||
@@ -27,17 +26,8 @@ interface ResolvedSourceOptions {
|
||||
appSpecificPassword: string;
|
||||
}
|
||||
|
||||
type SummaryData = Pick<IosSubmissionOptions, 'ascAppId' | 'appleId' | 'projectId'> &
|
||||
ArchiveSourceSummaryFields;
|
||||
|
||||
class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOptions> {
|
||||
protected readonly appStoreName: string = 'Apple App Store';
|
||||
|
||||
constructor(ctx: IosSubmissionContext, options: IosSubmissionOptions) {
|
||||
super(AppPlatform.Ios, ctx, options);
|
||||
}
|
||||
|
||||
async submitAsync(): Promise<void> {
|
||||
export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmissionOptions> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptions();
|
||||
const submissionConfig = await this.formatSubmissionConfigAsync(
|
||||
this.options,
|
||||
@@ -46,33 +36,30 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
SummaryHumanReadableKeys
|
||||
);
|
||||
|
||||
const result = await this.startSubmissionAsync(
|
||||
return await this.createSubmissionAsync({
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id,
|
||||
this.ctx.commandFlags.verbose
|
||||
);
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (result === SubmissionStatus.Finished) {
|
||||
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/${this.options.ascAppId}/appstore/ios`,
|
||||
{ learnMoreMessage: '' }
|
||||
)
|
||||
);
|
||||
}
|
||||
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
|
||||
);
|
||||
@@ -86,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,17 +3,18 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import SubmissionService from '../../SubmissionService';
|
||||
import { IosSubmitCommandFlags } from '../../types';
|
||||
import { IosSubmissionConfig } from '../IosSubmissionConfig';
|
||||
import IosSubmitCommand from '../IosSubmitCommand';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('ora');
|
||||
jest.mock('../../SubmissionService');
|
||||
jest.mock('../../../graphql/mutations/SubmissionMutation', () => ({
|
||||
SubmissionMutation: {
|
||||
createIosSubmissionAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
getUserAsync: jest.fn(() => mockJester),
|
||||
@@ -43,7 +44,6 @@ describe(IosSubmitCommand, () => {
|
||||
});
|
||||
afterAll(() => {
|
||||
vol.reset();
|
||||
|
||||
jest.unmock('@expo/config');
|
||||
});
|
||||
|
||||
@@ -52,61 +52,35 @@ describe(IosSubmitCommand, () => {
|
||||
});
|
||||
|
||||
describe('sending submission', () => {
|
||||
const originalStartSubmissionAsync = SubmissionService.startSubmissionAsync;
|
||||
const originalGetSubmissionAsync = SubmissionService.getSubmissionAsync;
|
||||
beforeAll(() => {
|
||||
SubmissionService.startSubmissionAsync = jest.fn(SubmissionService.startSubmissionAsync);
|
||||
SubmissionService.getSubmissionAsync = jest.fn(SubmissionService.getSubmissionAsync);
|
||||
});
|
||||
afterAll(() => {
|
||||
SubmissionService.startSubmissionAsync = originalStartSubmissionAsync;
|
||||
SubmissionService.getSubmissionAsync = originalGetSubmissionAsync;
|
||||
});
|
||||
afterEach(() => {
|
||||
asMock(SubmissionService.startSubmissionAsync).mockClear();
|
||||
asMock(SubmissionService.getSubmissionAsync).mockClear();
|
||||
});
|
||||
|
||||
it('sends a request to Submission Service', async () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(SubmissionService.getSubmissionAsync).mockImplementationOnce(
|
||||
async (submissionId: string): Promise<SubmissionFragment> => {
|
||||
const actualSubmission = await originalGetSubmissionAsync(submissionId);
|
||||
return {
|
||||
...actualSubmission,
|
||||
status: SubmissionStatus.Finished,
|
||||
};
|
||||
}
|
||||
);
|
||||
asMock(getProjectIdAsync).mockImplementationOnce(() => projectId);
|
||||
|
||||
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
|
||||
|
||||
const options: IosSubmitCommandFlags = {
|
||||
latest: false,
|
||||
url: 'http://expo.dev/fake.ipa',
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '12345678',
|
||||
verbose: false,
|
||||
};
|
||||
const ctx = IosSubmitCommand.createContext(testProject.projectRoot, projectId, options);
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
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(SubmissionService.startSubmissionAsync).toHaveBeenCalledWith(
|
||||
AppPlatform.Ios,
|
||||
projectId,
|
||||
iosSubmissionConfig,
|
||||
undefined
|
||||
);
|
||||
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.ipa',
|
||||
appleIdUsername: '[email protected]',
|
||||
appleAppSpecificPassword: 'supersecret',
|
||||
ascAppIdentifier: '12345678',
|
||||
},
|
||||
});
|
||||
|
||||
delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
|
||||
});
|
||||
|
||||
@@ -1,56 +1,17 @@
|
||||
export interface SubmissionContext<T extends SubmitCommandFlags> extends BaseSubmissionContext {
|
||||
commandFlags: T;
|
||||
}
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { SubmitProfile } from '@expo/eas-json';
|
||||
|
||||
export interface BaseSubmissionContext {
|
||||
export interface SubmissionContext<T extends Platform> {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
platform: T;
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export interface SubmitCommandFlags {
|
||||
export interface SubmitArchiveFlags {
|
||||
latest?: boolean;
|
||||
id?: string;
|
||||
path?: string;
|
||||
url?: string;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
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>;
|
||||
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user