mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc14b5b30 | ||
|
|
bee98ce25e | ||
|
|
0e6a5315ab | ||
|
|
91b51528ef | ||
|
|
5e3962afc2 | ||
|
|
f07c52d2fb | ||
|
|
50846a1a96 | ||
|
|
4cb726f374 | ||
|
|
da9d1b4984 | ||
|
|
64ad52dc45 | ||
|
|
6169e5ff5b | ||
|
|
ef33973f7f | ||
|
|
33a5254f2a | ||
|
|
0d737ecf6d | ||
|
|
951eea2396 | ||
|
|
75968bf449 | ||
|
|
32aa46ac69 | ||
|
|
339cae2532 | ||
|
|
e6ec3bfe6a | ||
|
|
012518c375 | ||
|
|
cc8c8584d9 | ||
|
|
d1910b3c42 | ||
|
|
4a1fa3d4fa | ||
|
|
61178c9d06 | ||
|
|
094c04c7f0 | ||
|
|
37cca4878d | ||
|
|
2b40e1ec05 | ||
|
|
6e95d3afcc | ||
|
|
6b9b9f0be2 | ||
|
|
f14b963b29 | ||
|
|
ddd2814e8f | ||
|
|
36f15a81a9 | ||
|
|
ece93dac20 | ||
|
|
0226dbba5a | ||
|
|
e773e77185 | ||
|
|
893cbb2ec5 | ||
|
|
1898de2609 | ||
|
|
fe03cc28c4 | ||
|
|
5309b7b6ce |
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: {
|
||||
|
||||
@@ -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:
|
||||
|
||||
+62
-1
@@ -6,14 +6,73 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 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 build profile schema validation when `simulator` key is present for iOS ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
|
||||
- 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))
|
||||
|
||||
## [0.24.0](https://github.com/expo/eas-cli/releases/tag/v0.24.0) - 2021-08-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Allow publishing of multiple update groups. ([#566](https://github.com/expo/eas-cli/pull/566) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Retry status code 500-600 server errors from Apple servers with cookies auth. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Throw internal server errors when Apple servers fail to authenticate. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add --auto flag for eas branch:publish. ([#549](https://github.com/expo/eas-cli/pull/549) by [@jkhales](https://github.com/jkhales))
|
||||
- Add submissions link in output of `eas submit`. ([#553](https://github.com/expo/eas-cli/pull/553) by [@ajsmth](https://github.com/ajsmth))
|
||||
- Add submit profiles. ([#555](https://github.com/expo/eas-cli/pull/555) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- 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))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--sku` flag being ignored when running `eas submit -p ios`. ([#559](https://github.com/expo/eas-cli/pull/559) by [@barthap](https://github.com/barthap))
|
||||
- Set correct distribution for simulator builds. ([#556](https://github.com/expo/eas-cli/pull/556) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add warnings around common issues (`NODE_ENV=production`, git ignored `googleServicesFile`). ([#569](https://github.com/expo/eas-cli/pull/569) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.23.0](https://github.com/expo/eas-cli/releases/tag/v0.23.0) - 2021-08-05
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Stop including deprecated storageBucket field when publishing. ([#547](https://github.com/expo/eas-cli/pull/547) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Pass through metadata context to app version resolver. ([#550](https://github.com/expo/eas-cli/pull/550) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.22.4](https://github.com/expo/eas-cli/releases/tag/v0.22.4) - 2021-08-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix build profile schema validation when `simulator` key is present for iOS. ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
|
||||
|
||||
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
|
||||
|
||||
### 🛠 Breaking changes
|
||||
@@ -30,11 +89,13 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix link to build details page where previously we used `project.name` instead of `project.slug` - leave out project segment entirely and depend on redirect. ([#554](https://github.com/expo/eas-cli/pull/554) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
|
||||
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -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.22.4"
|
||||
"version": "0.25.0"
|
||||
}
|
||||
+15
-8
@@ -10,19 +10,26 @@
|
||||
"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": {
|
||||
"@babel/core": "7.15.0",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
+49
-28
@@ -72,7 +72,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -86,7 +86,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -100,7 +100,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -111,7 +111,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -124,6 +124,7 @@ USAGE
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--clear-cache Clear cache before the build
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
@@ -131,7 +132,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.22.4/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -142,7 +143,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -156,7 +157,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.22.4/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -174,14 +175,22 @@ OPTIONS
|
||||
--channel=channel
|
||||
--distribution=(store|internal|simulator)
|
||||
--gitCommitHash=gitCommitHash
|
||||
|
||||
--json Enable JSON output, non-JSON messages will be printed
|
||||
to stderr
|
||||
|
||||
--limit=limit
|
||||
|
||||
--platform=(all|android|ios)
|
||||
|
||||
--runtimeVersion=runtimeVersion
|
||||
|
||||
--sdkVersion=sdkVersion
|
||||
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -190,9 +199,12 @@ view a build for your project
|
||||
```
|
||||
USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
|
||||
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.22.4/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -207,7 +219,7 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -218,7 +230,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -229,7 +241,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -243,7 +255,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -254,7 +266,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -271,7 +283,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`
|
||||
|
||||
@@ -282,7 +294,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -296,7 +308,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -313,7 +325,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -330,7 +342,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.22.4/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -341,7 +353,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
@@ -373,13 +385,18 @@ OPTIONS
|
||||
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 key used to authenticate with
|
||||
Google Play
|
||||
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, ...)
|
||||
@@ -388,6 +405,8 @@ OPTIONS
|
||||
|
||||
--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)
|
||||
|
||||
@@ -403,6 +422,8 @@ OPTIONS
|
||||
|
||||
--verbose Always print logs from Submission Service
|
||||
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
|
||||
@@ -421,7 +442,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -440,7 +461,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -454,7 +475,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -468,7 +489,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.22.4/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -488,7 +509,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -502,5 +523,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.4/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- 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
+2455
-702
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.22.4",
|
||||
"author": "Expo <support@expo.io>",
|
||||
"version": "0.25.0",
|
||||
"author": "Expo <support@expo.dev>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
@@ -10,100 +10,93 @@
|
||||
"dependencies": {
|
||||
"@amplitude/identify": "1.7.0",
|
||||
"@amplitude/node": "1.7.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.24",
|
||||
"@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.22.3",
|
||||
"@expo/eas-build-job": "0.2.46",
|
||||
"@expo/eas-json": "^0.25.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",
|
||||
"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/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",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.21.1",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"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",
|
||||
@@ -183,7 +176,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);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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';
|
||||
@@ -18,8 +20,9 @@ const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
let client: Amplitude.NodeClient | null = null;
|
||||
let userIdentifyCalled = false;
|
||||
let amplitudeClient: Amplitude.NodeClient | null = null;
|
||||
let rudderstackClient: RudderAnalytics | null = null;
|
||||
let userIdentified = false;
|
||||
let identifyData: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
@@ -32,11 +35,33 @@ export async function initAsync(): Promise<void> {
|
||||
}
|
||||
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', true);
|
||||
if (amplitudeEnabled) {
|
||||
const apiKey =
|
||||
const config =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? 'cdebbc678931403439486c4750781544'
|
||||
: '4ac443afd5073c0df6169291db1d3495';
|
||||
client = Amplitude.init(apiKey, { retryClass: new Amplitude.OfflineRetryHandler(apiKey) });
|
||||
? {
|
||||
// staging environment
|
||||
amplitudeWriteKey: 'cdebbc678931403439486c4750781544',
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
amplitudeWriteKey: '4ac443afd5073c0df6169291db1d3495',
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
amplitudeClient = Amplitude.init(config.amplitudeWriteKey, {
|
||||
retryClass: new Amplitude.OfflineRetryHandler(config.amplitudeWriteKey),
|
||||
});
|
||||
|
||||
rudderstackClient = new RudderAnalytics(
|
||||
config.rudderstackWriteKey,
|
||||
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
rudderstackClient.logger.silent = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +78,7 @@ export async function setUserDataAsync(
|
||||
if (!savedDeviceId) {
|
||||
await UserSettings.setAsync('amplitudeDeviceId', deviceId);
|
||||
}
|
||||
|
||||
identifyData = {
|
||||
userId,
|
||||
deviceId,
|
||||
@@ -63,41 +89,79 @@ export async function setUserDataAsync(
|
||||
}
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (client) {
|
||||
await client.flush();
|
||||
if (rudderstackClient) {
|
||||
rudderstackClient.flush();
|
||||
}
|
||||
|
||||
if (amplitudeClient) {
|
||||
await amplitudeClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent(name: string, properties: Record<string, any> = {}) {
|
||||
if (client) {
|
||||
ensureUserIdentified();
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
client.logEvent({
|
||||
if (!amplitudeClient && !rudderstackClient) {
|
||||
return;
|
||||
}
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: packageJSON?.version, source: 'eas cli' };
|
||||
|
||||
if (amplitudeClient) {
|
||||
amplitudeClient.logEvent({
|
||||
event_type: name,
|
||||
event_properties: properties,
|
||||
event_properties: { ...properties, ...commonEventProperties },
|
||||
...(userId && { user_id: userId }),
|
||||
...(deviceId && { device_id: deviceId }),
|
||||
...getContext(),
|
||||
...getAmplitudeContext(),
|
||||
});
|
||||
}
|
||||
|
||||
if (rudderstackClient) {
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ensureUserIdentified() {
|
||||
if (client && !userIdentifyCalled && identifyData) {
|
||||
client.identify(identifyData.userId, identifyData.deviceId, identifyData.identify);
|
||||
userIdentifyCalled = true;
|
||||
if (!(rudderstackClient || amplitudeClient) || userIdentified || !identifyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (amplitudeClient) {
|
||||
amplitudeClient.identify(identifyData.userId, identifyData.deviceId, identifyData.identify);
|
||||
}
|
||||
|
||||
if (rudderstackClient) {
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
});
|
||||
}
|
||||
userIdentified = true;
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
function getAmplitudeContext() {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os_name: platform,
|
||||
os_version: os.release(),
|
||||
device_brand: platform,
|
||||
device_model: platform,
|
||||
source_version: packageJSON?.version,
|
||||
source: 'eas cli',
|
||||
};
|
||||
}
|
||||
|
||||
function getRudderStackContext() {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: packageJSON?.version ?? undefined },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ export function getExpoApiBaseUrl(): string {
|
||||
|
||||
export function getExpoWebsiteBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging.expo.io`;
|
||||
return `https://staging.expo.dev`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://expo.test`;
|
||||
} else {
|
||||
return `https://expo.io`;
|
||||
return `https://expo.dev`;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ 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';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
@@ -48,6 +49,9 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
}
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import ora from 'ora';
|
||||
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
@@ -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.');
|
||||
@@ -159,7 +159,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: `Uploaded to EAS`,
|
||||
completedMessage: (duration: string) =>
|
||||
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`,
|
||||
})
|
||||
);
|
||||
return bucketKey;
|
||||
@@ -218,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,
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function ensureProjectConfiguredAsync(
|
||||
} else {
|
||||
error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.io/build/eas-json'
|
||||
'https://docs.expo.dev/build/eas-json'
|
||||
)})`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
|
||||
@@ -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]: '🤖',
|
||||
};
|
||||
@@ -11,6 +11,7 @@ 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';
|
||||
import { transformJob } from './graphql';
|
||||
@@ -25,6 +26,9 @@ export async function prepareIosBuildAsync(
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -41,9 +41,13 @@ export async function collectMetadata<T extends Platform>(
|
||||
platformContext?: MetadataContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution =
|
||||
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
|
||||
? 'simulator'
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: await resolveAppVersionAsync(ctx),
|
||||
appVersion: await resolveAppVersionAsync(ctx, platformContext),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.workflow,
|
||||
@@ -51,7 +55,7 @@ export async function collectMetadata<T extends Platform>(
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: ctx.exp.runtimeVersion,
|
||||
...channelOrReleaseChannel,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildError,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
@@ -12,7 +13,8 @@ import {
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
@@ -28,14 +30,18 @@ export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +70,13 @@ function printBuildResult(build: BuildFragment): void {
|
||||
|
||||
if (build.distribution === DistributionType.Internal) {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
const installUrl = getInternalDistributionInstallUrl(build);
|
||||
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
|
||||
// It's tricky to install the .apk file directly on Android so let's fallback
|
||||
// to the build details page and let people press the button to download there
|
||||
const qrcodeUrl =
|
||||
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
|
||||
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
|
||||
Log.log(`${indentString(code, 2)}\n`)
|
||||
);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} Open this link on your ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
|
||||
@@ -3,12 +3,14 @@ import assert from 'assert';
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
|
||||
}
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
if (project.__typename === 'App') {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/projects/${
|
||||
project.name
|
||||
}/builds/${build.id}`;
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
|
||||
} else {
|
||||
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { 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') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
|
||||
return;
|
||||
}
|
||||
const googleServicesFilePath = ctx.exp[ctx.platform]?.googleServicesFile;
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const rootDir = await vcs.getRootPathAsync();
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(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.`
|
||||
);
|
||||
Log.warn(
|
||||
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideDirectory(file: string, directory: string): boolean {
|
||||
return file.startsWith(directory);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfig, getDefaultTarget } from '@expo/config';
|
||||
import { ExpoConfig, getConfig, getDefaultTarget } from '@expo/config';
|
||||
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
@@ -10,11 +10,13 @@ import ora from 'ora';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
Actor,
|
||||
GetUpdateGroupAsyncQuery,
|
||||
Robot,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
User,
|
||||
ViewBranchQuery,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log from '../../log';
|
||||
@@ -22,7 +24,7 @@ import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUt
|
||||
import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUpdateInfoGroupAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
@@ -30,6 +32,7 @@ import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import vcs from '../../vcs';
|
||||
import { createUpdateBranchOnAppAsync } from './create';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
|
||||
@@ -63,6 +66,33 @@ async function getUpdateGroupAsync({
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
async function ensureBranchExists({
|
||||
appId,
|
||||
name: branchName,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
updates: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'];
|
||||
}> {
|
||||
const { app } = await viewUpdateBranchAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
const { id, updates } = updateBranch;
|
||||
return { id, updates };
|
||||
}
|
||||
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
return { id: newUpdateBranch.id, updates: [] };
|
||||
}
|
||||
|
||||
export default class BranchPublish extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a branch.';
|
||||
@@ -76,7 +106,7 @@ export default class BranchPublish extends Command {
|
||||
|
||||
static flags = {
|
||||
message: flags.string({
|
||||
description: 'Short message describing the updates.',
|
||||
description: 'short message describing the updates.',
|
||||
required: false,
|
||||
}),
|
||||
republish: flags.boolean({
|
||||
@@ -98,7 +128,7 @@ export default class BranchPublish extends Command {
|
||||
}),
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
description: `Only publish to a single platform`,
|
||||
description: `only publish to a single platform`,
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
@@ -107,13 +137,19 @@ export default class BranchPublish extends Command {
|
||||
description: `return a json with the new update group.`,
|
||||
default: false,
|
||||
}),
|
||||
auto: flags.boolean({
|
||||
description:
|
||||
'use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name },
|
||||
args: { name: branchName },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
@@ -134,32 +170,24 @@ export default class BranchPublish extends Command {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
let { runtimeVersion, sdkVersion } = exp;
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
// construct the runtimeVersion with special meaning indicating that the runtime is an
|
||||
// Expo SDK preset runtime that can be launched in Expo Go.
|
||||
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
|
||||
if (!runtimeVersion && sdkVersion && isManagedProject) {
|
||||
Log.withTick('Generating runtime version from sdk version');
|
||||
runtimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
|
||||
}
|
||||
const runtimeVersions = getRuntimeVersionObject(exp, platformFlag, projectDir);
|
||||
|
||||
if (!runtimeVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
if (!branchName && autoFlag) {
|
||||
branchName =
|
||||
(await vcs.getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`;
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
name = await selectAsync<string>(
|
||||
branchName = await selectAsync<string>(
|
||||
'which branch would you like to publish on?',
|
||||
branches.map(branch => {
|
||||
return {
|
||||
@@ -170,15 +198,15 @@ export default class BranchPublish extends Command {
|
||||
};
|
||||
})
|
||||
);
|
||||
assert(branchName, 'branch name must be specified.');
|
||||
}
|
||||
assert(name, 'branch name must be specified.');
|
||||
|
||||
const { id: branchId, updates } = await viewUpdateBranchAsync({
|
||||
const { id: branchId, updates } = await ensureBranchExists({
|
||||
appId: projectId,
|
||||
name,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
let updateInfoGroup: UpdateInfoGroup = {};
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
if (republish) {
|
||||
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
|
||||
@@ -206,7 +234,7 @@ export default class BranchPublish extends Command {
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,7 +250,7 @@ export default class BranchPublish extends Command {
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
@@ -245,7 +273,7 @@ export default class BranchPublish extends Command {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
updateInfoGroup[platform] = JSON.parse(manifestFragment);
|
||||
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
@@ -263,7 +291,7 @@ export default class BranchPublish extends Command {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = collectAssets({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets, exp);
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
@@ -271,6 +299,10 @@ export default class BranchPublish extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
if (!message && autoFlag) {
|
||||
message = (await vcs.getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
@@ -287,15 +319,38 @@ export default class BranchPublish extends Command {
|
||||
}));
|
||||
}
|
||||
|
||||
let newUpdateGroup;
|
||||
const runtimeToPlatformMapping: Record<string, string[]> = {};
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
|
||||
.filter(pair => pair[1] === runtime)
|
||||
.map(pair => pair[0]);
|
||||
}
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
|
||||
])
|
||||
);
|
||||
|
||||
if (republish && !oldRuntimeVersion) {
|
||||
throw new Error(
|
||||
'Can not find the runtime version of the update group that is being republished.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
message,
|
||||
};
|
||||
});
|
||||
let newUpdates;
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdateGroup = await PublishMutation.publishUpdateGroupAsync({
|
||||
branchId,
|
||||
updateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion! : runtimeVersion,
|
||||
message,
|
||||
});
|
||||
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to published updates');
|
||||
@@ -303,27 +358,118 @@ export default class BranchPublish extends Command {
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newUpdateGroup));
|
||||
Log.log(JSON.stringify(newUpdates));
|
||||
} else {
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'branch', value: name },
|
||||
{ label: 'runtime version', value: runtimeVersion },
|
||||
{ label: 'update group ID', value: newUpdateGroup.group },
|
||||
{ label: 'message', value: message },
|
||||
])
|
||||
);
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
const platforms = newUpdates
|
||||
.filter(update => update.runtimeVersion === runtime)
|
||||
.map(update => update.platform);
|
||||
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
|
||||
if (!newUpdate) {
|
||||
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
|
||||
}
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'branch', value: branchName },
|
||||
{ label: 'runtime version', value: runtime },
|
||||
{ label: 'platform', value: platforms.join(',') },
|
||||
{ label: 'update group ID', value: newUpdate.group },
|
||||
{ label: 'message', value: message },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
update: Pick<Update, 'message' | 'createdAt' | 'runtimeVersion'> & {
|
||||
actor?: Pick<Actor, 'firstName'> | null;
|
||||
function getRuntimeVersionObject(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: string,
|
||||
projectDir: string
|
||||
): Record<string, string> {
|
||||
let { runtimeVersion: defaultRuntimeVersion, sdkVersion } = exp;
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
// construct the runtimeVersion with special meaning indicating that the runtime is an
|
||||
// Expo SDK preset runtime that can be launched in Expo Go.
|
||||
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
|
||||
if (!defaultRuntimeVersion && sdkVersion && isManagedProject) {
|
||||
Log.withTick('Generating runtime version from sdk version');
|
||||
defaultRuntimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
|
||||
}
|
||||
const iOSRuntimeVersion = (exp as any).ios?.runtimeVersion; // TODO-JJ remove cast to any
|
||||
const androidRuntimeVersion = (exp as any).android?.runtimeVersion; // TODO-JJ remove cast to any
|
||||
let runtimeVersions: Record<string, string>;
|
||||
switch (platformFlag) {
|
||||
case 'ios': {
|
||||
runtimeVersions = {
|
||||
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
runtimeVersions = {
|
||||
android: androidRuntimeVersion ?? defaultRuntimeVersion,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'all': {
|
||||
runtimeVersions = {
|
||||
ios: iOSRuntimeVersion ?? defaultRuntimeVersion,
|
||||
android: androidRuntimeVersion ?? defaultRuntimeVersion,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Platform flag must be "ios", "android", or "all"');
|
||||
}
|
||||
if (Object.values(runtimeVersions).some(runtime => !runtime)) {
|
||||
throw new Error(
|
||||
"Couldn't find a 'runtimeVersion' for every platform. Please specify it under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
if (Object.values(runtimeVersions).some(runtime => typeof runtime !== 'string')) {
|
||||
throw new Error(
|
||||
`Please ensure that all of the runtime versions defined in the app.json are strings.`
|
||||
);
|
||||
}
|
||||
|
||||
return runtimeVersions;
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
update: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'][number]
|
||||
): string {
|
||||
const { message, createdAt, actor, runtimeVersion } = update;
|
||||
return `[${dateFormat(createdAt, 'mmm dd HH:MM')} by ${
|
||||
actor?.firstName ?? 'unknown'
|
||||
}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
|
||||
let actorName: string;
|
||||
switch (actor?.__typename) {
|
||||
case 'User': {
|
||||
actorName = (actor as Pick<User, 'username' | 'id'>).username;
|
||||
break;
|
||||
}
|
||||
case 'Robot': {
|
||||
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
|
||||
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
actorName = 'unknown';
|
||||
}
|
||||
return `[${dateFormat(
|
||||
createdAt,
|
||||
'mmm dd HH:MM'
|
||||
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
}
|
||||
@@ -6,15 +6,7 @@ import gql from 'graphql-tag';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
Maybe,
|
||||
Robot,
|
||||
Update,
|
||||
UpdateBranch,
|
||||
User,
|
||||
ViewBranchQuery,
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { ViewBranchQuery, ViewBranchQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -26,16 +18,7 @@ const PAGE_LIMIT = 10_000;
|
||||
export async function viewUpdateBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<
|
||||
Pick<UpdateBranch, 'id' | 'name'> & {
|
||||
updates: (Pick<
|
||||
Update,
|
||||
'id' | 'group' | 'message' | 'createdAt' | 'runtimeVersion' | 'platform' | 'manifestFragment'
|
||||
> & {
|
||||
actor?: Maybe<Pick<User, 'firstName' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
})[];
|
||||
}
|
||||
> {
|
||||
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<ViewBranchQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<ViewBranchQuery, ViewBranchQueryVariables>(
|
||||
@@ -78,11 +61,7 @@ export async function viewUpdateBranchAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const updateBranch = data.app?.byId.updateBranchByName;
|
||||
if (!updateBranch) {
|
||||
throw new Error(`Could not find branch "${name}"`);
|
||||
}
|
||||
return updateBranch;
|
||||
return data;
|
||||
}
|
||||
|
||||
export default class BranchView extends Command {
|
||||
@@ -131,10 +110,14 @@ export default class BranchView extends Command {
|
||||
}));
|
||||
}
|
||||
|
||||
const UpdateBranch = await viewUpdateBranchAsync({
|
||||
const { app } = await viewUpdateBranchAsync({
|
||||
appId: projectId,
|
||||
name,
|
||||
});
|
||||
const UpdateBranch = app?.byId.updateBranchByName;
|
||||
if (!UpdateBranch) {
|
||||
throw new Error(`Could not find branch "${name}"`);
|
||||
}
|
||||
|
||||
const updates = Object.values(groupBy(UpdateBranch.updates, u => u.group)).map(
|
||||
group => group[0]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -56,7 +56,7 @@ function logSuccess(platform: RequestedPlatform) {
|
||||
|
||||
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
|
||||
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
|
||||
- ${learnMore('https://docs.expo.io/build/introduction', {
|
||||
- ${learnMore('https://docs.expo.dev/build/introduction', {
|
||||
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
|
||||
})}`);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
interface RawBuildFlags {
|
||||
@@ -37,6 +38,7 @@ interface RawBuildFlags {
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
'clear-cache': boolean;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
interface BuildFlags {
|
||||
@@ -47,6 +49,7 @@ interface BuildFlags {
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
@@ -58,6 +61,10 @@ export default class Build extends EasCommand {
|
||||
default: false,
|
||||
hidden: true,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
default: false,
|
||||
}),
|
||||
'skip-project-configuration': flags.boolean({
|
||||
default: false,
|
||||
description: 'Skip project configuration',
|
||||
@@ -87,6 +94,9 @@ export default class Build extends EasCommand {
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags: rawFlags } = this.parse(Build);
|
||||
if (rawFlags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const { requestedPlatform } = flags;
|
||||
|
||||
@@ -139,7 +149,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
if (flags.wait) {
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
|
||||
printBuildResults(builds);
|
||||
printBuildResults(builds, flags.json);
|
||||
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
}
|
||||
}
|
||||
@@ -149,6 +159,9 @@ export default class Build extends EasCommand {
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('--platform is required when building in non-interactive mode');
|
||||
}
|
||||
if (flags.json && !nonInteractive) {
|
||||
throw new Error('--json is allowed only when building in non-interactive mode');
|
||||
}
|
||||
const requestedPlatform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
|
||||
|
||||
@@ -176,6 +189,7 @@ export default class Build extends EasCommand {
|
||||
local: flags['local'],
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
json: flags['json'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,7 +246,7 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
if (nonInteractive) {
|
||||
Log.error('We detected that your eas.json is using a deprecated format.');
|
||||
Log.error(
|
||||
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.io/build/eas-json'
|
||||
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
@@ -254,7 +268,7 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
}
|
||||
} else {
|
||||
Log.error(
|
||||
"Aborting, update your eas.json according to https://docs.expo.io/build/eas-json and run 'eas build' again"
|
||||
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
|
||||
);
|
||||
error('Unsupported eas.json format', { exit: 1 });
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildList extends Command {
|
||||
static description = 'list all builds for your project';
|
||||
@@ -25,6 +26,9 @@ export default class BuildList extends Command {
|
||||
platform: flags.enum({
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
}),
|
||||
status: flags.enum({
|
||||
options: [
|
||||
BuildStatus.NEW,
|
||||
@@ -56,11 +60,15 @@ export default class BuildList extends Command {
|
||||
async run() {
|
||||
const { flags } = this.parse(BuildList);
|
||||
const {
|
||||
json,
|
||||
platform: requestedPlatform,
|
||||
status: buildStatus,
|
||||
distribution: buildDistribution,
|
||||
limit = 10,
|
||||
} = flags;
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const platform = toAppPlatform(requestedPlatform);
|
||||
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
|
||||
@@ -100,11 +108,15 @@ export default class BuildList extends Command {
|
||||
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
|
||||
}
|
||||
|
||||
const list = builds
|
||||
.map(build => formatGraphQLBuild(build))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
if (json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
const list = builds
|
||||
.map(build => formatGraphQLBuild(build))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
|
||||
Log.log(`\n${list}`);
|
||||
Log.log(`\n${list}`);
|
||||
}
|
||||
} else {
|
||||
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
@@ -11,14 +11,27 @@ import {
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class BuildView extends Command {
|
||||
static description = 'view a build for your project';
|
||||
|
||||
static args = [{ name: 'BUILD_ID' }];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { BUILD_ID: buildId } = this.parse(BuildView).args;
|
||||
const {
|
||||
args: { BUILD_ID: buildId },
|
||||
flags,
|
||||
} = this.parse(BuildView);
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -47,7 +60,11 @@ export default class BuildView extends Command {
|
||||
spinner.succeed(`Showing the last build for the project ${projectName}`);
|
||||
}
|
||||
|
||||
Log.log(`\n${formatGraphQLBuild(build)}`);
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(build);
|
||||
} else {
|
||||
Log.log(`\n${formatGraphQLBuild(build)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (buildId) {
|
||||
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } 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';
|
||||
import { displayLogsAsync } from '../submissions/utils/logs';
|
||||
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
|
||||
import { waitForSubmissionsEndAsync } from '../submissions/utils/wait';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
const ANDROID_FLAGS = 'Android specific options';
|
||||
const IOS_FLAGS = 'iOS specific options';
|
||||
|
||||
interface Flags {
|
||||
verbose: boolean;
|
||||
wait: boolean;
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
iosOptions: Partial<IosSubmitCommandFlags>;
|
||||
androidOptions: Partial<AndroidSubmitCommandFlags>;
|
||||
}
|
||||
|
||||
export default class BuildSubmit extends EasCommand {
|
||||
static description = 'Submits build artifact to app store';
|
||||
static usage = 'submit --platform=(android|ios)';
|
||||
@@ -41,6 +58,9 @@ export default class BuildSubmit extends EasCommand {
|
||||
}),
|
||||
|
||||
/* Common flags for both platforms */
|
||||
profile: flags.string({
|
||||
description: 'Name of the submit profile from eas.json',
|
||||
}),
|
||||
latest: flags.boolean({
|
||||
description: 'Submit the latest build for specified platform',
|
||||
exclusive: ['id', 'path', 'url'],
|
||||
@@ -65,7 +85,6 @@ export default class BuildSubmit extends EasCommand {
|
||||
|
||||
verbose: flags.boolean({
|
||||
description: 'Always print logs from Submission Service',
|
||||
default: false,
|
||||
helpLabel: COMMON_FLAGS,
|
||||
}),
|
||||
|
||||
@@ -77,7 +96,8 @@ export default class BuildSubmit extends EasCommand {
|
||||
}),
|
||||
|
||||
key: flags.string({
|
||||
description: 'Path to the JSON key used to authenticate with Google Play',
|
||||
description:
|
||||
'Path to the JSON file with service account key used to authenticate with Google Play',
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
'android-package': flags.string({
|
||||
@@ -86,14 +106,17 @@ export default class BuildSubmit extends EasCommand {
|
||||
}),
|
||||
|
||||
track: flags.enum({
|
||||
description: 'The track of the application to use',
|
||||
default: 'internal',
|
||||
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: 'Release status (used when uploading new APKs/AABs)',
|
||||
default: 'completed',
|
||||
description: '[default: completed] Release status (used when uploading new APKs/AABs)',
|
||||
options: ['completed', 'draft', 'halted', 'inProgress'],
|
||||
helpLabel: ANDROID_FLAGS,
|
||||
}),
|
||||
@@ -129,8 +152,7 @@ export default class BuildSubmit extends EasCommand {
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
language: flags.string({
|
||||
description: 'Primary language (e.g. English, German, ...)',
|
||||
default: 'en-US',
|
||||
description: '[default: en-US] Primary language (e.g. English, German, ...)',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
'company-name': flags.string({
|
||||
@@ -138,14 +160,124 @@ export default class BuildSubmit extends EasCommand {
|
||||
'The name of your company, needed only for the first upload of any app to App Store',
|
||||
helpLabel: IOS_FLAGS,
|
||||
}),
|
||||
wait: flags.boolean({
|
||||
default: true,
|
||||
allowNo: true,
|
||||
description: 'Wait for submission to complete',
|
||||
}),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const flags = this.parseFlags();
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await this.promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (platform === AppPlatform.Android) {
|
||||
const submitProfile = flags.profile
|
||||
? await easJsonReader.readSubmitProfileAsync(flags.profile, Platform.ANDROID)
|
||||
: {};
|
||||
const commandFlags: AndroidSubmitCommandFlags = {
|
||||
releaseStatus: 'completed',
|
||||
track: 'internal',
|
||||
changesNotSentForReview: false,
|
||||
...submitProfile,
|
||||
...flags.androidOptions,
|
||||
};
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
submissions.push(await command.runAsync());
|
||||
} else {
|
||||
const submitProfile = flags.profile
|
||||
? await easJsonReader.readSubmitProfileAsync(flags.profile, Platform.IOS)
|
||||
: {};
|
||||
const commandFlags: IosSubmitCommandFlags = {
|
||||
language: 'en-US',
|
||||
...submitProfile,
|
||||
...flags.iosOptions,
|
||||
};
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
});
|
||||
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
|
||||
) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!\n') +
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.\n' +
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.\n' +
|
||||
'- When it’s done, you can see your build here' +
|
||||
learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${flags.iosOptions.ascAppId}/appstore/ios`,
|
||||
{
|
||||
learnMoreMessage: '',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
await displayLogsAsync(submission, { verbose: flags.verbose });
|
||||
if (completedSubmissions.length > 1) {
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
this.exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
}
|
||||
}
|
||||
|
||||
private parseFlags(): Flags {
|
||||
const {
|
||||
flags: {
|
||||
platform,
|
||||
profile,
|
||||
verbose,
|
||||
wait,
|
||||
|
||||
// android
|
||||
'android-package': androidPackage,
|
||||
'release-status': releaseStatus,
|
||||
'changes-not-sent-for-review': changesNotSentForReview,
|
||||
track,
|
||||
type,
|
||||
key: serviceAccountKeyPath,
|
||||
|
||||
// ios
|
||||
'apple-id': appleId,
|
||||
@@ -159,71 +291,56 @@ export default class BuildSubmit extends EasCommand {
|
||||
...flags
|
||||
},
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Make this work outside project dir
|
||||
if (!projectDir) {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
if (platform === AppPlatform.Android) {
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
androidPackage,
|
||||
releaseStatus,
|
||||
return {
|
||||
platform: platform as Platform,
|
||||
profile,
|
||||
androidOptions: {
|
||||
...(androidPackage && { androidPackage }),
|
||||
...(releaseStatus && { releaseStatus }),
|
||||
...(track && { track }),
|
||||
...(changesNotSentForReview && { changesNotSentForReview }),
|
||||
...(serviceAccountKeyPath && { serviceAccountKeyPath }),
|
||||
...flags,
|
||||
};
|
||||
|
||||
const ctx = AndroidSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
} else if (platform === AppPlatform.Ios) {
|
||||
const options: IosSubmitCommandFlags = {
|
||||
appleId,
|
||||
ascAppId,
|
||||
appleTeamId,
|
||||
appName,
|
||||
bundleIdentifier,
|
||||
companyName,
|
||||
},
|
||||
iosOptions: {
|
||||
...(appleId && { appleId }),
|
||||
...(ascAppId && { ascAppId }),
|
||||
...(appleTeamId && { appleTeamId }),
|
||||
...(appName && { appName }),
|
||||
...(bundleIdentifier && { bundleIdentifier }),
|
||||
...(companyName && { companyName }),
|
||||
...flags,
|
||||
};
|
||||
},
|
||||
verbose,
|
||||
wait,
|
||||
};
|
||||
}
|
||||
|
||||
const ctx = IosSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ export class RemoveKeystore {
|
||||
)
|
||||
);
|
||||
Log.warn(
|
||||
'Please read https://docs.expo.io/distribution/building-standalone-apps/#if-you-choose-to-build-for-android for more info before proceeding.'
|
||||
'Please read https://docs.expo.dev/distribution/building-standalone-apps/#if-you-choose-to-build-for-android for more info before proceeding.'
|
||||
);
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function validateKeystoreAsync(keystore: Keystore): Promise<void> {
|
||||
keystore.keyAlias,
|
||||
]);
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
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';
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ async function generateDistributionCertificateAsync(
|
||||
`✅ Distribution Certificates can be revoked with no side effects for App Store builds.`
|
||||
)
|
||||
);
|
||||
Log.log(learnMore('https://docs.expo.io/distribution/app-signing/#summary'));
|
||||
Log.log(learnMore('https://docs.expo.dev/distribution/app-signing/#summary'));
|
||||
Log.newLine();
|
||||
|
||||
const { distCertsToRevoke } = await promptAsync({
|
||||
|
||||
@@ -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.');
|
||||
@@ -80,7 +80,7 @@ async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<
|
||||
}
|
||||
|
||||
Log.log(chalk.grey(`⚠️ Revoking a Push Key will affect other apps that rely on it`));
|
||||
Log.log(learnMore('https://docs.expo.io/distribution/app-signing/#push-notification-keys'));
|
||||
Log.log(learnMore('https://docs.expo.dev/distribution/app-signing/#push-notification-keys'));
|
||||
Log.log();
|
||||
|
||||
const { pushKeysToRevoke } = await promptAsync({
|
||||
|
||||
@@ -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"
|
||||
`;
|
||||
+1
-1
@@ -32,7 +32,7 @@ const IosAppCredentialsQuery = {
|
||||
},
|
||||
appSpecificPassword: {
|
||||
id: '64bdd007-da7b-4190-a571-edb87e9324a1',
|
||||
appleIdUsername: 'quin@expo.io',
|
||||
appleIdUsername: 'quin@expo.dev',
|
||||
passwordLabel: 'description for super secret password',
|
||||
updatedAt: '2020-10-13T18:39:04.463Z',
|
||||
__typename: 'AppleAppSpecificPassword',
|
||||
|
||||
@@ -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 "${
|
||||
@@ -152,7 +152,14 @@ export async function ensureAppExistsAsync(
|
||||
language,
|
||||
companyName,
|
||||
bundleIdentifier,
|
||||
}: { name: string; language?: string; companyName?: string; bundleIdentifier: string }
|
||||
sku,
|
||||
}: {
|
||||
name: string;
|
||||
language?: string;
|
||||
companyName?: string;
|
||||
bundleIdentifier: string;
|
||||
sku?: string;
|
||||
}
|
||||
) {
|
||||
const context = getRequestContext(authCtx);
|
||||
const spinner = ora(`Linking to App Store ${chalk.dim(bundleIdentifier)}`).start();
|
||||
@@ -170,8 +177,9 @@ export async function ensureAppExistsAsync(
|
||||
name,
|
||||
primaryLocale: language,
|
||||
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 (
|
||||
|
||||
@@ -81,11 +81,11 @@ export async function promptPasswordAsync({
|
||||
|
||||
if (cachedPassword) {
|
||||
Log.log(`\u203A Using password for ${username} from your local Keychain`);
|
||||
Log.log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
|
||||
Log.log(` ${learnMore('https://docs.expo.dev/distribution/security#keychain')}`);
|
||||
return cachedPassword;
|
||||
}
|
||||
|
||||
// https://docs.expo.io/distribution/security/#apple-developer-account-credentials
|
||||
// https://docs.expo.dev/distribution/security/#apple-developer-account-credentials
|
||||
Log.log(
|
||||
wrapAnsi(
|
||||
chalk.bold(
|
||||
@@ -160,7 +160,7 @@ async function cachePasswordAsync({ username, password }: Auth.UserCredentials):
|
||||
}
|
||||
|
||||
Log.log(`\u203A Saving Apple ID password to the local Keychain`);
|
||||
Log.log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
|
||||
Log.log(` ${learnMore('https://docs.expo.dev/distribution/security#keychain')}`);
|
||||
const serviceName = getKeychainServiceName(username);
|
||||
return Keychain.setPasswordAsync({ username, password, serviceName });
|
||||
}
|
||||
+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: quin@expo.io 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: quin@expo.dev 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;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
const buildProfileNames = await this.easJsonReader.getBuildProfileNamesAsync();
|
||||
if (buildProfileNames.length === 0) {
|
||||
throw new Error(
|
||||
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.io/build/eas-json/ for more details'
|
||||
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details'
|
||||
);
|
||||
} else if (buildProfileNames.length === 1) {
|
||||
return buildProfileNames[0];
|
||||
|
||||
@@ -14,7 +14,9 @@ export async function runRegistrationUrlMethodAsync(
|
||||
): Promise<void> {
|
||||
const registrationURL = await generateDeviceRegistrationURLAsync(accountId, appleTeam);
|
||||
Log.newLine();
|
||||
qrcodeTerminal.generate(registrationURL, code => Log.log(`${indentString(code, 2)}\n`));
|
||||
qrcodeTerminal.generate(registrationURL, { small: true }, code =>
|
||||
Log.log(`${indentString(code, 2)}\n`)
|
||||
);
|
||||
Log.log(
|
||||
'Open the following link on your iOS devices (or scan the QR code) and follow the instructions to install the development profile:'
|
||||
);
|
||||
|
||||
+104
-30
@@ -438,6 +438,7 @@ export type App = Project & {
|
||||
submissions: Array<Submission>;
|
||||
/** Deployments associated with this app */
|
||||
deployments: Array<Deployment>;
|
||||
deployment?: Maybe<Deployment>;
|
||||
/** iOS app credentials for the project */
|
||||
iosAppCredentials: Array<IosAppCredentials>;
|
||||
/** Android app credentials for the project */
|
||||
@@ -484,6 +485,7 @@ export type App = Project & {
|
||||
users?: Maybe<Array<Maybe<User>>>;
|
||||
/** @deprecated Field no longer supported */
|
||||
releases: Array<Maybe<AppRelease>>;
|
||||
latestReleaseForReleaseChannel?: Maybe<AppRelease>;
|
||||
};
|
||||
|
||||
|
||||
@@ -523,8 +525,16 @@ export type AppSubmissionsArgs = {
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppDeploymentsArgs = {
|
||||
limit: Scalars['Int'];
|
||||
buildListMaxSize: Scalars['Int'];
|
||||
mostRecentlyUpdatedAt?: Maybe<Scalars['DateTime']>;
|
||||
options?: Maybe<DeploymentOptions>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppDeploymentArgs = {
|
||||
channel: Scalars['String'];
|
||||
runtimeVersion: Scalars['String'];
|
||||
options?: Maybe<DeploymentOptions>;
|
||||
};
|
||||
|
||||
|
||||
@@ -600,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'];
|
||||
@@ -884,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;
|
||||
@@ -898,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>;
|
||||
};
|
||||
@@ -969,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 = {
|
||||
@@ -1010,14 +1030,20 @@ export type SubmissionFilter = {
|
||||
status?: Maybe<SubmissionStatus>;
|
||||
};
|
||||
|
||||
export type DeploymentOptions = {
|
||||
/** Max number of associated builds to return */
|
||||
buildListMaxSize?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
|
||||
export type Deployment = {
|
||||
__typename?: 'Deployment';
|
||||
id?: Maybe<Scalars['String']>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
recentBuilds?: Maybe<Array<Maybe<Build>>>;
|
||||
branchToReceive?: Maybe<UpdateBranch>;
|
||||
id: Scalars['ID'];
|
||||
runtimeVersion: Scalars['String'];
|
||||
channel: Scalars['String'];
|
||||
recentBuilds: Array<Build>;
|
||||
branchMapping?: Maybe<UpdateBranch>;
|
||||
mostRecentlyUpdatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type UpdateBranch = {
|
||||
@@ -1028,7 +1054,6 @@ export type UpdateBranch = {
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
updates: Array<Update>;
|
||||
mostRecentUpdateForEachPlatform: Array<Update>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1208,6 +1233,7 @@ export type AndroidAppCredentials = {
|
||||
app: App;
|
||||
applicationIdentifier?: Maybe<Scalars['String']>;
|
||||
androidFcm?: Maybe<AndroidFcm>;
|
||||
googleServiceAccountKeyForSubmissions?: Maybe<GoogleServiceAccountKey>;
|
||||
androidAppBuildCredentialsList: Array<AndroidAppBuildCredentials>;
|
||||
isLegacy: Scalars['Boolean'];
|
||||
/** @deprecated use androidAppBuildCredentialsList instead */
|
||||
@@ -1250,6 +1276,18 @@ export enum AndroidFcmVersion {
|
||||
V1 = 'V1'
|
||||
}
|
||||
|
||||
export type GoogleServiceAccountKey = {
|
||||
__typename?: 'GoogleServiceAccountKey';
|
||||
id: Scalars['ID'];
|
||||
account: Account;
|
||||
projectIdentifier: Scalars['String'];
|
||||
privateKeyIdentifier: Scalars['String'];
|
||||
clientEmail: Scalars['String'];
|
||||
clientIdentifier: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type AndroidAppBuildCredentials = {
|
||||
__typename?: 'AndroidAppBuildCredentials';
|
||||
id: Scalars['ID'];
|
||||
@@ -2879,10 +2917,15 @@ 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;
|
||||
/** Create an Android EAS Submit submission */
|
||||
createAndroidSubmission: CreateSubmissionResult;
|
||||
};
|
||||
|
||||
|
||||
@@ -2895,6 +2938,11 @@ export type SubmissionMutationCreateIosSubmissionArgs = {
|
||||
input: CreateIosSubmissionInput;
|
||||
};
|
||||
|
||||
|
||||
export type SubmissionMutationCreateAndroidSubmissionArgs = {
|
||||
input: CreateAndroidSubmissionInput;
|
||||
};
|
||||
|
||||
export type CreateSubmissionInput = {
|
||||
appId: Scalars['ID'];
|
||||
platform: AppPlatform;
|
||||
@@ -2910,18 +2958,35 @@ export type CreateSubmissionResult = {
|
||||
|
||||
export type CreateIosSubmissionInput = {
|
||||
appId: Scalars['ID'];
|
||||
config: NewIosSubmissionConfig;
|
||||
config: IosSubmissionConfigInput;
|
||||
submittedBuildId?: Maybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
export type NewIosSubmissionConfig = {
|
||||
export type IosSubmissionConfigInput = {
|
||||
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
|
||||
appleAppSpecificPassword?: Maybe<Scalars['String']>;
|
||||
archiveUrl: Scalars['String'];
|
||||
archiveUrl?: Maybe<Scalars['String']>;
|
||||
appleIdUsername: Scalars['String'];
|
||||
ascAppIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateAndroidSubmissionInput = {
|
||||
appId: Scalars['ID'];
|
||||
config: AndroidSubmissionConfigInput;
|
||||
submittedBuildId?: Maybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
export type AndroidSubmissionConfigInput = {
|
||||
googleServiceAccountKeyId?: Maybe<Scalars['String']>;
|
||||
googleServiceAccountKeyJson?: Maybe<Scalars['String']>;
|
||||
archiveUrl?: Maybe<Scalars['String']>;
|
||||
archiveType?: Maybe<SubmissionAndroidArchiveType>;
|
||||
applicationIdentifier: Scalars['String'];
|
||||
track: SubmissionAndroidTrack;
|
||||
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
|
||||
changesNotSentForReview?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type UpdateChannelMutation = {
|
||||
__typename?: 'UpdateChannelMutation';
|
||||
/**
|
||||
@@ -2993,7 +3058,7 @@ export type UpdateBranchMutation = {
|
||||
/** Delete an EAS branch and all of its updates as long as the branch is not being used by any channels */
|
||||
deleteUpdateBranch: DeleteUpdateBranchResult;
|
||||
/** Publish an update group to a branch */
|
||||
publishUpdateGroup: Array<Update>;
|
||||
publishUpdateGroups: Array<Update>;
|
||||
};
|
||||
|
||||
|
||||
@@ -3013,8 +3078,8 @@ export type UpdateBranchMutationDeleteUpdateBranchArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type UpdateBranchMutationPublishUpdateGroupArgs = {
|
||||
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
|
||||
export type UpdateBranchMutationPublishUpdateGroupsArgs = {
|
||||
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
|
||||
};
|
||||
|
||||
export type EditUpdateBranchInput = {
|
||||
@@ -3052,7 +3117,7 @@ export type PartialManifestAsset = {
|
||||
fileSHA256: Scalars['String'];
|
||||
bundleKey: Scalars['String'];
|
||||
contentType: Scalars['String'];
|
||||
storageBucket: Scalars['String'];
|
||||
fileExtension?: Maybe<Scalars['String']>;
|
||||
storageKey: Scalars['String'];
|
||||
};
|
||||
|
||||
@@ -3222,6 +3287,7 @@ export type MeMutationLeaveAccountArgs = {
|
||||
|
||||
export type MeMutationInitiateSecondFactorAuthenticationArgs = {
|
||||
deviceConfigurations: Array<Maybe<SecondFactorDeviceConfiguration>>;
|
||||
recaptchaResponseToken?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -4770,7 +4836,7 @@ export type GetSignedUploadMutation = (
|
||||
);
|
||||
|
||||
export type UpdatePublishMutationVariables = Exact<{
|
||||
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
|
||||
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -4778,9 +4844,9 @@ export type UpdatePublishMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateBranch: (
|
||||
{ __typename?: 'UpdateBranchMutation' }
|
||||
& { publishUpdateGroup: Array<(
|
||||
& { publishUpdateGroups: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group'>
|
||||
& Pick<Update, 'id' | 'group' | 'runtimeVersion' | 'platform'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
@@ -4802,6 +4868,7 @@ export type CreateSubmissionMutation = (
|
||||
& { submission: (
|
||||
{ __typename?: 'Submission' }
|
||||
& Pick<Submission, 'id'>
|
||||
& SubmissionFragment
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
@@ -5135,7 +5202,14 @@ 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'>
|
||||
) }
|
||||
), error?: Maybe<(
|
||||
{ __typename?: 'SubmissionError' }
|
||||
& Pick<SubmissionError, 'errorCode' | 'message'>
|
||||
)> }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { PublishUpdateGroupInput, Update, UpdatePublishMutation } from '../generated';
|
||||
import { PublishUpdateGroupInput, UpdatePublishMutation } from '../generated';
|
||||
|
||||
const PublishMutation = {
|
||||
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
|
||||
@@ -30,27 +30,28 @@ const PublishMutation = {
|
||||
},
|
||||
|
||||
async publishUpdateGroupAsync(
|
||||
publishUpdateGroupInput: PublishUpdateGroupInput
|
||||
): Promise<Pick<Update, 'group'>> {
|
||||
publishUpdateGroupsInput: PublishUpdateGroupInput[]
|
||||
): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdatePublishMutation>(
|
||||
gql`
|
||||
mutation UpdatePublishMutation($publishUpdateGroupInput: PublishUpdateGroupInput) {
|
||||
mutation UpdatePublishMutation($publishUpdateGroupsInput: [PublishUpdateGroupInput!]!) {
|
||||
updateBranch {
|
||||
publishUpdateGroup(publishUpdateGroupInput: $publishUpdateGroupInput) {
|
||||
publishUpdateGroups(publishUpdateGroupsInput: $publishUpdateGroupsInput) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ publishUpdateGroupInput }
|
||||
{ publishUpdateGroupsInput }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const { group } = data.updateBranch.publishUpdateGroup[0]!;
|
||||
return { group };
|
||||
return data.updateBranch.publishUpdateGroups;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -7,12 +8,9 @@ import {
|
||||
AppPlatform,
|
||||
CreateSubmissionMutation,
|
||||
CreateSubmissionMutationVariables,
|
||||
Submission,
|
||||
SubmissionFragment,
|
||||
} from '../generated';
|
||||
|
||||
interface CreateSubmissionResult {
|
||||
submission: Pick<Submission, 'id'>;
|
||||
}
|
||||
import { SubmissionFragmentNode } from '../types/Submission';
|
||||
|
||||
export const SubmissionMutation = {
|
||||
async createSubmissionAsync(input: {
|
||||
@@ -20,7 +18,7 @@ export const SubmissionMutation = {
|
||||
submittedBuildId?: string;
|
||||
platform: AppPlatform;
|
||||
config: JSONObject;
|
||||
}): Promise<CreateSubmissionResult> {
|
||||
}): Promise<SubmissionFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateSubmissionMutation, CreateSubmissionMutationVariables>(
|
||||
@@ -42,15 +40,17 @@ export const SubmissionMutation = {
|
||||
) {
|
||||
submission {
|
||||
id
|
||||
...SubmissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(SubmissionFragmentNode)}
|
||||
`,
|
||||
input
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return nullthrows(data.submission?.createSubmission);
|
||||
return nullthrows(data.submission?.createSubmission.submission);
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,14 @@ export const SubmissionFragmentNode = gql`
|
||||
id
|
||||
status
|
||||
platform
|
||||
app {
|
||||
id
|
||||
name
|
||||
ownerAccount {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
error {
|
||||
errorCode
|
||||
message
|
||||
|
||||
@@ -85,7 +85,7 @@ export default class Log {
|
||||
/**
|
||||
* Format links as dim (unless disabled) with an underline.
|
||||
*
|
||||
* @example Learn more: https://expo.io
|
||||
* @example Learn more: https://expo.dev
|
||||
* @param url
|
||||
*/
|
||||
export function learnMore(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
import {
|
||||
MetadataJoi,
|
||||
TIMEOUT_LIMIT,
|
||||
buildUpdateInfoGroupAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
@@ -131,23 +131,23 @@ describe(convertAssetToUpdateInfoGroupFormatAsync, () => {
|
||||
it('resolves to the correct value', async () => {
|
||||
const path = uuidv4();
|
||||
fs.writeFileSync(path, 'I am pretending to be a jpeg');
|
||||
const type = 'jpg';
|
||||
const fileExtension = '.jpg';
|
||||
const asset = {
|
||||
type,
|
||||
fileExtension,
|
||||
contentType: 'image/jpeg',
|
||||
path,
|
||||
};
|
||||
await expect(convertAssetToUpdateInfoGroupFormatAsync(asset)).resolves.toEqual({
|
||||
bundleKey: 'c939e759656f577c058f445bfb19182e',
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(buildUpdateInfoGroupAsync, () => {
|
||||
describe(buildUnsortedUpdateInfoGroupAsync, () => {
|
||||
const androidBundlePath = uuidv4();
|
||||
const assetPath = uuidv4();
|
||||
fs.writeFileSync(androidBundlePath, 'I am a js bundle');
|
||||
@@ -155,17 +155,17 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
|
||||
it('returns the correct value', async () => {
|
||||
await expect(
|
||||
buildUpdateInfoGroupAsync(
|
||||
buildUnsortedUpdateInfoGroupAsync(
|
||||
{
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [
|
||||
{
|
||||
type: 'jpg',
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
},
|
||||
@@ -182,17 +182,17 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
assets: [
|
||||
{
|
||||
bundleKey: 'c939e759656f577c058f445bfb19182e',
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
},
|
||||
],
|
||||
launchAsset: {
|
||||
bundleKey: 'ec0dd14670aae108f99a810df9c1482c',
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
fileSHA256: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
|
||||
},
|
||||
extra: {
|
||||
@@ -232,7 +232,7 @@ describe(collectAssets, () => {
|
||||
|
||||
const userDefinedAssets = [
|
||||
{
|
||||
type: 'jpg',
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
|
||||
},
|
||||
@@ -267,7 +267,7 @@ describe(collectAssets, () => {
|
||||
expect(collectAssets({ inputDir, platforms: defaultPublishPlatforms })).toEqual({
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/android.js`),
|
||||
},
|
||||
@@ -275,7 +275,7 @@ describe(collectAssets, () => {
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/ios.js`),
|
||||
},
|
||||
@@ -286,7 +286,7 @@ describe(collectAssets, () => {
|
||||
expect(collectAssets({ inputDir, platforms: ['ios'] })).toEqual({
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/ios.js`),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
import { getProjectDashboardUrl } from '../build/utils/url';
|
||||
import { AppPrivacy } from '../graphql/generated';
|
||||
import { AppMutation } from '../graphql/mutations/AppMutation';
|
||||
import { ProjectQuery } from '../graphql/queries/ProjectQuery';
|
||||
@@ -26,12 +28,14 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
assert(account, `You must have access to the ${accountName} account to run this command`);
|
||||
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const projectLink = terminalLink(projectFullName, projectDashboardUrl);
|
||||
|
||||
const spinner = ora(`Linking to project ${chalk.bold(projectFullName)}`).start();
|
||||
|
||||
const maybeId = await findProjectIdByAccountNameAndSlugNullableAsync(accountName, projectName);
|
||||
if (maybeId) {
|
||||
spinner.succeed(`Linked to project ${chalk.bold(projectFullName)}`);
|
||||
spinner.succeed(`Linked to project ${chalk.bold(projectLink)}`);
|
||||
return maybeId;
|
||||
}
|
||||
|
||||
@@ -42,7 +46,7 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
projectName,
|
||||
privacy: projectInfo.privacy,
|
||||
});
|
||||
spinner.succeed(`Created ${chalk.bold(projectFullName)} on Expo`);
|
||||
spinner.succeed(`Created ${chalk.bold(projectLink)} on Expo`);
|
||||
return id;
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
@@ -62,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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -24,7 +24,7 @@ export async function isEasEnabledForProjectAsync(projectId: string): Promise<bo
|
||||
}
|
||||
|
||||
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
|
||||
'https://expo.io/eas'
|
||||
'https://expo.dev/eas'
|
||||
)}`;
|
||||
|
||||
export function warnEasUnavailable() {
|
||||
|
||||
@@ -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(
|
||||
@@ -28,7 +28,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildConte
|
||||
);
|
||||
Log.warn(`We recommend you to abort, fix the ${chalk.bold('metro.config.js')}, and try again.`);
|
||||
Log.warn(
|
||||
learnMore('https://docs.expo.io/guides/customizing-metro/', {
|
||||
learnMore('https://docs.expo.dev/guides/customizing-metro/', {
|
||||
learnMoreMessage: 'Learn more on customizing Metro',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
|
||||
case 'Robot':
|
||||
if (!exp.owner) {
|
||||
throw new Error(
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.io/versions/latest/config/app/#owner'
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
|
||||
);
|
||||
}
|
||||
return exp.owner;
|
||||
@@ -39,7 +39,7 @@ export function getUsername(exp: ExpoConfig, user: Actor): string | undefined {
|
||||
// owner field is necessary to run `expo prebuild`
|
||||
if (!exp.owner) {
|
||||
throw new Error(
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.io/versions/latest/config/app/#owner'
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
|
||||
);
|
||||
}
|
||||
// robot users don't have usernames
|
||||
@@ -82,7 +82,7 @@ export async function setProjectIdAsync(
|
||||
Log.warn('It looks like you are using a dynamic configuration!');
|
||||
Log.log(
|
||||
chalk.dim(
|
||||
'https://docs.expo.io/workflow/configuration/#dynamic-configuration-with-appconfigjs)\n'
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)\n'
|
||||
)
|
||||
);
|
||||
Log.warn(
|
||||
|
||||
@@ -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';
|
||||
@@ -16,15 +16,6 @@ import Log from '../log';
|
||||
import { uploadWithPresignedPostAsync } from '../uploads';
|
||||
|
||||
export const TIMEOUT_LIMIT = 60_000; // 1 minute
|
||||
const STORAGE_BUCKET = getStorageBucket();
|
||||
|
||||
function getStorageBucket(): string {
|
||||
if (process.env.EXPO_STAGING || process.env.EXPO_LOCAL) {
|
||||
return 'update-assets-staging';
|
||||
} else {
|
||||
return 'update-assets-production';
|
||||
}
|
||||
}
|
||||
|
||||
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
|
||||
type Metadata = {
|
||||
@@ -35,7 +26,7 @@ type Metadata = {
|
||||
};
|
||||
};
|
||||
export type RawAsset = {
|
||||
type: string;
|
||||
fileExtension?: string;
|
||||
contentType: string;
|
||||
path: string;
|
||||
};
|
||||
@@ -117,20 +108,24 @@ export async function convertAssetToUpdateInfoGroupFormatAsync(
|
||||
asset: RawAsset
|
||||
): Promise<PartialManifestAsset> {
|
||||
const fileSHA256 = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
const contentType = asset.contentType;
|
||||
const { contentType, fileExtension } = asset;
|
||||
|
||||
const storageKey = getStorageKey(contentType, fileSHA256);
|
||||
const bundleKey = (await calculateFileHashAsync(asset.path, 'md5')).toString('hex');
|
||||
|
||||
return {
|
||||
fileSHA256,
|
||||
contentType,
|
||||
storageBucket: STORAGE_BUCKET,
|
||||
storageKey,
|
||||
bundleKey,
|
||||
fileExtension,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildUpdateInfoGroupAsync(
|
||||
/**
|
||||
* This will be sorted later based on the platform's runtime versions.
|
||||
*/
|
||||
export async function buildUnsortedUpdateInfoGroupAsync(
|
||||
assets: CollectedAssets,
|
||||
exp: ExpoConfig
|
||||
): Promise<UpdateInfoGroup> {
|
||||
@@ -230,13 +225,18 @@ export function collectAssets({
|
||||
for (const platform of platforms) {
|
||||
assetsFinal[platform] = {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
|
||||
},
|
||||
assets: metadata.fileMetadata[platform].assets.map(asset => {
|
||||
let fileExtension;
|
||||
if (asset.ext) {
|
||||
// ensure the file extension has a '.' prefix
|
||||
fileExtension = asset.ext.startsWith('.') ? asset.ext : `.${asset.ext}`;
|
||||
}
|
||||
return {
|
||||
type: asset.ext,
|
||||
fileExtension,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(distRoot, asset.path),
|
||||
};
|
||||
|
||||
@@ -1,88 +1,43 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
|
||||
import { AppPlatform, SubmissionFragment } from '../graphql/generated';
|
||||
import { SubmissionMutation } from '../graphql/mutations/SubmissionMutation';
|
||||
import Log from '../log';
|
||||
import { sleep } from '../utils/promise';
|
||||
import SubmissionService, {
|
||||
DEFAULT_CHECK_INTERVAL_MS,
|
||||
SubmissionConfig,
|
||||
} from './SubmissionService';
|
||||
import { displayLogs } from './utils/logs';
|
||||
|
||||
abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
protected abstract readonly appStoreName: string;
|
||||
import { AndroidSubmissionConfig } from './android/AndroidSubmissionConfig';
|
||||
import { IosSubmissionConfig } from './ios/IosSubmissionConfig';
|
||||
import { BaseSubmissionContext } from './types';
|
||||
|
||||
export default abstract class BaseSubmitter<
|
||||
SubmissionContext extends BaseSubmissionContext,
|
||||
SubmissionOptions
|
||||
> {
|
||||
protected constructor(
|
||||
private platform: AppPlatform,
|
||||
protected ctx: SubmissionContext,
|
||||
protected options: SubmissionOptions
|
||||
) {}
|
||||
|
||||
public abstract submitAsync(): Promise<void>;
|
||||
public abstract submitAsync(): Promise<SubmissionFragment>;
|
||||
|
||||
protected async startSubmissionAsync(
|
||||
submissionConfig: SubmissionConfig,
|
||||
buildId?: string,
|
||||
verbose: boolean = false
|
||||
): Promise<SubmissionStatus> {
|
||||
protected async createSubmissionAsync(
|
||||
submissionConfig: AndroidSubmissionConfig | IosSubmissionConfig,
|
||||
buildId?: string
|
||||
): 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 = await SubmissionMutation.createSubmissionAsync({
|
||||
appId: submissionConfig.projectId,
|
||||
platform: this.platform,
|
||||
config: submissionConfig as unknown as JSONObject,
|
||||
submittedBuildId: buildId,
|
||||
});
|
||||
scheduleSpinner.succeed();
|
||||
return submission;
|
||||
} catch (err) {
|
||||
scheduleSpinner.fail('Failed to schedule submission');
|
||||
throw err;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -8,6 +8,7 @@ export interface AndroidSubmissionConfig {
|
||||
track: ReleaseTrack;
|
||||
serviceAccount: string;
|
||||
releaseStatus?: ReleaseStatus;
|
||||
changesNotSentForReview: boolean;
|
||||
}
|
||||
|
||||
export enum ReleaseStatus {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Result, result } from '@expo/results';
|
||||
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppPlatform, 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';
|
||||
@@ -12,11 +13,15 @@ import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
|
||||
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
|
||||
|
||||
class AndroidSubmitCommand {
|
||||
static createContext(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
commandFlags: AndroidSubmitCommandFlags
|
||||
): AndroidSubmissionContext {
|
||||
static createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
}: {
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
commandFlags: AndroidSubmitCommandFlags;
|
||||
}): AndroidSubmissionContext {
|
||||
return {
|
||||
projectDir,
|
||||
projectId,
|
||||
@@ -26,15 +31,15 @@ class AndroidSubmitCommand {
|
||||
|
||||
constructor(private ctx: AndroidSubmissionContext) {}
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
const submissionOptions = this.getAndroidSubmissionOptions();
|
||||
const submissionOptions = await this.getAndroidSubmissionOptionsAsync();
|
||||
const submitter = new AndroidSubmitter(this.ctx, submissionOptions);
|
||||
await submitter.submitAsync();
|
||||
return await submitter.submitAsync();
|
||||
}
|
||||
|
||||
private getAndroidSubmissionOptions(): AndroidSubmissionOptions {
|
||||
const androidPackageSource = this.resolveAndroidPackageSource();
|
||||
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);
|
||||
@@ -60,16 +65,25 @@ class AndroidSubmitCommand {
|
||||
releaseStatus: releaseStatus.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
serviceAccountSource: serviceAccountSource.enforceValue(),
|
||||
changesNotSentForReview: this.ctx.commandFlags.changesNotSentForReview,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveAndroidPackageSource(): Result<AndroidPackageSource> {
|
||||
let androidPackage: string | undefined;
|
||||
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<string | undefined> {
|
||||
const { exp } = getConfig(this.ctx.projectDir, { skipSDKVersionRequirement: true });
|
||||
try {
|
||||
return await getApplicationIdAsync(this.ctx.projectDir, exp);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveAndroidPackageSourceAsync(): Promise<Result<AndroidPackageSource>> {
|
||||
let androidPackage: string | undefined;
|
||||
if (this.ctx.commandFlags.androidPackage) {
|
||||
androidPackage = this.ctx.commandFlags.androidPackage;
|
||||
} else if (exp.android?.package) {
|
||||
androidPackage = exp.android.package;
|
||||
} else {
|
||||
androidPackage = await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
}
|
||||
if (androidPackage) {
|
||||
return result({
|
||||
@@ -147,11 +161,11 @@ class AndroidSubmitCommand {
|
||||
}
|
||||
|
||||
private resolveServiceAccountSource(): Result<ServiceAccountSource> {
|
||||
const { key } = this.ctx.commandFlags;
|
||||
if (key) {
|
||||
const { serviceAccountKeyPath } = this.ctx.commandFlags;
|
||||
if (serviceAccountKeyPath) {
|
||||
return result({
|
||||
sourceType: ServiceAccountSourceType.path,
|
||||
path: key,
|
||||
path: serviceAccountKeyPath,
|
||||
});
|
||||
} else {
|
||||
return result({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { AndroidArchiveType, AndroidSubmissionContext, ArchiveType } from '../types';
|
||||
@@ -14,7 +14,10 @@ import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from './AndroidS
|
||||
import { ServiceAccountSource, getServiceAccountAsync } from './ServiceAccountSource';
|
||||
|
||||
export interface AndroidSubmissionOptions
|
||||
extends Pick<AndroidSubmissionConfig, 'track' | 'releaseStatus' | 'projectId'> {
|
||||
extends Pick<
|
||||
AndroidSubmissionConfig,
|
||||
'track' | 'releaseStatus' | 'projectId' | 'changesNotSentForReview'
|
||||
> {
|
||||
androidPackageSource: AndroidPackageSource;
|
||||
archiveSource: ArchiveSource;
|
||||
serviceAccountSource: ServiceAccountSource;
|
||||
@@ -27,13 +30,11 @@ interface ResolvedSourceOptions {
|
||||
}
|
||||
|
||||
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> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptions();
|
||||
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
|
||||
@@ -43,10 +44,9 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
|
||||
await this.startSubmissionAsync(
|
||||
return await this.createSubmissionAsync(
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id,
|
||||
this.ctx.commandFlags.verbose
|
||||
resolvedSourceOptions.archive.build?.id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
|
||||
): Promise<AndroidSubmissionConfig> {
|
||||
const serviceAccount = await fs.readFile(serviceAccountPath, 'utf-8');
|
||||
const { track, releaseStatus, projectId } = options;
|
||||
const { track, releaseStatus, projectId, changesNotSentForReview } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
androidPackage,
|
||||
archiveUrl: archive.location,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
track,
|
||||
changesNotSentForReview,
|
||||
releaseStatus,
|
||||
projectId,
|
||||
serviceAccount,
|
||||
@@ -84,12 +84,14 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
|
||||
): Summary {
|
||||
const { projectId, track, releaseStatus } = options;
|
||||
const { projectId, track, releaseStatus, changesNotSentForReview } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
projectId,
|
||||
androidPackage,
|
||||
track,
|
||||
changesNotSentForReview,
|
||||
releaseStatus,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
serviceAccountPath,
|
||||
@@ -105,6 +107,7 @@ type Summary = {
|
||||
track: ReleaseTrack;
|
||||
releaseStatus?: ReleaseStatus;
|
||||
projectId?: string;
|
||||
changesNotSentForReview?: boolean;
|
||||
} & ArchiveSourceSummaryFields;
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
@@ -113,6 +116,7 @@ const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
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',
|
||||
|
||||
@@ -3,15 +3,10 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
SubmissionFragment,
|
||||
SubmissionStatus,
|
||||
} from '../../../graphql/generated';
|
||||
import { AppPlatform, BuildFragment } 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';
|
||||
@@ -19,7 +14,11 @@ import AndroidSubmitCommand from '../AndroidSubmitCommand';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('ora');
|
||||
jest.mock('../../SubmissionService');
|
||||
jest.mock('../../../graphql/mutations/SubmissionMutation', () => ({
|
||||
SubmissionMutation: {
|
||||
createSubmissionAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
jest.mock('../../utils/builds');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
@@ -44,7 +43,7 @@ describe(AndroidSubmitCommand, () => {
|
||||
|
||||
const fakeBuildFragment: Partial<BuildFragment> = {
|
||||
id: uuidv4(),
|
||||
artifacts: { buildUrl: 'http://expo.io/fake.apk' },
|
||||
artifacts: { buildUrl: 'http://expo.dev/fake.apk' },
|
||||
appVersion: '1.2.3',
|
||||
platform: AppPlatform.Android,
|
||||
};
|
||||
@@ -71,107 +70,81 @@ 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 = {
|
||||
const commandFlags: AndroidSubmitCommandFlags = {
|
||||
latest: false,
|
||||
url: 'http://expo.io/fake.apk',
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
type: 'apk',
|
||||
key: '/google-service-account.json',
|
||||
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,
|
||||
commandFlags,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const androidSubmissionConfig: AndroidSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.io/fake.apk',
|
||||
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.createSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: androidSubmissionConfig,
|
||||
platform: AppPlatform.Android,
|
||||
});
|
||||
});
|
||||
|
||||
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 = {
|
||||
const commandFlags: AndroidSubmitCommandFlags = {
|
||||
latest: true,
|
||||
type: 'apk',
|
||||
key: '/google-service-account.json',
|
||||
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,
|
||||
commandFlags,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const androidSubmissionConfig: AndroidSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.io/fake.apk',
|
||||
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.createSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: androidSubmissionConfig,
|
||||
platform: AppPlatform.Android,
|
||||
submittedBuildId: fakeBuildFragment.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,6 @@ interface CreateAppOptions {
|
||||
appName: string;
|
||||
bundleIdentifier: string;
|
||||
appleTeamId?: string;
|
||||
itcTeamId?: string;
|
||||
language?: string;
|
||||
companyName?: string;
|
||||
sku?: string;
|
||||
@@ -70,6 +69,7 @@ async function createAppStoreConnectAppAsync(options: CreateAppOptions): Promise
|
||||
appName,
|
||||
language,
|
||||
companyName,
|
||||
sku,
|
||||
} = options;
|
||||
|
||||
const authCtx = await authenticateAsync({
|
||||
@@ -99,8 +99,9 @@ async function createAppStoreConnectAppAsync(options: CreateAppOptions): Promise
|
||||
language,
|
||||
companyName,
|
||||
bundleIdentifier: bundleId,
|
||||
sku,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (
|
||||
// Name is invalid
|
||||
error.message.match(
|
||||
|
||||
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import getenv from 'getenv';
|
||||
import wrapAnsi from 'wrap-ansi';
|
||||
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import UserSettings from '../../user/UserSettings';
|
||||
@@ -18,11 +18,15 @@ import {
|
||||
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
||||
|
||||
class IosSubmitCommand {
|
||||
static createContext(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
commandFlags: IosSubmitCommandFlags
|
||||
): IosSubmissionContext {
|
||||
static createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
commandFlags,
|
||||
}: {
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
commandFlags: IosSubmitCommandFlags;
|
||||
}): IosSubmissionContext {
|
||||
return {
|
||||
projectDir,
|
||||
projectId,
|
||||
@@ -32,11 +36,11 @@ class IosSubmitCommand {
|
||||
|
||||
constructor(private ctx: IosSubmissionContext) {}
|
||||
|
||||
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> {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AppPlatform, SubmissionStatus } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { AppPlatform, SubmissionFragment } from '../../graphql/generated';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { IosSubmissionContext } from '../types';
|
||||
@@ -31,13 +28,11 @@ type SummaryData = Pick<IosSubmissionOptions, 'ascAppId' | 'appleId' | 'projectI
|
||||
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> {
|
||||
async submitAsync(): Promise<SubmissionFragment> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptions();
|
||||
const submissionConfig = await this.formatSubmissionConfigAsync(
|
||||
this.options,
|
||||
@@ -50,25 +45,10 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
|
||||
const result = await this.startSubmissionAsync(
|
||||
return await this.createSubmissionAsync(
|
||||
submissionConfig,
|
||||
resolvedSourceOptions.archive.build?.id,
|
||||
this.ctx.commandFlags.verbose
|
||||
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: '' }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveSourceOptions(): Promise<ResolvedSourceOptions> {
|
||||
|
||||
@@ -3,17 +3,21 @@ 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 { AppPlatform } 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: {
|
||||
createSubmissionAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../project/ensureProjectExists');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
getUserAsync: jest.fn(() => mockJester),
|
||||
@@ -43,7 +47,6 @@ describe(IosSubmitCommand, () => {
|
||||
});
|
||||
afterAll(() => {
|
||||
vol.reset();
|
||||
|
||||
jest.unmock('@expo/config');
|
||||
});
|
||||
|
||||
@@ -52,61 +55,38 @@ 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 = {
|
||||
const commandFlags: IosSubmitCommandFlags = {
|
||||
latest: false,
|
||||
url: 'http://expo.io/fake.ipa',
|
||||
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,
|
||||
commandFlags,
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
const iosSubmissionConfig: IosSubmissionConfig = {
|
||||
archiveUrl: 'http://expo.io/fake.ipa',
|
||||
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.createSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: iosSubmissionConfig,
|
||||
platform: AppPlatform.Ios,
|
||||
});
|
||||
|
||||
delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export interface SubmissionContext<T extends SubmitCommandFlags> {
|
||||
export interface BaseSubmissionContext {
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export interface SubmissionContext<T extends SubmitCommandFlags> extends BaseSubmissionContext {
|
||||
commandFlags: T;
|
||||
}
|
||||
|
||||
export interface SubmitCommandFlags {
|
||||
latest: boolean;
|
||||
latest?: boolean;
|
||||
id?: string;
|
||||
path?: string;
|
||||
url?: string;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export enum AndroidArchiveType {
|
||||
@@ -26,10 +28,11 @@ export type ArchiveType = AndroidArchiveType | IosArchiveType;
|
||||
// Android specific types
|
||||
export interface AndroidSubmitCommandFlags extends SubmitCommandFlags {
|
||||
type?: 'apk' | 'aab';
|
||||
key?: string;
|
||||
serviceAccountKeyPath?: string;
|
||||
androidPackage?: string;
|
||||
track: string;
|
||||
releaseStatus: string;
|
||||
changesNotSentForReview: boolean;
|
||||
}
|
||||
|
||||
export type AndroidSubmissionContext = SubmissionContext<AndroidSubmitCommandFlags>;
|
||||
@@ -42,7 +45,6 @@ export interface IosSubmitCommandFlags extends SubmitCommandFlags {
|
||||
|
||||
// used only when running produce
|
||||
appleTeamId?: string;
|
||||
itcTeamId?: string;
|
||||
appName?: string;
|
||||
bundleIdentifier?: string;
|
||||
sku?: string;
|
||||
|
||||
@@ -51,11 +51,11 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
[SubmissionErrorCode.IOS_MISSING_APP_ICON]:
|
||||
'Your iOS app icon is missing or is an invalid format. The icon must be a 1024x1024 PNG image with no transparency.\n' +
|
||||
'Please check your icon image and icon configuration in app.json.\n' +
|
||||
`${learnMore('https://docs.expo.io/guides/app-icons/')}`,
|
||||
`${learnMore('https://docs.expo.dev/guides/app-icons/')}`,
|
||||
[SubmissionErrorCode.IOS_INVALID_SIGNATURE]:
|
||||
'Your app signature seems to be invalid.\n' +
|
||||
"Please check your iOS Distribution Certificate and your app's Provisioning Profile.\n" +
|
||||
`${learnMore('https://docs.expo.io/distribution/app-signing')}`,
|
||||
`${learnMore('https://docs.expo.dev/distribution/app-signing')}`,
|
||||
[SubmissionErrorCode.IOS_INCORRECT_CREDENTIALS]:
|
||||
'Your Apple ID or app-specific password is incorrect. Please verify that you entered them correctly and try again.',
|
||||
[SubmissionErrorCode.IOS_IPAD_INVALID_ORIENTATION]:
|
||||
|
||||
@@ -5,21 +5,20 @@ import { SubmissionFragment, SubmissionStatus } from '../../graphql/generated';
|
||||
import { default as LogModule } from '../../log';
|
||||
import { printSubmissionError } from './errors';
|
||||
|
||||
export async function displayLogs(
|
||||
submission: SubmissionFragment | null,
|
||||
status: SubmissionStatus | null,
|
||||
verbose: boolean
|
||||
export async function displayLogsAsync(
|
||||
submission: SubmissionFragment,
|
||||
{ verbose = false }: { verbose?: boolean } = {}
|
||||
): Promise<void> {
|
||||
let printedUnknownError = false;
|
||||
if (status === SubmissionStatus.Errored && submission?.error) {
|
||||
if (submission.status === SubmissionStatus.Errored && submission.error) {
|
||||
printedUnknownError = printSubmissionError(submission.error);
|
||||
}
|
||||
if ((printedUnknownError || verbose) && submission) {
|
||||
await downloadAndPrintSubmissionLogs(submission);
|
||||
if (printedUnknownError || verbose) {
|
||||
await downloadAndPrintSubmissionLogsAsync(submission);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAndPrintSubmissionLogs(submission: SubmissionFragment): Promise<void> {
|
||||
async function downloadAndPrintSubmissionLogsAsync(submission: SubmissionFragment): Promise<void> {
|
||||
if (!submission.logsUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import chalk from 'chalk';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { SubmissionFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
|
||||
export function printSubmissionDetailsUrls(submissions: SubmissionFragment[]): void {
|
||||
if (submissions.length === 1) {
|
||||
const [submission] = submissions;
|
||||
Log.log(`Submission details: ${chalk.underline(getSubmissionDetailsUrl(submission))}`);
|
||||
} else {
|
||||
submissions.forEach(submission => {
|
||||
Log.log(
|
||||
`${appPlatformDisplayNames[submission.platform]} submission details: ${chalk.underline(
|
||||
getSubmissionDetailsUrl(submission)
|
||||
)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubmissionDetailsUrl(submission: SubmissionFragment): string {
|
||||
const { id, app } = submission;
|
||||
return new URL(
|
||||
`/accounts/${app.ownerAccount.name}/projects/${app.name}/submissions/${id}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import ora from 'ora';
|
||||
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../../graphql/generated';
|
||||
import { SubmissionQuery } from '../../graphql/queries/SubmissionQuery';
|
||||
import Log from '../../log';
|
||||
import { sleep } from '../../utils/promise';
|
||||
|
||||
const APP_STORE_NAMES: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Android]: 'Google Play Store',
|
||||
[AppPlatform.Ios]: 'Apple App Store',
|
||||
};
|
||||
|
||||
const CHECK_TIMEOUT_MS = 3600_000;
|
||||
const CHECK_INTERVAL_MS = 5_000;
|
||||
|
||||
export async function waitForSubmissionsEndAsync(
|
||||
initialSubmissions: SubmissionFragment[]
|
||||
): Promise<SubmissionFragment[]> {
|
||||
Log.log(
|
||||
`Waiting for submission${
|
||||
initialSubmissions.length > 1 ? 's' : ''
|
||||
} to complete. You can press Ctrl+C to exit.`
|
||||
);
|
||||
|
||||
const spinner = ora(`Submitting`).start();
|
||||
|
||||
let time = new Date().getTime();
|
||||
const timeoutTime = time + CHECK_TIMEOUT_MS;
|
||||
while (time <= timeoutTime) {
|
||||
const submissions = await Promise.all(
|
||||
initialSubmissions.map(({ id }) => SubmissionQuery.byIdAsync(id, { useCache: false }))
|
||||
);
|
||||
|
||||
if (submissions.length === 1) {
|
||||
const [submission] = submissions;
|
||||
spinner.text = getSingleSpinnerText(submission);
|
||||
if (submission.status === SubmissionStatus.Finished) {
|
||||
spinner.succeed();
|
||||
return submissions;
|
||||
} else if (submission.status === SubmissionStatus.Errored) {
|
||||
spinner.fail();
|
||||
return submissions;
|
||||
} else if (submission.status === SubmissionStatus.Canceled) {
|
||||
spinner.warn();
|
||||
return submissions;
|
||||
}
|
||||
} else {
|
||||
spinner.text = getMultipleSpinnerText(submissions);
|
||||
|
||||
const finished = countWithStatus(submissions, SubmissionStatus.Finished);
|
||||
const errored = countWithStatus(submissions, SubmissionStatus.Errored);
|
||||
const canceled = countWithStatus(submissions, SubmissionStatus.Canceled);
|
||||
const nonCompleted = submissions.length - (finished + errored + canceled);
|
||||
if (nonCompleted === 0) {
|
||||
if (finished === submissions.length) {
|
||||
spinner.succeed('All submissions have finished');
|
||||
} else {
|
||||
spinner.fail('Some submissions were canceled or failed');
|
||||
}
|
||||
return submissions;
|
||||
}
|
||||
}
|
||||
|
||||
time = new Date().getTime();
|
||||
await sleep(CHECK_INTERVAL_MS);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error('Timeout reached! It is taking longer than expected to complete, aborting...');
|
||||
}
|
||||
|
||||
function getSingleSpinnerText(submission: SubmissionFragment): string {
|
||||
const { platform, status } = submission;
|
||||
const appStoreName = APP_STORE_NAMES[platform];
|
||||
switch (status) {
|
||||
case SubmissionStatus.AwaitingBuild:
|
||||
return `Submitting your app to ${appStoreName}: waiting for the associated build to complete`;
|
||||
case SubmissionStatus.InQueue:
|
||||
return `Submitting your app to ${appStoreName}: waiting for an available submitter`;
|
||||
case SubmissionStatus.InProgress:
|
||||
return `Submitting your app to ${appStoreName}: submission in progress`;
|
||||
case SubmissionStatus.Finished:
|
||||
return `Submitted your app to ${appStoreName}!`;
|
||||
case SubmissionStatus.Errored:
|
||||
return `Something went wrong when submitting your app to ${appStoreName}.`;
|
||||
case SubmissionStatus.Canceled:
|
||||
return 'The submission has been canceled';
|
||||
}
|
||||
}
|
||||
|
||||
function getMultipleSpinnerText(submissions: SubmissionFragment[]): string {
|
||||
const awaitingSubmissions = countWithStatus(submissions, SubmissionStatus.AwaitingBuild);
|
||||
const inQueue = countWithStatus(submissions, SubmissionStatus.InQueue);
|
||||
const inProgress = countWithStatus(submissions, SubmissionStatus.InProgress);
|
||||
const finished = countWithStatus(submissions, SubmissionStatus.Finished);
|
||||
const errored = countWithStatus(submissions, SubmissionStatus.Errored);
|
||||
const canceled = countWithStatus(submissions, SubmissionStatus.Canceled);
|
||||
const text = [
|
||||
awaitingSubmissions && `Awaiting submissions: ${awaitingSubmissions}`,
|
||||
inQueue && `Submissions in queue: ${inQueue}`,
|
||||
inProgress && `Submissions in progress: ${inProgress}`,
|
||||
canceled && `Canceled submissions: ${canceled}`,
|
||||
errored && `Failed submissions: ${errored}`,
|
||||
finished && `Finished submissions: ${finished}`,
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
return text;
|
||||
}
|
||||
|
||||
function countWithStatus(submissions: SubmissionFragment[], status: SubmissionStatus): number {
|
||||
return submissions.filter(submission => submission.status === status).length;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export async function uploadWithPresignedPostAsync(
|
||||
const response = await uploadPromise;
|
||||
handleProgressEvent({ isComplete: true });
|
||||
return String(response.headers.location);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
handleProgressEvent({ isComplete: true, error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const getUserAsync = jest.fn();
|
||||
@@ -1,3 +1,5 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import ApiV2Error from '../ApiV2Error';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
@@ -40,19 +42,16 @@ export async function ensureLoggedInAsync(): Promise<Actor> {
|
||||
try {
|
||||
user = await getUserAsync();
|
||||
} catch (_) {}
|
||||
|
||||
if (!user) {
|
||||
Log.warn('An Expo user account is required to proceed.');
|
||||
Log.newLine();
|
||||
Log.log('Log in to EAS');
|
||||
await showLoginPromptAsync(); // TODO: login or register
|
||||
user = await getUserAsync();
|
||||
if (!user) {
|
||||
// just to satisfy ts
|
||||
throw new Error('Failed to access user data');
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
return nullthrows(user);
|
||||
}
|
||||
|
||||
export function ensureActorHasUsername(user: Actor): string {
|
||||
|
||||
@@ -18,7 +18,7 @@ type SessionData = {
|
||||
export function getSession(): SessionData | null {
|
||||
try {
|
||||
return JsonFile.read<UserSettingsData>(getStateJsonPath())?.auth ?? null;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
let stdoutWrite: NodeJS.WriteStream['write'] | undefined;
|
||||
|
||||
export function enableJsonOutput() {
|
||||
if (stdoutWrite) {
|
||||
return;
|
||||
}
|
||||
stdoutWrite = process.stdout.write;
|
||||
process.stdout.write = (...args: any) => {
|
||||
return process.stderr.write.call(null, args);
|
||||
};
|
||||
}
|
||||
|
||||
export function printJsonOnlyOutput(value: object): void {
|
||||
assert(stdoutWrite, 'this should only be called with --json flag');
|
||||
try {
|
||||
process.stdout.write = stdoutWrite;
|
||||
Log.log(JSON.stringify(sanitizeValue(value), null, 2));
|
||||
} finally {
|
||||
process.stdout.write = (...args: any) => {
|
||||
return process.stderr.write.call(null, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeValue(value: any): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(val => sanitizeValue(val));
|
||||
} else if (value && typeof value === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key !== '__typename' && value[key] !== null) {
|
||||
result[key] = sanitizeValue(value[key]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ function createProgressTracker({
|
||||
}: {
|
||||
total?: number;
|
||||
message: string | ((ratio: number) => string);
|
||||
completedMessage: string;
|
||||
completedMessage: string | ((duration: string) => string);
|
||||
}): ProgressHandler {
|
||||
let bar: ora.Ora | null = null;
|
||||
let calcTotal: number = total ?? 0;
|
||||
@@ -66,7 +66,11 @@ function createProgressTracker({
|
||||
if (error) {
|
||||
bar.fail();
|
||||
} else if (isComplete) {
|
||||
bar.succeed(`${completedMessage} ${chalk.dim(prettyTime)}`);
|
||||
if (typeof completedMessage === 'string') {
|
||||
bar.succeed(`${completedMessage} ${chalk.dim(prettyTime)}`);
|
||||
} else {
|
||||
bar.succeed(completedMessage(prettyTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { Client } from './vcs';
|
||||
|
||||
@@ -64,6 +64,11 @@ export default class GitClient extends Client {
|
||||
}
|
||||
|
||||
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
|
||||
if (await this.hasUncommittedChangesAsync()) {
|
||||
// it should not happen, we need that to make sure that check before clone
|
||||
// is always caused by case sensitivity
|
||||
throw new Error('You have some uncommitted changes in your repository.');
|
||||
}
|
||||
let gitRepoUri;
|
||||
if (process.platform === 'win32') {
|
||||
// getRootDirectoryAsync() will return C:/path/to/repo on Windows and path
|
||||
@@ -74,15 +79,32 @@ export default class GitClient extends Client {
|
||||
// file:/// so only file:// needs to be prepended
|
||||
gitRepoUri = `file://${await this.getRootPathAsync()}`;
|
||||
}
|
||||
|
||||
await spawnAsync('git', [
|
||||
'clone',
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
gitRepoUri,
|
||||
destinationPath,
|
||||
]);
|
||||
const isCaseSensitive = await isGitCaseSensitiveAsync();
|
||||
await setGitCaseSensitivityAsync(true);
|
||||
try {
|
||||
if (await this.hasUncommittedChangesAsync()) {
|
||||
Log.error('Detected inconsistent filename casing between your local filesystem and git.');
|
||||
Log.error('This will likely cause your build to fail. Impacted files:');
|
||||
await spawnAsync('git', ['status', '--short'], { stdio: 'inherit' });
|
||||
Log.newLine();
|
||||
Log.error(
|
||||
`Error: Resolve filename casing inconsistencies before proceeding. ${learnMore(
|
||||
'https://expo.fyi/macos-ignorecase'
|
||||
)}`
|
||||
);
|
||||
throw new Error('You have some uncommitted changes in your repository.');
|
||||
}
|
||||
await spawnAsync('git', [
|
||||
'clone',
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
gitRepoUri,
|
||||
destinationPath,
|
||||
]);
|
||||
} finally {
|
||||
await setGitCaseSensitivityAsync(isCaseSensitive);
|
||||
}
|
||||
}
|
||||
|
||||
public async getCommitHashAsync(): Promise<string | undefined> {
|
||||
@@ -141,7 +163,7 @@ export default class GitClient extends Client {
|
||||
async function isGitInstalledAsync(): Promise<boolean> {
|
||||
try {
|
||||
await spawnAsync('git', ['--help']);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
@@ -174,3 +196,37 @@ async function gitDiffAsync({ withPager = false }: { withPager?: boolean } = {})
|
||||
const options = withPager ? [] : ['--no-pager'];
|
||||
await spawnAsync('git', [...options, 'diff'], { stdio: ['ignore', 'inherit', 'inherit'] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if git is configured to be case sensitive
|
||||
* @returns {boolean | undefined}
|
||||
* - boolean - is git case senstive
|
||||
* - undefined - case sensitivity is not configured and git is using default behavior
|
||||
*/
|
||||
async function isGitCaseSensitiveAsync(): Promise<boolean | undefined> {
|
||||
if (process.platform !== 'darwin') {
|
||||
return undefined;
|
||||
}
|
||||
const result = await spawnAsync('git', ['config', '--get', 'core.ignorecase']);
|
||||
const isIgnoreCaseEnabled = result.stdout.trim();
|
||||
if (isIgnoreCaseEnabled === '') {
|
||||
return undefined;
|
||||
} else if (isIgnoreCaseEnabled === 'true') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function setGitCaseSensitivityAsync(enable: boolean | undefined): Promise<void> {
|
||||
// we are assuming that if someone sets that on non-macos device then
|
||||
// they know what they are doing
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
if (enable === undefined) {
|
||||
await spawnAsync('git', ['config', '--unset', 'core.ignorecase']);
|
||||
} else {
|
||||
await spawnAsync('git', ['config', 'core.ignorecase', String(!enable)]);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ const NO_VCS_WARNING = `Using EAS CLI without version control system is not reco
|
||||
|
||||
function resolveVcsClient(): Client {
|
||||
if (process.env.EAS_NO_VCS) {
|
||||
log.warn(NO_VCS_WARNING);
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
log.warn(NO_VCS_WARNING);
|
||||
}
|
||||
return new LocalClient();
|
||||
}
|
||||
return new GitClient();
|
||||
|
||||
@@ -62,6 +62,9 @@ export abstract class Client {
|
||||
// (optional) checks if the file is ignored, an implementation should ensure
|
||||
// that if file exists and `isFileIgnoredAsync` returns true, then that file
|
||||
// should not be included in the project tarball.
|
||||
//
|
||||
// @param filePath has to be a relative normalized path pointing to a file
|
||||
// located under the root of the repository
|
||||
public async isFileIgnoredAsync(filePath: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user