Compare commits

...
64 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
Wojciech Kozyra bc9b8442d7 v0.22.4 2021-08-02 13:07:52 +02:00
Wojciech Kozyra a84762b00d [eas-cli] override distribution when building for simulator 2021-08-02 12:57:59 +02:00
Wojciech Kozyra 2d117e1d7e v0.22.3 2021-08-02 12:49:51 +02:00
Sallar 986674942d [eas-json][eas-cli] add simulator key to build profile json validator (#546)
* Add simulator to build profile json validator

* Update CHANGELOG.md

* Fix PR number

* Change the order of simulator key
2021-08-02 12:46:21 +02:00
Wojciech Kozyra 13c224589b Update CHANGELOG 2021-08-02 10:57:16 +02:00
Wojciech Kozyra 20b091fbfa v0.22.2 2021-08-02 10:35:08 +02:00
Wojciech Kozyra 168b6afdfa [eas-cli] do not read BuildSettings on managed project 2021-08-02 10:10:03 +02:00
Dominik Sokal 3c6320b929 Update CHANGELOG.md 2021-07-30 14:03:18 +02:00
Dominik Sokal 4f1dc582bd [ENG-1570] evaluate interpolated iOS version strings (#541)
* evaluate interpolated ios version strings

* update CHANGELOG
2021-07-30 14:00:31 +02:00
Wojciech Kozyra daf2ef1e30 [eas-cli] fix unknown state error (#543)
* [eas-cli] fix unknown state error

* update CHANGELOG
2021-07-30 13:32:22 +02:00
Wojciech Kozyra f703bd8b5a [eas-cli] validate metadata in build request (#542)
* [eas-cli] validate metadata in build request

* Update CHANGELOG
2021-07-30 13:23:47 +02:00
Dominik Sokal 997cd7058a [ENG-1475] Additional filters for eas build:list (#540)
* generate graphql code

* use new App.builds api

* add more filters to build:list

* update CHANGELOG
2021-07-30 10:31:51 +02:00
Wojciech Kozyra e896c2f6c5 Update CHANGELOG 2021-07-29 14:55:04 +02:00
Wojciech Kozyra 3d723ca426 Use new eas.json format + auto migrate from old format (#537)
* Use new eas.json format + aot migrate old format

* review feedback

* review feedback 2

* fix
2021-07-29 14:50:08 +02:00
Dominik Sokal c17e1b3fa2 [ENG-1600] warn when building managed project that does not extend @expo/metro-config in metro.config.js (#534)
* warn when building managed project that does not extend @expo/metro-config in metro.config.js

* update CHANGELOG

* address PR feedback

* fix lint warnings/errors
2021-07-29 09:33:37 +02:00
Quinlan Jung d70fc36485 update changelog 2021-07-28 15:48:22 -07:00
Quinlan Jung 47784ca6d3 v0.22.1 2021-07-28 14:58:33 -07:00
Quinlan Jung 3e5a3aca27 [eas-cli] fix keystore upload bug (#538)
* [eas-cli] fix upload bug

* changelog
2021-07-28 14:55:36 -07:00
Dominik Sokal 89b0227594 [chore] upgrade: eslint, prettier, etc. (#536)
* upgrade: eslint, prettier, etc.

* run prettier
2021-07-28 11:12:06 +02:00
Dominik Sokal b98a30e7c6 install missing dep: fast-glob 2021-07-27 13:16:23 +02:00
Dominik Sokal 5c7862a685 upgrade typescript + clean up deps (#533)
* upgrade typescript to 4.3.5

* remove unused dependencies

* upgrade tslib

* update CHANGELOG
2021-07-27 12:22:39 +02:00
Bartłomiej KlocekandDominik Sokal 82833a9eb4 [eas submit][android] Autodetect google-services JSON key path (#520)
* foundation

* detection logic

* add tests

* Update CHANGELOG, ignore `node_modules`

* Display a chooser for multiple files

* Improve tests

* move `filterAsync` to a separate file

* Apply suggestions from code review

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

* Fix indent

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-27 12:22:25 +02:00
Dominik Sokal 66171c3c4b [ENG-1524] use envs from eas.json when resolving app config (#532)
* use envs from eas.json when resolving app config

* update CHANGELOG

* address PR feedback
2021-07-26 15:38:42 +02:00
Wojciech KozyraandDominik Sokal 179def91c7 [eas-cli] use .pbxproj file to check if native Xcode project is present (#531)
* [eas-cli] use .pbxproj file to check if native Xcode project is present

* Update CHANGELOG.md

* Update packages/eas-cli/src/project/ios/scheme.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-26 15:33:44 +02:00
Dominik Sokal 9f63ad4527 update CHANGELOG 2021-07-23 10:33:19 +02:00
171 changed files with 11768 additions and 7377 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:
+104 -2
View File
@@ -6,12 +6,116 @@ 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 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
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- 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
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
### 🎉 New features
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
@@ -19,8 +123,6 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
### 🎉 New features
-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.0"
"version": "0.25.0"
}
+16 -9
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": "^6.6.0",
"eslint-config-universe": "^4.0.0",
"eslint-plugin-graphql": "^4.0.0",
"jest": "^26.4.2",
"jest-watch-typeahead": "^0.6.1",
"lerna": "^3.22.1",
"prettier": "^2.0.5"
"@types/jest": "27.0.1",
"@types/node": "16.7.8",
"eslint": "7.32.0",
"eslint-config-universe": "7.0.1",
"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"
}
}
+73 -27
View File
@@ -42,6 +42,7 @@ eas --help COMMAND
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
@@ -71,7 +72,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -85,7 +86,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -99,7 +100,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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]`
@@ -110,7 +111,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -123,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
@@ -130,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.0/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]`
@@ -141,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.0/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`
@@ -155,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.0/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`
@@ -166,12 +168,29 @@ USAGE
$ eas build:list
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--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.0/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]`
@@ -180,9 +199,27 @@ 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.0/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`
Show the eas.json config
```
USAGE
$ eas config
OPTIONS
-p, --platform=(android|ios)
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.25.0/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
@@ -193,7 +230,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -204,7 +241,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -218,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.0/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]`
@@ -229,7 +266,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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]`
@@ -246,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`
@@ -257,7 +294,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -271,7 +308,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -288,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.0/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`
@@ -305,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.0/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`
@@ -316,7 +353,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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)`
@@ -348,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, ...)
@@ -363,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)
@@ -378,6 +422,8 @@ OPTIONS
--verbose Always print logs from Submission Service
--[no-]wait Wait for submission to complete
ALIASES
$ eas build:submit
@@ -396,7 +442,7 @@ EXAMPLES
and provide its App ID
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -415,7 +461,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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]`
@@ -429,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.0/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`
@@ -443,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.0/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`
@@ -463,7 +509,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.0/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`
@@ -477,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.0/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;
+2918 -860
View File
File diff suppressed because it is too large. Load diff
+43 -56
View File
@@ -1,8 +1,8 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.22.0",
"author": "Expo <support@expo.io>",
"version": "0.25.0",
"author": "Expo <support@expo.dev>",
"bin": {
"eas": "./bin/run"
},
@@ -10,104 +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.44",
"@expo/eas-json": "^0.22.0",
"@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.4",
"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": "1.14.1",
"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/color-string": "^1.5.0",
"@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/tough-cookie": "^4.0.0",
"@types/uuid": "^8.3.0",
"@types/wrap-ansi": "^3.0.0",
"@vercel/ncc": "^0.23.0",
"axios": "^0.21.0",
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"@types/semver": "7.3.8",
"@types/tar": "4.0.5",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.1",
"@types/wrap-ansi": "3.0.0",
"axios": "0.21.1",
"eslint-plugin-graphql": "4.0.0",
"memfs": "3.2.2",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"ts-node": "^8",
"typescript": "^3.9.7",
"wonka": "^4.0.14"
"nock": "^13.0.5"
},
"engines": {
"node": ">=8.0.0"
"node": ">=12.0.0"
},
"files": [
"/bin",
@@ -187,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`;
}
}
@@ -14,10 +14,11 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
+26 -26
View File
@@ -1,5 +1,4 @@
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -15,25 +14,24 @@ import {
} from '../../project/android/applicationId';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import {
BuildRequestSender,
CredentialsResult,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = await createBuildContextAsync<Platform.ANDROID>({
commandCtx,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
Log.addNewLineIfNone();
@@ -51,18 +49,22 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
if (ctx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
}
},
getMetadataContext: () => ({}),
prepareJobAsync,
sendBuildRequestAsync: async (
appId: string,
@@ -90,21 +92,19 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.commandCtx.projectDir,
ctx.commandCtx.exp
);
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
nonInteractive: ctx.commandCtx.nonInteractive,
await createCredentialsContextAsync(ctx.projectDir, {
exp: ctx.exp,
nonInteractive: ctx.nonInteractive,
}),
{
app: {
account: nullthrows(
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
`You do not have access to account: ${ctx.commandCtx.accountName}`
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.commandCtx.projectName,
projectName: ctx.projectName,
androidApplicationIdentifier,
},
}
@@ -1,4 +1,5 @@
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
@@ -13,13 +14,19 @@ interface JobData {
credentials?: AndroidCredentials;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -34,28 +41,28 @@ export async function prepareJobAsync(
}
: {};
const { gradleCommand } = ctx.buildProfile;
let buildType: Android.BuildType | undefined = ctx.buildProfile.buildType;
if (!buildType && !gradleCommand && ctx.buildProfile.distribution === 'internal') {
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const job = {
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
ndk: ctx.buildProfile.ndk,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
image: buildProfile.image ?? 'default',
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...buildCredentials,
@@ -63,11 +70,11 @@ export async function prepareJobAsync(
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
gradleCommand,
artifactPath: ctx.buildProfile.artifactPath,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
username,
buildType,
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
};
return sanitizeJob(job);
+130 -28
View File
@@ -1,19 +1,24 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { UploadSessionType } from '../graphql/generated';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import Log from '../log';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore } from '../log';
import { promptAsync } from '../prompts';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { requestedPlatformDisplayNames } from './constants';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { collectMetadata } from './metadata';
import { MetadataContext, collectMetadata } from './metadata';
import { Platform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
@@ -36,69 +41,68 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
getMetadataContext: () => MetadataContext<TPlatform>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends Job
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(builder.ctx),
async () => await builder.ensureCredentialsAsync(ctx),
{
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
}
);
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
});
}
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
}`,
{
nonInteractive: builder.ctx.commandCtx.nonInteractive,
}
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
const projectArchive = builder.ctx.commandCtx.local
const projectArchive = ctx.local
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(builder.ctx),
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadata(builder.ctx, {
credentialsSource: credentialsResult?.source,
});
const job = await builder.prepareJobAsync(builder.ctx, {
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadata(ctx, metadataContext);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (builder.ctx.commandCtx.local) {
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} 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.');
@@ -155,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;
@@ -177,7 +184,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
): Promise<string> {
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
async () => {
@@ -186,13 +193,13 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.commandCtx.projectId,
ctx.projectId,
job,
metadata
);
printDeprecationWarnings(deprecationInfo);
return build.id;
return build;
},
{
successEvent: Event.BUILD_REQUEST_SUCCESS,
@@ -214,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,
@@ -274,3 +281,98 @@ async function reviewAndCommitChangesAsync(
});
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log(
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
);
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
+55 -72
View File
@@ -1,12 +1,13 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
import Log, { learnMore } from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { promptAsync } from '../prompts';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
@@ -21,6 +22,37 @@ const configureCommitMessage = {
[RequestedPlatform.All]: 'Configure EAS Build',
};
export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
const message = 'This project is not configured to build with EAS. Set it up now?';
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: requestedPlatform,
});
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.dev/build/eas-json'
)})`,
{ exit: 1 }
);
}
}
export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
@@ -63,95 +95,46 @@ export async function configureAsync(options: {
}
}
const ANDROID_MANAGED_DEFAULTS = {
release: {
buildType: 'app-bundle',
},
const MANAGED_DEFAULTS = {
release: {},
development: {
buildType: 'development-client',
distribution: 'internal',
},
};
const ANDROID_GENERIC_DEFAULTS = {
release: {
gradleCommand: ':app:bundleRelease',
},
development: {
gradleCommand: ':app:assembleDebug',
developmentClient: true,
distribution: 'internal',
},
};
const IOS_MANAGED_DEFAULTS = {
release: {
buildType: 'release',
},
const GENERIC_DEFAULTS = {
release: {},
development: {
buildType: 'development-client',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
release: {
schemeBuildConfiguration: 'Release',
},
development: {
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
let existingEasJson;
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
await reader.validateAsync();
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAndValidateAsync();
existingEasJson = await reader.readRawAsync();
Log.withTick('Validated eas.json');
// If we have already populated eas.json with the default fields for the
// platform then proceed
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
return;
} else if (
ctx.requestedPlatform === 'all' &&
existingEasJson.builds.ios &&
existingEasJson.builds.android
) {
return;
}
return;
}
const shouldInitIOS =
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
const shouldInitAndroid =
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
const easJson = {
builds: {
...existingEasJson?.builds,
...(shouldInitAndroid
? {
android: ctx.hasAndroidNativeProject
? ANDROID_GENERIC_DEFAULTS
: ANDROID_MANAGED_DEFAULTS,
}
: null),
...(shouldInitIOS
? {
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : IOS_MANAGED_DEFAULTS,
}
: null),
},
};
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? { build: GENERIC_DEFAULTS }
: { build: MANAGED_DEFAULTS };
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcs.trackFileAsync(easJsonPath);
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
-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]: '🤖',
};
+59 -93
View File
@@ -1,11 +1,12 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { getProjectAccountName } from '../project/projectUtils';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
@@ -13,66 +14,6 @@ import { ensureLoggedInAsync } from '../user/actions';
import { Platform, RequestedPlatform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface CommandContext {
requestedPlatform: RequestedPlatform;
profile: string;
projectDir: string;
projectId: string;
user: Actor;
accountName: string;
projectName: string;
exp: ExpoConfig;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}
export async function createCommandContextAsync({
requestedPlatform,
profile,
exp,
projectDir,
projectId,
nonInteractive = false,
local,
clearCache = false,
skipProjectConfiguration = false,
waitForBuildEnd,
}: {
requestedPlatform: RequestedPlatform;
profile: string;
exp: ExpoConfig;
projectId: string;
projectDir: string;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}): Promise<CommandContext> {
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
return {
requestedPlatform,
profile,
projectDir,
projectId,
user,
accountName,
projectName,
exp,
nonInteractive,
local,
clearCache,
skipProjectConfiguration,
waitForBuildEnd,
};
}
export interface ConfigureContext {
user: Actor;
projectDir: string;
@@ -84,56 +25,83 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: IosBuildProfile;
export interface BuildContext<T extends Platform> {
commandCtx: CommandContext;
trackingCtx: TrackingContext;
accountName: string;
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
exp: ExpoConfig;
local: boolean;
nonInteractive: boolean;
platform: T;
buildProfile: PlatformBuildProfile<T>;
projectDir: string;
projectId: string;
projectName: string;
skipProjectConfiguration: boolean;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
}
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
easConfig,
commandCtx,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
easConfig: EasConfig;
commandCtx: CommandContext;
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
if (!buildProfile) {
throw new Error(`${platform} build profile does not exist`);
}
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(commandCtx.projectDir, platform);
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const devClientProperties = getDevClientEventProperties({
platform,
projectDir: commandCtx.projectDir,
projectDir,
buildProfile,
workflow,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
commandCtx,
workflow,
trackingCtx,
platform,
accountName,
buildProfile,
buildProfileName,
clearCache,
exp,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
}
@@ -141,21 +109,19 @@ function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
workflow,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
workflow: Workflow;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
} else if ('buildType' in buildProfile) {
includesDevClient = buildProfile.buildType === 'development-client';
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
} else if (buildProfile.developmentClient) {
includesDevClient = true;
} else {
includesDevClient = false;
}
-173
View File
@@ -1,173 +0,0 @@
import { EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { BuildFragment, BuildStatus } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { CommandContext } from './context';
import { prepareIosBuildAsync } from './ios/build';
import { Platform, RequestedPlatform } from './types';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
const scheduledBuilds = await startBuildsAsync(commandCtx);
if (commandCtx.local) {
return;
}
Log.newLine();
printLogsUrls(commandCtx.accountName, scheduledBuilds);
Log.newLine();
if (commandCtx.waitForBuildEnd) {
const builds = await waitForBuildEndAsync(scheduledBuilds.map(i => i.buildId));
printBuildResults(commandCtx.accountName, builds);
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
async function startBuildsAsync(
commandCtx: CommandContext
): Promise<{ platform: Platform; buildId: string }[]> {
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const easConfig = await new EasJsonReader(
commandCtx.projectDir,
commandCtx.requestedPlatform
).readAsync(commandCtx.profile);
const builds: {
platform: Platform;
sendBuildRequestAsync: () => Promise<string | undefined>;
}[] = [];
if (shouldBuildAndroid) {
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
}
if (shouldBuildIos) {
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
}
return (
await Promise.all(
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
platform,
buildId: await sendBuildRequestAsync(),
}))
)
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
}
async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
@@ -149,7 +149,7 @@ describe(readBuildNumberAsync, () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -157,7 +157,7 @@ describe(readBuildNumberAsync, () => {
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -167,21 +167,33 @@ describe(readShortVersionAsync, () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
shortVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
});
expect(buildNumber).toBe('1.0.0');
});
});
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
});
});
function initGenericProject(): ExpoConfig {
function initGenericProject({
shortVersion = '1.0.0',
version = '1',
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
@@ -197,9 +209,9 @@ function initGenericProject(): ExpoConfig {
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>${shortVersion}</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>${version}</string>
</dict>
</plist>`,
},
+35 -26
View File
@@ -1,66 +1,75 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
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';
import { prepareJobAsync } from './prepareJob';
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = await createBuildContextAsync<Platform.IOS>({
commandCtx,
platform: Platform.IOS,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
);
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: commandCtx.projectDir,
nonInteractive: commandCtx.nonInteractive,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
});
},
getMetadataContext: (): IosMetadataContext => {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return { buildSettings };
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
+1 -1
View File
@@ -38,7 +38,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
+7 -11
View File
@@ -16,20 +16,16 @@ export async function ensureIosCredentialsAsync(
return;
}
const { projectDir } = buildCtx.commandCtx;
const ctx = await createCredentialsContextAsync(projectDir, {
nonInteractive: buildCtx.commandCtx.nonInteractive,
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
exp: buildCtx.exp,
nonInteractive: buildCtx.nonInteractive,
});
const provider = new IosCredentialsProvider(ctx, {
app: getAppFromContext(ctx),
const provider = new IosCredentialsProvider(credentialsCtx, {
app: getAppFromContext(credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.workflow
),
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
@@ -45,5 +41,5 @@ export async function ensureIosCredentialsAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return buildCtx.buildProfile.distribution !== 'simulator';
return !buildCtx.buildProfile.simulator;
}
+18 -11
View File
@@ -15,13 +15,18 @@ interface JobData {
buildScheme: string;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
@@ -30,12 +35,12 @@ export async function prepareJobAsync(
}
}
const job = {
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.distribution,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
node: ctx.buildProfile.node,
@@ -47,8 +52,9 @@ export async function prepareJobAsync(
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
clear: ctx.clearCache,
},
secrets: {
buildCredentials,
@@ -57,20 +63,21 @@ export async function prepareJobAsync(
updates: { channel: ctx.buildProfile.channel },
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.schemeBuildConfiguration,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.buildType,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
if (!ctx.buildProfile.image && ctx.commandCtx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.commandCtx.exp.sdkVersion);
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
if (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
+11 -4
View File
@@ -4,11 +4,13 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { evaluateString } from '../utils/template';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
@@ -104,12 +106,16 @@ export async function bumpVersionInAppJsonAsync({
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
return (
infoPlist.CFBundleShortVersionString &&
evaluateString(infoPlist.CFBundleShortVersionString, buildSettings)
);
} else {
return exp.version;
}
@@ -117,12 +123,13 @@ export async function readShortVersionAsync(
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
return infoPlist.CFBundleVersion && evaluateString(infoPlist.CFBundleVersion, buildSettings);
} else {
return IOSConfig.Version.getBuildNumber(exp);
}
+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(
+44 -30
View File
@@ -1,5 +1,6 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource, IosEnterpriseProvisioning } from '@expo/eas-json';
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
@@ -27,56 +28,69 @@ import { isExpoUpdatesInstalled } from './utils/updates';
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? object
: IosMetadataContext;
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function collectMetadata<T extends Platform>(
ctx: BuildContext<T>,
{
credentialsSource,
}: {
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
platformContext?: MetadataContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
return {
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
appVersion: await resolveAppVersionAsync(ctx, platformContext),
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
cliVersion: packageJSON.version,
workflow: ctx.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
runtimeVersion: ctx.commandCtx.exp.runtimeVersion,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
distribution,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx),
buildProfile: ctx.commandCtx.profile,
buildProfile: ctx.buildProfileName,
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
};
return sanitizeMetadata(metadata);
}
async function resolveAppVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
return await readVersionNameAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveAppBuildVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
const versionCode = await readVersionCodeAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.exp);
return versionCode !== undefined ? String(versionCode) : undefined;
}
}
@@ -85,16 +99,16 @@ async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
return await getBundleIdentifierAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
} else {
return await getApplicationIdAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getApplicationIdAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
@@ -116,10 +130,10 @@ async function getNativeReleaseChannelAsync<T extends Platform>(
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
@@ -131,10 +145,10 @@ async function getNativeChannelAsync<T extends Platform>(
): Promise<string | undefined> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
case Platform.IOS: {
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
}
+6
View File
@@ -15,4 +15,10 @@ export enum BuildStatus {
CANCELED = 'canceled',
}
export enum BuildDistributionType {
STORE = 'store',
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -5,13 +5,12 @@ 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) {
const actor = getActorName(build);
const account = build.project.__typename === 'App' ? build.project.ownerAccount.name : 'unknown';
const fields: { label: string; value?: string | null }[] = [
{ label: 'ID', value: build.id },
{
@@ -77,7 +76,7 @@ export function formatGraphQLBuild(build: BuildFragment) {
},
{
label: 'Logs',
value: getBuildLogsUrl({ buildId: build.id, account }),
value: getBuildLogsUrl(build),
},
{
label: 'Artifact',
@@ -4,6 +4,7 @@ import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
import {
AppPlatform,
BuildError,
BuildFragment,
BuildStatus,
@@ -12,51 +13,39 @@ import {
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../constants';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(
accountName: string,
builds: { platform: 'android' | 'ios'; buildId: string }[]
): void {
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
const { buildId } = builds[0];
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(({ buildId, platform }) => {
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${requestedPlatformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
);
});
}
}
export function printBuildResults(accountName: string, builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(accountName, build);
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
if (json) {
printJsonOnlyOutput(builds);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build =>
printBuildResult(accountName, 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));
}
}
}
function printBuildResult(accountName: string, build: BuildFragment): void {
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
const userError = build.error;
@@ -80,12 +69,14 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
}
if (build.distribution === DistributionType.Internal) {
const logsUrl = getBuildLogsUrl({
buildId: build.id,
account: accountName,
});
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
const logsUrl = getBuildLogsUrl(build);
// 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]
@@ -0,0 +1,5 @@
import template from 'lodash/template';
export function evaluateString(s: string, vars: Record<string, any>): string {
return template(s, { interpolate: /\$\(([\s\S]+?)\)/g })(vars);
}
+11 -8
View File
@@ -3,14 +3,17 @@ import assert from 'assert';
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getBuildLogsUrl({
buildId,
account,
}: {
buildId: string;
account: string;
}): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
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}/builds/${build.id}`;
} else {
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
}
}
export function getArtifactUrl(artifactId: string): string {
+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,
@@ -67,9 +67,9 @@ async function selectBuildToCancelAsync(
let builds;
try {
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.New }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
]);
spinner.stop();
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
@@ -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',
})}`);
}
+211 -145
View File
@@ -1,24 +1,57 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { configureAsync } from '../../build/configure';
import { createCommandContextAsync } from '../../build/context';
import { buildAsync } from '../../build/create';
import { RequestedPlatform } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { Workflow } from '@expo/eas-build-job';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error, exit } from '@oclif/errors';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { Platform, RequestedPlatform } from '../../build/types';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildFragment, BuildStatus } from '../../graphql/generated';
import Log from '../../log';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
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 {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
'clear-cache': boolean;
json: boolean;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
}
export default class Build extends EasCommand {
static description = 'Start a build';
@@ -28,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',
@@ -56,155 +93,184 @@ export default class Build extends EasCommand {
};
async run(): Promise<void> {
const { flags } = this.parse(Build);
const { flags: rawFlags } = this.parse(Build);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
const startedBuilds: BuildFragment[] = [];
let metroConfigValidated = false;
for (const platform of platformsToBuild) {
const ctx = await createBuildContextAsync({
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(ctx);
metroConfigValidated = true;
}
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const maybeBuild = await this.startBuildAsync(ctx);
if (maybeBuild) {
startedBuilds.push(maybeBuild);
}
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds);
Log.newLine();
if (flags.wait) {
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds, flags.json);
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
throw new Error('--platform is required when building in non-interactive mode');
}
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
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());
if (flags.local) {
if (requestedPlatform === RequestedPlatform.All) {
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
if (flags['skip-credentials-check']) {
Log.warnDeprecatedFlag(
'skip-credentials-check',
'Build credential validation is always skipped with the --non-interactive flag. You can also skip interactively.'
'Build credentials validation is always skipped with the --non-interactive flag. You can also skip interactively.'
);
Log.newLine();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
process.exitCode = 1;
return;
}
exp = (await ensureProjectConfiguredAsync(projectDir, platform)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
profile: flags.profile,
exp,
projectDir,
projectId,
nonInteractive,
clearCache: flags['clear-cache'],
local: flags.local,
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
profile: flags['profile'],
nonInteractive,
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
};
}
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{ title: 'All', value: RequestedPlatform.All },
{ title: 'iOS', value: RequestedPlatform.Ios },
{ title: 'Android', value: RequestedPlatform.Android },
],
});
await buildAsync(commandCtx);
}
}
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
if (platform === RequestedPlatform.All) {
Log.error('Builds for multiple platforms are not supported with flag --local');
return false;
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
Log.error('Unsupported platform, macOS is required to build apps for iOS');
return false;
} else {
return true;
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.Ios,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
async function ensureProjectConfiguredAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<ExpoConfig | null> {
const platformsToConfigure = await getPlatformsToConfigureAsync(projectDir, platform);
if (!platformsToConfigure) {
return null;
}
// Ensure the prompt is consistent with the platforms we need to configure
let message = 'This project is not configured to build with EAS. Set it up now?';
if (platformsToConfigure === RequestedPlatform.Ios) {
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
} else if (platformsToConfigure === RequestedPlatform.Android) {
message = 'Your Android project is not configured to build with EAS. Set it up now?';
}
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: platformsToConfigure,
});
if (await vcs.hasUncommittedChangesAsync()) {
throw new Error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
);
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
return exp;
} else {
throw new Error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`
);
}
}
async function getPlatformsToConfigureAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<RequestedPlatform | null> {
if (!(await fs.pathExists(path.join(projectDir, 'eas.json')))) {
return platform;
}
const easConfig = await new EasJsonReader(projectDir, platform).readRawAsync();
if (platform === RequestedPlatform.All) {
if (easConfig.builds?.android && easConfig.builds?.ios) {
return null;
} else if (easConfig.builds?.ios) {
return RequestedPlatform.Android;
} else if (easConfig.builds?.android) {
return RequestedPlatform.Ios;
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [Platform.ANDROID, Platform.IOS];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [Platform.ANDROID];
} else {
return [Platform.IOS];
}
} else if (
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
easConfig.builds?.[platform]
) {
return null;
}
return platform;
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
let sendBuildRequestAsync: BuildRequestSender;
if (ctx.platform === Platform.ANDROID) {
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
} else {
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
}
return await sendBuildRequestAsync();
}
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
exit(1);
}
}
}
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
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.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"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 });
}
}
}
+73 -12
View File
@@ -3,9 +3,13 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { BuildStatus, RequestedPlatform } from '../../build/types';
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import {
@@ -13,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';
@@ -21,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,
@@ -31,16 +39,40 @@ export default class BuildList extends Command {
BuildStatus.CANCELED,
],
}),
distribution: flags.enum({
options: [
BuildDistributionType.STORE,
BuildDistributionType.INTERNAL,
BuildDistributionType.SIMULATOR,
],
}),
channel: flags.string(),
appVersion: flags.string(),
appBuildVersion: flags.string(),
sdkVersion: flags.string(),
runtimeVersion: flags.string(),
appIdentifier: flags.string(),
buildProfile: flags.string(),
gitCommitHash: flags.string(),
limit: flags.integer(),
};
async run() {
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
BuildList
).flags;
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);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -52,8 +84,19 @@ export default class BuildList extends Command {
try {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
platform,
status: graphqlBuildStatus,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
});
if (builds.length) {
@@ -65,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}`);
}
@@ -90,7 +137,7 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
}
};
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | undefined => {
if (!buildStatus) {
return undefined;
} else if (buildStatus === BuildStatus.NEW) {
@@ -107,3 +154,17 @@ const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | un
return GraphQLBuildStatus.Canceled;
}
};
const toGraphQLBuildDistribution = (
buildDistribution?: BuildDistributionType
): DistributionType | undefined => {
if (buildDistribution === BuildDistributionType.STORE) {
return DistributionType.Store;
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
return DistributionType.Internal;
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
return DistributionType.Simulator;
} else {
return undefined;
}
};
+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}`);
@@ -37,9 +37,7 @@ async function promptForRolloutPercentAsync({
return rolloutPercent;
}
function getRolloutInfo(
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery
): {
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
@@ -48,12 +46,14 @@ function getRolloutInfo(
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
);
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const newBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!newBranch || !oldBranch) {
throw new Error(
@@ -140,9 +140,10 @@ async function startRolloutAsync({
branchMapping: JSON.stringify(newBranchMapping),
});
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
@@ -37,9 +37,11 @@ export type BranchMapping = {
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(
branchMappingString?: string
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
+68
View File
@@ -0,0 +1,68 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Command, flags } from '@oclif/command';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export { Platform } from '@expo/eas-build-job';
export default class Config extends Command {
static description = 'Show the eas.json config';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: flags.string(),
};
async run() {
const { flags } = this.parse(Config);
const { platform: maybePlatform, profile: maybeProfile } = flags as {
platform?: Platform;
profile?: string;
};
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
'Select build profile',
(
await reader.getBuildProfileNamesAsync()
).map(profileName => ({
title: profileName,
value: profileName,
}))
));
const platform =
maybePlatform ??
(await selectAsync('Select platform', [
{
title: 'Android',
value: Platform.ANDROID,
},
{
title: 'iOS',
value: Platform.IOS,
},
]));
const profile = await reader.readBuildProfileAsync(profileName, platform);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
}
+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: [
{
@@ -13,9 +13,8 @@ export class AssignFcm {
ctx: Context,
fcm: AndroidFcmFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
this.app
);
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
appCredentials,
{
@@ -23,9 +23,8 @@ export async function canCopyLegacyCredentialsAsync(
return false; // modern credentials already exist
}
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
const legacyAppCredentials =
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
return !!legacyAppCredentials; // user has some legacy credentials
}
@@ -41,16 +40,14 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
const spinner = ora('Classic credentials detected, copying to EAS...').start();
try {
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
const legacyAppCredentials =
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
if (!legacyAppCredentials) {
return;
}
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
app
);
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(app);
const legacyFcm = legacyAppCredentials.androidFcm;
if (legacyFcm) {
const clonedFcm = await ctx.android.createFcmAsync(
@@ -120,9 +117,8 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
!ctx.nonInteractive,
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
);
const existingDefaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
appLookupParams
);
const existingDefaultBuildCredentials =
await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(appLookupParams);
if (existingDefaultBuildCredentials) {
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
existingDefaultBuildCredentials,
@@ -25,6 +25,7 @@ export class CreateKeystore {
if (providedKeystore) {
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
validateKeystore(providedKeystoreWithType);
return providedKeystoreWithType;
}
return await generateRandomKeystoreAsync();
}
@@ -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,11 +1,15 @@
import { confirmAsync } from '../../../../prompts';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../utils/promptForCredentials');
describe('CreateKeystore', () => {
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -16,6 +20,20 @@ describe('CreateKeystore', () => {
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
});
it('creates a keystore by uploading', async () => {
(askForUserProvidedAsync as jest.Mock).mockImplementation(() => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
await createKeystoreAction.runAsync(ctx);
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledWith(
appLookupParams.account,
expect.objectContaining(testKeystore)
);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
@@ -120,9 +120,8 @@ export async function createAndroidAppBuildCredentialsAsync(
androidKeystoreId: string;
}
): Promise<AndroidAppBuildCredentialsFragment> {
const androidAppCredentials = await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
appLookupParams
);
const androidAppCredentials =
await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(appLookupParams);
const buildCredentialsList = androidAppCredentials.androidAppBuildCredentialsList;
const existingDefaultBuildCredentials =
buildCredentialsList.find(buildCredentials => buildCredentials.isDefault) ?? null;
@@ -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) {
+9 -6
View File
@@ -3,6 +3,7 @@ import chalk from 'chalk';
import pick from 'lodash/pick';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName } from '../project/projectUtils';
import { confirmAsync } from '../prompts';
import { Actor, getActorDisplayName } from '../user/User';
@@ -23,6 +24,7 @@ interface AppleCtxOptions {
interface Options extends AppleCtxOptions {
nonInteractive?: boolean;
exp?: ExpoConfig;
}
export interface Context {
@@ -45,12 +47,13 @@ export async function createCredentialsContextAsync(
): Promise<Context> {
const user = await ensureLoggedInAsync();
let expoConfig: ExpoConfig | undefined;
try {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
expoConfig = exp;
} catch (error) {
// ignore error, context might be created outside of expo project
let expoConfig: ExpoConfig | undefined = options.exp;
if (!expoConfig) {
try {
expoConfig = getExpoConfig(projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
}
return new CredentialsContext(projectDir, user, expoConfig, options);
@@ -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';
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { CredentialsSource, DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
@@ -18,7 +18,7 @@ import { isAdHocProfile } from './utils/provisioningProfile';
interface Options {
app: App;
targets: Target[];
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -67,12 +67,8 @@ export class ConfigureProvisioningProfile {
app: AppLookupParams,
profileFromApple: ProvisioningProfileStoreInfo
): Promise<AppleProvisioningProfileMutationResult> {
const {
developerPortalIdentifier,
certificateP12,
certificatePassword,
serialNumber,
} = this.distributionCertificate;
const { developerPortalIdentifier, certificateP12, certificatePassword, serialNumber } =
this.distributionCertificate;
assert(
certificateP12 && certificatePassword,
'Distribution Certificate P12 and Password is required'
@@ -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({
@@ -101,11 +101,12 @@ export class SetupAdhocProvisioningProfile {
const chosenDevices = await chooseDevices(registeredAppleDevices, provisionedDeviceIdentifiers);
// 4. Reuse or create the profile on Apple Developer Portal
const provisioningProfileStoreInfo = await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
chosenDevices.map(({ identifier }) => identifier),
this.app.bundleIdentifier,
distCert.serialNumber
);
const provisioningProfileStoreInfo =
await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
chosenDevices.map(({ identifier }) => identifier),
this.app.bundleIdentifier,
distCert.serialNumber
);
// 5. Create or update the profile on servers
const appleAppIdentifier = await ctx.ios.createOrGetExistingAppleAppIdentifierAsync(
@@ -1,4 +1,4 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -12,7 +12,7 @@ import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
interface Options {
app: App;
targets: Target[];
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -1,4 +1,4 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import {
IosDistributionType as GraphQLIosDistributionType,
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
interface Options {
app: GraphQLAppLookupParams;
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -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);
@@ -52,11 +52,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
@@ -80,11 +81,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
@@ -112,11 +114,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
@@ -145,11 +148,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
@@ -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')
@@ -25,24 +25,30 @@ const validateBooleanOptions = (options: any): boolean => {
return typeof options === 'boolean';
};
const validatePrefixedStringArrayOptions = (prefix: string) => (options: any): boolean => {
return (
Array.isArray(options) &&
options.every(option => typeof option === 'string' && option.startsWith(prefix))
);
};
const validatePrefixedStringArrayOptions =
(prefix: string) =>
(options: any): boolean => {
return (
Array.isArray(options) &&
options.every(option => typeof option === 'string' && option.startsWith(prefix))
);
};
const validateStringArrayOptions = (options: any): boolean => {
return Array.isArray(options) && options.every(option => typeof option === 'string');
};
const createValidateStringOptions = (allowed: string[]) => (options: any): boolean => {
return allowed.includes(options);
};
const createValidateStringOptions =
(allowed: string[]) =>
(options: any): boolean => {
return allowed.includes(options);
};
const createValidateStringArrayOptions = (allowed: string[]) => (options: any): boolean => {
return Array.isArray(options) && options.every(option => allowed.includes(option));
};
const createValidateStringArrayOptions =
(allowed: string[]) =>
(options: any): boolean => {
return Array.isArray(options) && options.every(option => allowed.includes(option));
};
const validateDevProdString = createValidateStringOptions(['development', 'production']);
@@ -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(
@@ -73,9 +73,8 @@ async function findProfileByBundleIdAsync(
// there is an expo managed profile with our desired certificate
// return the profile that will be valid for the longest duration
return {
profile: expoProfilesWithCertificate.sort(sortByExpiration)[
expoProfilesWithCertificate.length - 1
],
profile:
expoProfilesWithCertificate.sort(sortByExpiration)[expoProfilesWithCertificate.length - 1],
didUpdate: false,
};
} else if (expoProfiles) {
@@ -228,16 +227,12 @@ export async function createOrReuseAdhocProvisioningProfileAsync(
const spinner = ora(`Handling Apple ad hoc provisioning profiles`).start();
try {
const context = getRequestContext(authCtx);
const {
didUpdate,
didCreate,
profileName,
...adhocProvisioningProfile
} = await manageAdHocProfilesAsync(context, {
udids,
bundleId: bundleIdentifier,
certSerialNumber: distCertSerialNumber,
});
const { didUpdate, didCreate, profileName, ...adhocProvisioningProfile } =
await manageAdHocProfilesAsync(context, {
udids,
bundleId: bundleIdentifier,
certSerialNumber: distCertSerialNumber,
});
if (didCreate) {
spinner.succeed(`Created new profile: ${profileName}`);
@@ -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 });
}
@@ -11,6 +11,7 @@ export interface App {
export interface Target {
targetName: string;
buildConfiguration?: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
}
@@ -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"`;
@@ -21,12 +21,10 @@ describe('print credentials', () => {
},
projectName: 'test52',
};
const testIosAppCredentialsData = await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(
'@quinlanj/test52',
{
const testIosAppCredentialsData =
await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync('@quinlanj/test52', {
appleAppIdentifierId: 'test-id',
}
);
});
const appCredentials = {
test52: nullthrows(testIosAppCredentialsData),
};
@@ -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(
@@ -162,14 +162,8 @@ function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsF
const maybeProvProf = buildCredentials.provisioningProfile;
Log.log(` Provisioning Profile:`);
if (maybeProvProf) {
const {
expiration,
updatedAt,
status,
developerPortalIdentifier,
appleTeam,
appleDevices,
} = maybeProvProf;
const { expiration, updatedAt, status, developerPortalIdentifier, appleTeam, appleDevices } =
maybeProvProf;
if (developerPortalIdentifier) {
Log.log(` Developer Portal ID: ${developerPortalIdentifier}`);
}
@@ -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;
}
@@ -1,4 +1,5 @@
import { EasJsonReader, IosBuildProfile } from '@expo/eas-json';
import { Platform } from '@expo/eas-build-job';
import { IosBuildProfile } from '@expo/eas-json';
import assert from 'assert';
import nullthrows from 'nullthrows';
@@ -201,9 +202,8 @@ export class ManageIos {
const iosAppCredentialsMap: IosAppCredentialsMap = {};
for (const target of targets) {
const appLookupParams = getAppLookupParamsFromContext(ctx, target);
iosAppCredentialsMap[
target.targetName
] = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
iosAppCredentialsMap[target.targetName] =
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
}
displayIosCredentials(app, iosAppCredentialsMap, targets);
}
@@ -285,9 +285,10 @@ export class ManageIos {
}
const app = { account, projectName: ctx.exp.slug };
const easJsonReader = new EasJsonReader(ctx.projectDir, 'ios');
const easConfig = await new SelectBuildProfileFromEasJson(easJsonReader).runAsync(ctx);
const buildProfile = nullthrows(easConfig.builds.ios, 'iOS build profile must be defined');
const buildProfile = await new SelectBuildProfileFromEasJson(
ctx.projectDir,
Platform.IOS
).runAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
@@ -16,9 +16,7 @@ export enum SelectAndroidBuildCredentialsResultType {
export class SelectAndroidBuildCredentials {
constructor(private app: AppLookupParams) {}
async runAsync(
ctx: Context
): Promise<
async runAsync(ctx: Context): Promise<
| {
resultType: SelectAndroidBuildCredentialsResultType.CREATE_REQUEST;
result: AndroidAppBuildCredentialsMetadataInput;
@@ -1,14 +1,20 @@
import { EasConfig, EasJsonReader } from '@expo/eas-json';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { Context } from '../context';
export class SelectBuildProfileFromEasJson {
constructor(private easJsonReader: EasJsonReader) {}
async runAsync(ctx: Context): Promise<EasConfig> {
export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonReader: EasJsonReader;
constructor(private projectDir: string, private platform: T) {
this.easJsonReader = new EasJsonReader(projectDir);
}
async runAsync(ctx: Context): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
const easConfig = await this.easJsonReader.readAsync(profileName);
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(profileName, this.platform);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
@@ -17,7 +23,7 @@ export class SelectBuildProfileFromEasJson {
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];
@@ -8,8 +8,8 @@ export class SelectIosDistributionTypeGraphqlFromBuildProfile {
constructor(private buildProfile: IosBuildProfile) {}
async runAsync(ctx: Context): Promise<IosDistributionTypeGraphql> {
const { distribution } = this.buildProfile;
if (distribution === 'simulator') {
const { distribution, simulator } = this.buildProfile;
if (simulator) {
throw new Error('A simulator distribution does not require credentials to be configured.');
} else if (distribution === 'store') {
return IosDistributionTypeGraphql.AppStore;
@@ -7,15 +7,14 @@ import { SelectIosDistributionTypeGraphqlFromBuildProfile } from '../SelectIosDi
describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('errors with a simulator distribution', async () => {
const buildProfile = {
distribution: 'simulator',
simulator: true,
} as IosBuildProfile;
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
await expect(
selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx)
).rejects.toThrowError();
@@ -28,12 +27,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
ctx
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
const iosDistributionTypeGraphql =
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AppStore);
});
it('returns ENTERPRISE type with an internal distribution with the universal provisioningEnterprise configuration', async () => {
@@ -45,12 +42,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
ctx
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
const iosDistributionTypeGraphql =
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.Enterprise);
});
it('returns ADHOC type with an internal distribution with the adhoc provisioningEnterprise configuration', async () => {
@@ -62,12 +57,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
ctx
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
const iosDistributionTypeGraphql =
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AdHoc);
});
});
@@ -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:'
);
@@ -26,10 +28,11 @@ async function generateDeviceRegistrationURLAsync(
accountId: string,
appleTeam: Pick<AppleTeam, 'id'>
) {
const appleDeviceRegistrationRequest = await AppleDeviceRegistrationRequestMutation.createAppleDeviceRegistrationRequestAsync(
appleTeam.id,
accountId
);
const appleDeviceRegistrationRequest =
await AppleDeviceRegistrationRequestMutation.createAppleDeviceRegistrationRequestAsync(
appleTeam.id,
accountId
);
return formatRegistrationURL(appleDeviceRegistrationRequest.id);
}
+170 -58
View File
@@ -436,6 +436,9 @@ export type App = Project & {
buildOrBuildJobs: Array<BuildOrBuildJob>;
/** EAS Submissions associated with this app */
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 */
@@ -482,11 +485,13 @@ export type App = Project & {
users?: Maybe<Array<Maybe<User>>>;
/** @deprecated Field no longer supported */
releases: Array<Maybe<AppRelease>>;
latestReleaseForReleaseChannel?: Maybe<AppRelease>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppBuildsArgs = {
filter?: Maybe<BuildFilter>;
offset: Scalars['Int'];
limit: Scalars['Int'];
status?: Maybe<BuildStatus>;
@@ -517,6 +522,22 @@ export type AppSubmissionsArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: 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>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppIosAppCredentialsArgs = {
filter?: Maybe<IosAppCredentialsFilter>;
@@ -589,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'];
@@ -605,6 +633,25 @@ export enum AppPrivacy {
Hidden = 'HIDDEN'
}
export type BuildFilter = {
platform?: Maybe<AppPlatform>;
status?: Maybe<BuildStatus>;
distribution?: Maybe<DistributionType>;
channel?: Maybe<Scalars['String']>;
appVersion?: Maybe<Scalars['String']>;
appBuildVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
appIdentifier?: Maybe<Scalars['String']>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
};
export enum AppPlatform {
Ios = 'IOS',
Android = 'ANDROID'
}
export enum BuildStatus {
New = 'NEW',
InQueue = 'IN_QUEUE',
@@ -614,9 +661,10 @@ export enum BuildStatus {
Canceled = 'CANCELED'
}
export enum AppPlatform {
Ios = 'IOS',
Android = 'ANDROID'
export enum DistributionType {
Store = 'STORE',
Internal = 'INTERNAL',
Simulator = 'SIMULATOR'
}
/** Represents an EAS Build */
@@ -629,6 +677,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
/** @deprecated User type is deprecated */
initiatingUser?: Maybe<User>;
initiatingActor?: Maybe<Actor>;
cancelingActor?: Maybe<Actor>;
artifacts?: Maybe<BuildArtifacts>;
logFiles: Array<Scalars['String']>;
updatedAt?: Maybe<Scalars['DateTime']>;
@@ -834,12 +883,6 @@ export type BuildMetrics = {
buildDuration?: Maybe<Scalars['Int']>;
};
export enum DistributionType {
Store = 'STORE',
Internal = 'INTERNAL',
Simulator = 'SIMULATOR'
}
export enum BuildIosEnterpriseProvisioning {
Adhoc = 'ADHOC',
Universal = 'UNIVERSAL'
@@ -858,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;
@@ -872,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>;
};
@@ -943,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 = {
@@ -984,6 +1030,44 @@ 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: Scalars['ID'];
runtimeVersion: Scalars['String'];
channel: Scalars['String'];
recentBuilds: Array<Build>;
branchMapping?: Maybe<UpdateBranch>;
mostRecentlyUpdatedAt: Scalars['DateTime'];
};
export type UpdateBranch = {
__typename?: 'UpdateBranch';
id: Scalars['ID'];
appId: Scalars['ID'];
name: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
updates: Array<Update>;
};
export type UpdateBranchUpdatesArgs = {
offset: Scalars['Int'];
limit: Scalars['Int'];
filter?: Maybe<UpdatesFilter>;
};
export type UpdatesFilter = {
platform?: Maybe<AppPlatform>;
runtimeVersions?: Maybe<Array<Scalars['String']>>;
};
export type IosAppCredentialsFilter = {
appleAppIdentifierId?: Maybe<Scalars['String']>;
};
@@ -1149,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 */
@@ -1191,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'];
@@ -1239,28 +1336,6 @@ export type UpdateChannelUpdateBranchesArgs = {
limit: Scalars['Int'];
};
export type UpdateBranch = {
__typename?: 'UpdateBranch';
id: Scalars['ID'];
appId: Scalars['ID'];
name: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
updates: Array<Update>;
};
export type UpdateBranchUpdatesArgs = {
offset: Scalars['Int'];
limit: Scalars['Int'];
filter?: Maybe<UpdatesFilter>;
};
export type UpdatesFilter = {
platform?: Maybe<AppPlatform>;
runtimeVersions?: Maybe<Array<Scalars['String']>>;
};
export type EnvironmentSecret = {
__typename?: 'EnvironmentSecret';
id: Scalars['ID'];
@@ -2842,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;
};
@@ -2858,6 +2938,11 @@ export type SubmissionMutationCreateIosSubmissionArgs = {
input: CreateIosSubmissionInput;
};
export type SubmissionMutationCreateAndroidSubmissionArgs = {
input: CreateAndroidSubmissionInput;
};
export type CreateSubmissionInput = {
appId: Scalars['ID'];
platform: AppPlatform;
@@ -2873,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';
/**
@@ -2956,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>;
};
@@ -2976,8 +3078,8 @@ export type UpdateBranchMutationDeleteUpdateBranchArgs = {
};
export type UpdateBranchMutationPublishUpdateGroupArgs = {
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
export type UpdateBranchMutationPublishUpdateGroupsArgs = {
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
};
export type EditUpdateBranchInput = {
@@ -3015,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'];
};
@@ -3185,6 +3287,7 @@ export type MeMutationLeaveAccountArgs = {
export type MeMutationInitiateSecondFactorAuthenticationArgs = {
deviceConfigurations: Array<Maybe<SecondFactorDeviceConfiguration>>;
recaptchaResponseToken?: Maybe<Scalars['String']>;
};
@@ -4630,6 +4733,7 @@ export type CreateAndroidBuildMutation = (
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
& BuildFragment
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
@@ -4654,6 +4758,7 @@ export type CreateIosBuildMutation = (
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
& BuildFragment
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
@@ -4731,7 +4836,7 @@ export type GetSignedUploadMutation = (
);
export type UpdatePublishMutationVariables = Exact<{
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
}>;
@@ -4739,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'>
)> }
) }
);
@@ -4763,6 +4868,7 @@ export type CreateSubmissionMutation = (
& { submission: (
{ __typename?: 'Submission' }
& Pick<Submission, 'id'>
& SubmissionFragment
) }
) }
) }
@@ -4854,8 +4960,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
appId: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
status?: Maybe<BuildStatus>;
platform?: Maybe<AppPlatform>;
filter?: Maybe<BuildFilter>;
}>;
@@ -5078,10 +5183,10 @@ export type BuildFragment = (
& Pick<Robot, 'firstName' | 'id'>
)>, project: (
{ __typename: 'Snack' }
& Pick<Snack, 'id'>
& Pick<Snack, 'id' | 'name'>
) | (
{ __typename: 'App' }
& Pick<App, 'id'>
& Pick<App, 'id' | 'name'>
& { ownerAccount: (
{ __typename?: 'Account' }
& Pick<Account, 'id' | 'name'>
@@ -5097,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,9 +1,11 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import nullthrows from 'nullthrows';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
AndroidJobInput,
BuildFragment,
BuildMetadataInput,
CreateAndroidBuildMutation,
CreateAndroidBuildMutationVariables,
@@ -12,9 +14,10 @@ import {
EasBuildDeprecationInfo,
IosJobInput,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
export interface BuildResult {
build: { id: string };
build: BuildFragment;
deprecationInfo?: EasBuildDeprecationInfo | null;
}
@@ -37,6 +40,7 @@ const BuildMutation = {
createAndroidBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
...BuildFragment
}
deprecationInfo {
type
@@ -45,6 +49,7 @@ const BuildMutation = {
}
}
}
${print(BuildFragmentNode)}
`,
input
)
@@ -70,6 +75,7 @@ const BuildMutation = {
createIosBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
...BuildFragment
}
deprecationInfo {
type
@@ -78,6 +84,7 @@ const BuildMutation = {
}
}
}
${print(BuildFragmentNode)}
`,
input
)
@@ -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;
},
};
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff. Show more