Compare commits

...
39 Commits
Author SHA1 Message Date
Dominik Sokal 5bc14b5b30 v0.25.0 2021-09-01 10:00:39 +02:00
L bee98ce25e [eas-cli] Update metro config check to support Windows (#588)
* Update metro config check to support Windows

Since Windows use backtick, the added regex address this issue.

* Update CHANGELOG.md

* Update CHANGELOG.md

* Fix eslint error
2021-09-01 09:54:29 +02:00
Dominik Sokal 0e6a5315ab clean up repo deps + use volta (#589)
* move eslint-plugin-graphql from root to eas-cli

* install @babel/core to suppress yarn install warning

* use volta

* fix lint warning in .eslintrc.js

* address PR feedback

* bump @types/node to 16.7.8
2021-08-31 17:11:52 +02:00
Wojciech KozyraandDominik Sokal 91b51528ef [eas-cli] detect case inconsistencies on macos (#570)
* [eas-cli] detect case inconsistencies on macos

* update CHANGELOG

* Apply suggestions from code review

Co-authored-by: Dominik Sokal <[email protected]>

* add explanatiion to undefined value

* add better error message + fyi page

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-31 16:56:36 +02:00
Wojciech KozyraandDominik Sokal 5e3962afc2 [eas-cli] fix detecting googleServicesFile with EAS_NO_VCS=1 (#583)
* [eas-cli] fix detecting  services file with

* update CHANGELOG

* Update packages/eas-cli/src/vcs/vcs.ts

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-31 16:30:29 +02:00
Dominik Sokal f07c52d2fb [ENG-755] add --no-wait support for eas submit (#578)
* support --no-wait in eas submit

* update CHANGELOG
2021-08-31 14:49:15 +02:00
Dominik Sokal 50846a1a96 Use ts-jest instead of babel-jest (#585)
* run tests with ts-jest

* remove babel from dependencies

* update CHANGELOG
2021-08-31 12:18:20 +02:00
Dominik Sokal 4cb726f374 bump typescript target to ES2019 (#584)
* bump typescript target to ES2019

* update CHANGELOG
2021-08-30 15:32:48 +02:00
Dominik Sokal da9d1b4984 replace @hapi/joi with joi + upgrade dependencies (#582)
* replace @hapi/joi with joi

* upgrade fs-extra to 10.0.0

* upgrade typescript to 4.4.2

* upgrade other eas-cli dependencies

* update repo deps

* fix typos

* add ': any' to every catch statement

* downgrade some deps as they require ts project to output ESM

* fix fs mock

* update snapshots

* downgrade indent-string (esm issue)

* update CHANGELOG
2021-08-30 14:07:25 +02:00
Dominik Sokal 64ad52dc45 drop support for node < 12 (#581)
* drop support for node < 12

* run CI workflows on node 16

* update CHANGELOG
2021-08-30 09:57:59 +02:00
Bartłomiej Klocek 6169e5ff5b Add ESLint rule to prevent relative package imports (#576) 2021-08-25 14:43:12 +02:00
Dominik Sokal ef33973f7f update CHANGELOG 2021-08-25 11:57:34 +02:00
Dominik Sokal 33a5254f2a v0.24.1 2021-08-25 11:48:08 +02:00
Dominik Sokal 0d737ecf6d update CHANGELOG 2021-08-25 11:39:46 +02:00
Dominik Sokal 951eea2396 fix EasJsonReader import 2021-08-25 11:37:04 +02:00
Dominik Sokal 75968bf449 update CHANGELOG 2021-08-25 11:25:43 +02:00
Dominik Sokal 32aa46ac69 v0.24.0 2021-08-25 11:15:02 +02:00
339cae2532 Bump apple-utils to v25 (#574)
* Bump apple-utils to v25

* Update CHANGELOG.md

* Apply suggestions from code review

Co-authored-by: Evan Bacon <[email protected]>
Co-authored-by: Dominik Sokal <[email protected]>
2021-08-25 09:48:03 +02:00
Axel DelafosseandDominik Sokal e6ec3bfe6a [eas-cli] update QR code url for Android internal distribution build and generate smaller QR code (#573)
* [eas-cli] update qr code url and generate smaller qr code

* [eas-cli] update changelog

* [eas-cli] fix prettier

* [eas-cli] update changelog

Co-authored-by: Dominik Sokal <[email protected]>

* [eas-cli] update changelog

Co-authored-by: Dominik Sokal <[email protected]>

* [eas-cli] clean code

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-24 17:45:32 +02:00
Juwan Wheatley 012518c375 Update expo.io references to expo.dev (#568)
* Update expo.io references to expo.dev

* fix snapshot
2021-08-23 19:48:37 -04:00
Brent Vatne cc8c8584d9 [eas-cli] Link to project page and add archive FYI link to build command output (#562) 2021-08-20 13:58:53 -07:00
jkh d1910b3c42 [eas-cli] add ability to double publish (#566) 2021-08-20 11:16:43 -07:00
Wojciech KozyraandDominik Sokal 4a1fa3d4fa [eas-cli] warn if NODE_ENV or googleServicesFile is missing (#569)
* [eas-cli] warn if NODE_ENV or googleServicesFile is missing

* update CHANGELOG

* Apply suggestions from code review

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-20 12:31:46 +02:00
Wojciech Kozyra 61178c9d06 add changesNotSentForReview option to android submissions (#560)
* add changesNotSentForReview option to android submits

* update CHANGELOG

* fix types in tests

* review feedback
2021-08-19 15:01:14 +02:00
Wojciech KozyraandDominik Sokal 094c04c7f0 [eas-cli] support --json flag for build commands (#567)
* [eas-cli] support --json flag for build commands

* sanitize json object

* update CHANGELOG

* Apply suggestions from code review

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-08-19 14:58:19 +02:00
kgc00 37cca4878d [eas-cli] Introduce RudderStack (#563)
- adds RudderStack to the analytics file
- reworks some of the existing amplitude tracking event structure to properly record data
2021-08-18 10:46:19 -07:00
Wojciech Kozyra 2b40e1ec05 [eas-cli] support submit profiles (#555)
* [eas-cli] support submit profiles

* Update CHANGELOG

* review feedback
2021-08-13 11:53:58 +02:00
Bartłomiej Klocek 6e95d3afcc [eas cli] Fix SKU not being applied to App Store Connect (#559)
* [eas submit] Fix SKU not being applied to App Store Connect

* Update CHANGELOG
2021-08-13 10:36:09 +02:00
Wojciech Kozyra 6b9b9f0be2 [eas-cli] fix metadata for simulator builds (#556)
* [eas-cli] fix metadata for simulator builds

* Update CHANGELOG
2021-08-13 10:08:27 +02:00
Brent Vatne f14b963b29 [eas-cli] Stop using project name in build details URL (#554) 2021-08-12 21:53:47 -07:00
andy ddd2814e8f [submit] log submissions link at end of eas-submit (#553) 2021-08-11 11:00:22 -07:00
jkh 36f15a81a9 [changelog] fix formatting (#552) 2021-08-06 07:47:31 -07:00
jkh ece93dac20 [eas-cli] add --auto flag to publish (#549) 2021-08-06 05:58:04 -07:00
Brent Vatne 0226dbba5a Update CHANGELOG and README 2021-08-05 15:34:16 -07:00
Brent Vatne e773e77185 v0.23.0 2021-08-05 15:19:01 -07:00
Brent Vatne 893cbb2ec5 [eas-cli] Pass through build settings metadata context to app version resolver (#550) 2021-08-05 15:17:30 -07:00
jkh 1898de2609 [eas-cli] add type to generated manifest fragment (#535) 2021-08-05 06:50:20 -07:00
jkh fe03cc28c4 [eas-cli] stop sending deprecated storageBucket field (#547) 2021-08-03 13:55:00 -07:00
Wojciech Kozyra 5309b7b6ce Update CHANGELOG 2021-08-02 13:23:20 +02:00
117 changed files with 7255 additions and 5398 deletions

No files matched your search

+3 -2
View File
@@ -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: {
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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'
+2 -2
View File
@@ -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
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['@expo/babel-preset-cli'],
};
};
+1 -3
View File
@@ -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
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.22.4"
"version": "0.25.0"
}
+15 -8
View File
@@ -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
View File
@@ -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 -->
+3
View File
@@ -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;
+2455 -702
View File
File diff suppressed because it is too large. Load diff
+40 -49
View File
@@ -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"
}
}
+2 -2
View File
@@ -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);
+84 -20
View File
@@ -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 },
};
}
+2 -2
View File
@@ -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);
}
+7 -4
View File
@@ -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,
+1 -1
View File
@@ -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 }
);
-11
View File
@@ -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]: '🤖',
};
+4
View File
@@ -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,
+1 -1
View File
@@ -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(
+6 -2
View File
@@ -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]
+5 -3
View File
@@ -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}`;
}
+53
View File
@@ -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);
+198 -52
View File
@@ -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}`;
}
+8 -25
View File
@@ -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',
})}`);
}
+17 -3
View File
@@ -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 });
}
+16 -4
View File
@@ -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}`);
}
+20 -3
View File
@@ -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}`);
+189 -72
View File
@@ -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);
@@ -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"
`;
@@ -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,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
View File
@@ -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
+1 -1
View File
@@ -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(
+11
View File
@@ -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() {
+2 -2
View File
@@ -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',
})
);
+3 -3
View File
@@ -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(
+16 -16
View File
@@ -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;
});
+7 -5
View File
@@ -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;
}
+1 -1
View File
@@ -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();
+4 -5
View File
@@ -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 {
+1 -1
View File
@@ -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;
}
+43
View File
@@ -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;
}
}
+6 -2
View File
@@ -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));
}
}
}
};
+67 -11
View File
@@ -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)]);
}
}
+3 -1
View File
@@ -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();
+3
View File
@@ -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