Compare commits

...
62 Commits
Author SHA1 Message Date
Juwan Wheatley 9021887072 v0.27.1 2021-09-10 18:59:11 -04:00
Juwan Wheatley 6a80d4c8e9 [eas-cli] fix secrets mutations (#606)
* [eas-cli] fix secrets mutations

* add changelog entry

* fix typo
2021-09-10 18:42:10 -04:00
Dominik Sokal 471e26817a update CHANGELOG 2021-09-10 14:42:07 +02:00
Dominik Sokal 14bbd1ee0f v0.27.0 2021-09-10 14:30:41 +02:00
Dominik Sokal 22b06b444d [eas-json] allow env vars in android.serviceAccountKeyPath (#604)
* [eas-json] allow env vars in android.serviceAccountKeyPath

* update CHANGELOG
2021-09-10 14:28:21 +02:00
Dominik Sokal cebd29e40d [ENG-755] implement auto-submits (#603)
* better experience when building for all platforms

* implement auto-submits

* address PR feedback

* update CHANGELOG

* refactor build command class

* better UX when Android submission completes without error

* address PR feedback

* small ux improvement
2021-09-10 12:32:00 +02:00
Dominik Sokal acf7e55b5b [ENG-755] add --non-interactive flag for eas submit (#600)
* add --non-interactive flag for eas submit

* update CHANGELOG

* add basic tests for --non-interactive mode

* fix long-running tests
2021-09-09 17:12:40 +02:00
Wojciech KozyraandDominik Sokal 7c29aa3b38 [eas-cli] fix google service detection on windows (#602)
* normalize path (C:/path/to/repo -> C:\path\to\repo)

* update CHANGELOG

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-09-09 15:03:00 +02:00
Dominik Sokal dee5b4cf60 mock generateRandomKeystoreAsync to make test execution faster 2021-09-09 11:19:39 +02:00
Dominik Sokal 896e14b1e4 [ENG-755] eas submit - implement --platform all (#598)
* reorder platform prompt options

* move RequestedPlatform enum

* unify selecting platforms in eas build/submit

* implement --platform all

* update CHANGELOG
2021-09-09 09:50:43 +02:00
Dominik Sokal 42c20d6ef5 update CHANGELOG 2021-09-08 10:12:38 +02:00
Dominik Sokal 0c94f80f68 v0.26.0 2021-09-08 09:50:50 +02:00
Wojciech Kozyra 95f308737e [eas-cli] support for names with unicode only characters (#596)
* [eas-cli] support for names with unicode only characters

* update CHANGELOG
2021-09-08 09:46:12 +02:00
Dominik Sokal 2a2a0392cd [ENG-755] drop support for eas submit CLI configuration params (#590)
* drop submit flags in favor of profiles

* use platform-specific mutations

* update CHANGELOG

* update asc app id log

* address PR feedback

* use release as default submit profile only when eas.json exists

* address more PR feedback

* add link to docs

* address even more PR feedback

* fix detecting release submit profile
2021-09-07 16:14:19 +02:00
Dominik Sokal bcbdae8278 upgrade axios to 0.21.4 2021-09-07 10:05:24 +02:00
Dominik Sokal 805aee67ee add eas diagnostics + github issue templates (#594)
* add 'eas diagnostics' command

* add issue templates

* update CHANGELOG
2021-09-06 16:03:51 +02:00
Dominik Sokal 0477984d7a add CODE_OF_CONDUCT.md 2021-09-06 15:58:39 +02:00
Wojciech Kozyra 4e96196e07 remove @babel/core from package.json (#593) 2021-09-06 15:46:28 +02:00
Mike Willbanks 25ee2abdaa [eas-cli] git config --get core.ignorecase exits with code 1 (#592)
When `git config --get core.ignorecase` is run and the variable is not
currently set, it will return with an exit code of 1 thus causing the
`eas build` command to stop running. To solve this, we try/catch and
then return undefined to allow for the default behavior.
2021-09-06 14:44:06 +02:00
kgc00 75b71ae457 [eas-cli] Adds Keystore Create Tracking (#579)
- Converts credentials command to an `EASCommand`
- Adds analytics for android keystore create commands
2021-09-03 09:36:22 -07:00
Dominik Sokal 7548fc35fd update CHANGELOG 2021-09-01 10:12:33 +02:00
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
154 changed files with 8760 additions and 6760 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: {
+47
View File
@@ -0,0 +1,47 @@
name: "\U0001F41B Bug Report in EAS CLI"
labels: 'bug'
description: 'You want to report a reproducible bug or regression in EAS CLI.'
body:
- type: markdown
attributes:
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
- type: markdown
attributes:
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
- type: markdown
attributes:
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
- type: markdown
attributes:
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
- type: textarea
validations:
required: true
attributes:
label: Summary
description: Describe the issue in 1 or 2 sentences
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
- type: textarea
validations:
required: true
attributes:
label: Environment
placeholder: Run `eas diagnostics` and paste the output here
- type: textarea
validations:
required: false
attributes:
label: Error output
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
- type: textarea
validations:
required: true
attributes:
label: Reproducible demo or steps to reproduce from a blank project
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
- type: markdown
attributes:
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
- type: markdown
attributes:
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
+14
View File
@@ -0,0 +1,14 @@
name: "\U0001F64B Enhancement"
labels: 'enhancement'
description: 'Want us to add something to EAS CLI?'
body:
- type: markdown
attributes:
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
- type: dropdown
validations:
required: true
attributes:
label: Do you understand that any feature requests opened in this repository will be closed?
options:
- 'Yes'
+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:
+102 -1
View File
@@ -10,10 +10,109 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 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))
- Fixed force-creation, app secret creation, and secret deletion mutations. ([#606](https://github.com/expo/eas-cli/pull/606) by [@fiberjw](https://github.com/fiberjw))
### 🧹 Chores
## [0.27.0](https://github.com/expo/eas-cli/releases/tag/v0.27.0) - 2021-09-10
### 🎉 New features
- Implement support for `--platform all` in `eas submit`. ([#598](https://github.com/expo/eas-cli/pull/598) by [@dsokal](https://github.com/dsokal))
- Implement support for `--non-interactive` in `eas submit`. ([#600](https://github.com/expo/eas-cli/pull/600) by [@dsokal](https://github.com/dsokal))
- Add auto-submit feature. Run `eas build --auto-submit` to submit automatically on build complete. ([#603](https://github.com/expo/eas-cli/pull/603) by [@dsokal](https://github.com/dsokal))
- Allow using env var for `android.serviceAccountKeyPath` in submit profiles. ([#604](https://github.com/expo/eas-cli/pull/604) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix detecting `googleServicesFile` on Windows. ([#602](https://github.com/expo/eas-cli/pull/602) by [@wkozyra95](https://github.com/wkozyra95))
## [0.26.0](https://github.com/expo/eas-cli/releases/tag/v0.26.0) - 2021-09-08
### 🛠 Breaking changes
- Require `eas submit` to be configured with eas.json submit profiles (see [https://docs.expo.dev/submit/eas-json/](https://docs.expo.dev/submit/eas-json/) for details). Drop support for CLI params. ([#590](https://github.com/expo/eas-cli/pull/590) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add `eas diagnostics` command. ([#594](https://github.com/expo/eas-cli/pull/594) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix `eas build` from throwing an exception when detecting the git `core.ignorecase` setting. ([#592](https://github.com/expo/eas-cli/pull/592) by [@mwillbanks](https://github.com/mwillbanks))
- Support app names that consist only of unicode characters. ([#596](https://github.com/expo/eas-cli/pull/596) by [@wkozyra95](https://github.com/wkozyra95))
## [0.25.0](https://github.com/expo/eas-cli/releases/tag/v0.25.0) - 2021-09-01
### 🛠 Breaking changes
- Drop support for node < 12. ([#581](https://github.com/expo/eas-cli/pull/581) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Support `--no-wait` in `eas submit`. ([#578](https://github.com/expo/eas-cli/pull/578) by [@dsokal](https://github.com/dsokal))
- Detect changes when `core.ignorecase` is set to true. ([#570](https://github.com/expo/eas-cli/pull/570) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Replace `@hapi/joi` with `joi`. Upgrade typescript to 4.4.2. Upgrade dependencies. ([#582](https://github.com/expo/eas-cli/pull/582) by [@dsokal](https://github.com/dsokal))
- Change typescript target from `ES2017` to `ES2019`. ([#584](https://github.com/expo/eas-cli/pull/584) by [@dsokal](https://github.com/dsokal))
- Use `ts-jest` instead of `babel-jest`. ([#585](https://github.com/expo/eas-cli/pull/585) by [@dsokal](https://github.com/dsokal))
## [0.24.1](https://github.com/expo/eas-cli/releases/tag/v0.24.1) - 2021-08-25
### 🐛 Bug fixes
- Fix wrong `EasJsonReader` import causing `eas submit` to fail. ([951ee](https://github.com/expo/eas-cli/commit/951eea2396591bd612aa7cb3038da35e45c33bb8) by [@dsokal](https://github.com/dsokal))
## [0.24.0](https://github.com/expo/eas-cli/releases/tag/v0.24.0) - 2021-08-25
### 🛠 Breaking changes
- Allow publishing of multiple update groups. ([#566](https://github.com/expo/eas-cli/pull/566) by [@jkhales](https://github.com/jkhales))
### 🎉 New features
- Retry status code 500-600 server errors from Apple servers with cookies auth. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
- Throw internal server errors when Apple servers fail to authenticate. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
- Add --auto flag for eas branch:publish. ([#549](https://github.com/expo/eas-cli/pull/549) by [@jkhales](https://github.com/jkhales))
- Add submissions link in output of `eas submit`. ([#553](https://github.com/expo/eas-cli/pull/553) by [@ajsmth](https://github.com/ajsmth))
- Add submit profiles. ([#555](https://github.com/expo/eas-cli/pull/555) by [@wkozyra95](https://github.com/wkozyra95))
- Add `changesNotSentForReview` option to android submissions. ([#560](https://github.com/expo/eas-cli/pull/560) by [@wkozyra95](https://github.com/wkozyra95))
- Support `--json` flag in build commands. ([#567](https://github.com/expo/eas-cli/pull/567) by [@wkozyra95](https://github.com/wkozyra95))
- Add link to https://expo.fyi/eas-build-archive in `eas build` output to make it easier to understand what is going on in the archive/upload phase of the build. ([#562](https://github.com/expo/eas-cli/pull/562) by [@brentvatne](https://github.com/brentvatne))
- Update QR code URL for Android internal distribution build and generate smaller QR code. ([#573](https://github.com/expo/eas-cli/pull/573) by [@axeldelafosse](https://github.com/axeldelafosse))
### 🐛 Bug fixes
- Fix `--sku` flag being ignored when running `eas submit -p ios`. ([#559](https://github.com/expo/eas-cli/pull/559) by [@barthap](https://github.com/barthap))
- Set correct distribution for simulator builds. ([#556](https://github.com/expo/eas-cli/pull/556) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add warnings around common issues (`NODE_ENV=production`, git ignored `googleServicesFile`). ([#569](https://github.com/expo/eas-cli/pull/569) by [@wkozyra95](https://github.com/wkozyra95))
## [0.23.0](https://github.com/expo/eas-cli/releases/tag/v0.23.0) - 2021-08-05
### 🛠 Breaking changes
- Stop including deprecated storageBucket field when publishing. ([#547](https://github.com/expo/eas-cli/pull/547) by [@jkhales](https://github.com/jkhales))
### 🐛 Bug fixes
- Pass through metadata context to app version resolver. ([#550](https://github.com/expo/eas-cli/pull/550) by [@brentvatne](https://github.com/brentvatne))
## [0.22.4](https://github.com/expo/eas-cli/releases/tag/v0.22.4) - 2021-08-02
### 🐛 Bug fixes
- Fix build profile schema validation when `simulator` key is present for iOS. ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
### 🛠 Breaking changes
@@ -30,11 +129,13 @@ This is the log of notable changes to EAS CLI and related packages.
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
- Fix link to build details page where previously we used `project.name` instead of `project.slug` - leave out project segment entirely and depend on redirect. ([#554](https://github.com/expo/eas-cli/pull/554) by [@brentvatne](https://github.com/brentvatne))
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
### 🎉 New features
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
+7
View File
@@ -0,0 +1,7 @@
# Community Code of Conduct
The Expo team is committed to fostering an open and welcoming environment.
**Our Community Guidelines can be found here:**
https://expo.dev/guidelines
-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.3"
"version": "0.27.1"
}
+14 -8
View File
@@ -10,19 +10,25 @@
"start": "lerna run watch --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts",
"lint": "eslint . --ext .ts --ext .js",
"release": "lerna version",
"test": "jest"
},
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
"@types/jest": "^26.0.13",
"eslint": "7.31.0",
"@types/jest": "27.0.1",
"@types/node": "16.7.8",
"eslint": "7.32.0",
"eslint-config-universe": "7.0.1",
"eslint-plugin-graphql": "4.0.0",
"jest": "^26.4.2",
"jest-watch-typeahead": "^0.6.1",
"lerna": "^3.22.1",
"prettier": "2.3.2"
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.0",
"prettier": "2.3.2",
"ts-jest": "27.0.5",
"typescript": "4.4.2"
},
"volta": {
"node": "16.8.0",
"yarn": "1.22.11"
}
}
+88 -99
View File
@@ -47,13 +47,14 @@ eas --help COMMAND
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
* [`eas submit`](#eas-submit)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
@@ -72,7 +73,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
@@ -86,7 +87,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
@@ -100,22 +101,22 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
View or change analytics settings
view or change analytics settings
```
USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
Start a build
start a build
```
USAGE
@@ -123,15 +124,28 @@ USAGE
OPTIONS
-p, --platform=(android|ios|all)
--clear-cache Clear cache before the build
--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
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
--auto-submit Submit on build complete using the submit profile with the same name as the
build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
--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_NAME [default: release] Name of the build profile from eas.json
--skip-project-configuration Skip project configuration
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
@@ -142,7 +156,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
@@ -156,7 +170,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.3/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
@@ -174,14 +188,22 @@ OPTIONS
--channel=channel
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--json Enable JSON output, non-JSON messages will be printed
to stderr
--limit=limit
--platform=(all|android|ios)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
@@ -190,13 +212,16 @@ 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.3/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
Show the eas.json config
show the eas.json config
```
USAGE
@@ -207,18 +232,18 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
Manage your credentials
manage your credentials
```
USAGE
$ eas credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
@@ -229,7 +254,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
@@ -243,7 +268,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
@@ -254,7 +279,18 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
log environment info to the console
```
USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
@@ -271,7 +307,7 @@ OPTIONS
--all see all commands in CLI
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
## `eas project:info`
@@ -282,7 +318,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
@@ -296,7 +332,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
@@ -313,7 +349,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
@@ -330,7 +366,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.3/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
@@ -341,87 +377,40 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit --platform=(android|ios)`
## `eas submit`
Submits build artifact to app store
submit build archive to app store
```
USAGE
$ eas submit --platform=(android|ios)
$ eas submit
OPTIONS
-p, --platform=(android|ios) For which platform you want to submit a build
-p, --platform=(android|ios|all)
--id=id ID of the build to submit
--latest Submit the latest build for specified platform
--non-interactive Run command in non-interactive mode
--path=path Path to the .apk/.aab/.ipa file
Android specific options=android-package Android package name (default: expo.android.package
from app config)
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
eas.json.
iOS specific options=app-name The name of your app as it will appear on the App
Store (default: expo.name from app config)
--url=url App archive url
iOS specific options=apple-id Your Apple ID username (you can also set
EXPO_APPLE_ID env variable)
--verbose Always print logs from Submission Service
iOS specific options=apple-team-id Your Apple Developer Team ID
--[no-]wait Wait for submission to complete
iOS specific options=asc-app-id App Store Connect unique application Apple ID
number. Providing this param results in skipping
app creation step. Learn more:
https://expo.fyi/asc-app-id
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
expo.ios.bundleIdentifier from app config)
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
iOS specific options=language [default: en-US] Primary language (e.g. English,
German, ...)
--latest Submit the latest build for specified platform
--path=path Path to the .apk/.aab file
Android specific options=(completed|draft|halted|inProgress) [default: completed] Release status (used when
uploading new APKs/AABs)
iOS specific options=sku An unique ID for your app that is not visible on
the App Store, will be generated unless provided
Android specific options=(production|beta|alpha|internal|rollout) [default: internal] The track of the application to
use
Android specific options=(apk|aab) Android archive type
--url=url App archive url
--verbose Always print logs from Submission Service
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
EXAMPLES
$ eas submit --platform=ios
- Fully interactive iOS submission
$ eas submit --platform=android
- Fully interactive Android submission
$ eas submit -p android --latest --key=/path/to/google-services.json
- Minimal non-interactive Android submission, however it can ask you for other params if not specified
$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
and provide its App ID
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
@@ -440,7 +429,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
@@ -454,7 +443,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.3/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
@@ -468,7 +457,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.3/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
@@ -488,7 +477,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
@@ -502,5 +491,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.3/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+3
View File
@@ -6,4 +6,7 @@ if (process.env.TMPDIR) {
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
// fs-extra@10 is not compatible with memfs, the following line fixes tests
(fs.realpath as any).native = jest.fn();
module.exports = fs;
+2455 -702
View File
File diff suppressed because it is too large. Load diff
+44 -51
View File
@@ -1,8 +1,8 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.22.3",
"author": "Expo <support@expo.io>",
"version": "0.27.1",
"author": "Expo <support@expo.dev>",
"bin": {
"eas": "./bin/run"
},
@@ -10,100 +10,95 @@
"dependencies": {
"@amplitude/identify": "1.7.0",
"@amplitude/node": "1.7.0",
"@expo/apple-utils": "0.0.0-alpha.24",
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/config": "3.3.42",
"@expo/config-plugins": "3.0.3",
"@expo/eas-build-job": "0.2.45",
"@expo/eas-json": "^0.22.3",
"@expo/config-plugins": "3.1.0",
"@expo/eas-build-job": "0.2.46",
"@expo/eas-json": "^0.27.0",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
"@expo/plist": "0.0.14",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.0.7",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/spawn-async": "1.5.0",
"@hapi/joi": "17.1.1",
"@oclif/command": "1.8.0",
"@oclif/config": "1.17.0",
"@oclif/errors": "1.3.3",
"@oclif/plugin-help": "3.2.0",
"@urql/core": "2.0.0",
"@urql/exchange-retry": "0.2.0",
"chalk": "4.1.0",
"@oclif/errors": "1.3.5",
"@oclif/plugin-help": "3.2.3",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"chalk": "4.1.2",
"cli-table3": "0.6.0",
"dateformat": "3.0.3",
"dateformat": "4.5.1",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-glob": "3.2.7",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "9.0.1",
"fs-extra": "10.0.0",
"getenv": "1.0.0",
"got": "11.5.2",
"graphql": "15.3.0",
"graphql-tag": "2.11.0",
"got": "11.8.2",
"graphql": "15.5.1",
"graphql-tag": "2.12.5",
"ignore": "5.1.8",
"indent-string": "4.0.0",
"jks-js": "1.0.0",
"jks-js": "1.0.1",
"joi": "17.4.2",
"keychain": "1.3.0",
"lodash": "4.17.20",
"mime": "2.4.7",
"lodash": "4.17.21",
"mime": "2.5.2",
"minimatch": "3.0.4",
"nanoid": "3.1.23",
"nanoid": "3.1.25",
"node-fetch": "2.6.1",
"node-forge": "0.10.0",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"progress": "2.0.3",
"prompts": "2.3.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.4",
"semver": "7.3.5",
"strip-ansi": "6.0.0",
"tar": "6.0.5",
"tar": "6.1.11",
"tempy": "0.7.0",
"terminal-link": "2.1.1",
"timeago.js": "4.0.2",
"tslib": "2.3.0",
"turndown": "7.0.0",
"tslib": "2.3.1",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.0",
"uuid": "8.3.2",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@expo/babel-preset-cli": "^0.2.17",
"@graphql-codegen/cli": "^1.19.2",
"@graphql-codegen/introspection": "^1.18.0",
"@graphql-codegen/typescript": "^1.17.11",
"@graphql-codegen/typescript-operations": "^1.17.10",
"@types/dateformat": "^3.0.1",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/form-data": "^2.5.0",
"@types/fs-extra": "^9.0.1",
"@types/fs-extra": "9.0.12",
"@types/getenv": "^1.0.0",
"@types/lodash": "^4.14.161",
"@types/mime": "^2.0.3",
"@types/node": "^12",
"@types/lodash": "4.14.172",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "^0.9.5",
"@types/progress": "^2.0.3",
"@types/prompts": "^2.0.9",
"@types/semver": "^7.3.4",
"@types/tar": "^4.0.3",
"@types/semver": "7.3.8",
"@types/tar": "4.0.5",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "^8.3.0",
"@types/wrap-ansi": "^3.0.0",
"axios": "0.21.1",
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"@types/uuid": "8.3.1",
"@types/wrap-ansi": "3.0.0",
"axios": "0.21.4",
"eslint-plugin-graphql": "4.0.0",
"memfs": "3.2.2",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"typescript": "4.3.5"
"nock": "^13.0.5"
},
"engines": {
"node": ">=8.0.0"
"node": ">=12.0.0"
},
"files": [
"/bin",
@@ -183,7 +178,5 @@
"test": "jest",
"version": "yarn oclif-dev readme && node patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
},
"types": "build/index.d.ts",
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
}
}
+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`;
}
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -21,8 +21,8 @@ import {
} from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
@@ -48,6 +48,9 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
@@ -1,4 +1,4 @@
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
@@ -7,7 +7,6 @@ import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
projectArchive: ArchiveSource;
+10 -7
View File
@@ -1,4 +1,4 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -8,18 +8,18 @@ import ora from 'ora';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import Log, { learnMore } from '../log';
import { requestedPlatformDisplayNames } from '../platform';
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 { MetadataContext, collectMetadata } from './metadata';
import { Platform, TrackingContext } from './types';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { commitPromptAsync, makeProjectTarballAsync } from './utils/repository';
@@ -102,7 +102,7 @@ export async function prepareBuildRequestForPlatformAsync<
} else {
try {
return await sendBuildRequestAsync(builder, job, metadata);
} catch (error) {
} catch (error: any) {
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
throw new Error('Build request failed.');
@@ -159,7 +159,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size
)})`,
completedMessage: `Uploaded to EAS`,
completedMessage: (duration: string) =>
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`,
})
);
return bucketKey;
@@ -218,7 +221,7 @@ async function withAnalyticsAsync<Result>(
const result = await fn();
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result;
} catch (error) {
} catch (error: any) {
Analytics.logEvent(analytics.failureEvent, {
...analytics.trackingCtx,
reason: error.message,
+29 -19
View File
@@ -1,11 +1,12 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log, { learnMore } from '../log';
import { RequestedPlatform } from '../platform';
import { resolveWorkflowAsync } from '../project/workflow';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
@@ -13,7 +14,6 @@ import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
import { ConfigureContext } from './context';
import { configureIosAsync } from './ios/configure';
import { RequestedPlatform } from './types';
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
const configureCommitMessage = {
@@ -46,7 +46,7 @@ export async function ensureProjectConfiguredAsync(
} else {
error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
'https://docs.expo.dev/build/eas-json'
)})`,
{ exit: 1 }
);
@@ -95,25 +95,35 @@ export async function configureAsync(options: {
}
}
const MANAGED_DEFAULTS = {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
build: {
release: {},
development: {
developmentClient: true,
distribution: 'internal',
},
},
submit: {
release: {},
},
};
const GENERIC_DEFAULTS = {
release: {},
development: {
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
const EAS_JSON_GENERIC_DEFAULT: EasJson = {
build: {
release: {},
development: {
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
},
},
submit: {
release: {},
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
@@ -129,8 +139,8 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? { build: GENERIC_DEFAULTS }
: { build: MANAGED_DEFAULTS };
? EAS_JSON_GENERIC_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcs.trackFileAsync(easJsonPath);
-18
View File
@@ -1,18 +0,0 @@
import { AppPlatform } from '../graphql/generated';
import { RequestedPlatform } from './types';
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
[RequestedPlatform.Ios]: 'iOS',
[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]: '🤖',
};
+3 -2
View File
@@ -1,17 +1,18 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { RequestedPlatform } from '../platform';
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';
import { ensureLoggedInAsync } from '../user/actions';
import { Platform, RequestedPlatform, TrackingContext } from './types';
import { TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface ConfigureContext {
+5 -2
View File
@@ -1,5 +1,5 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
@@ -10,7 +10,7 @@ import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { IosMetadataContext } from '../metadata';
import { Platform } from '../types';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
@@ -25,6 +25,9 @@ export async function prepareIosBuildAsync(
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
@@ -1,3 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { createCredentialsContextAsync } from '../../credentials/context';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
@@ -5,7 +7,6 @@ import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/ent
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
export async function ensureIosCredentialsAsync(
+2 -3
View File
@@ -1,4 +1,4 @@
import { ArchiveSource, Ios, Job, sanitizeJob } from '@expo/eas-build-job';
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import path from 'path';
import semver from 'semver';
@@ -7,7 +7,6 @@ import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import vcs from '../../vcs';
import { BuildContext } from '../context';
import { Platform } from '../types';
interface JobData {
projectArchive: ArchiveSource;
@@ -40,7 +39,7 @@ export async function prepareJobAsync(
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.scheme ? 'simulator' : ctx.buildProfile.distribution,
distribution: ctx.buildProfile.simulator ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
node: ctx.buildProfile.node,
+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(
+7 -4
View File
@@ -1,4 +1,4 @@
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
@@ -18,7 +18,6 @@ import {
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from './ios/UpdatesModule';
import { readBuildNumberAsync, readShortVersionAsync } from './ios/version';
import { Platform } from './types';
import { isExpoUpdatesInstalled } from './utils/updates';
/**
@@ -41,9 +40,13 @@ export async function collectMetadata<T extends Platform>(
platformContext?: MetadataContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appVersion: await resolveAppVersionAsync(ctx, platformContext),
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
cliVersion: packageJSON.version,
workflow: ctx.workflow,
@@ -51,7 +54,7 @@ export async function collectMetadata<T extends Platform>(
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
distribution,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx),
buildProfile: ctx.buildProfileName,
-8
View File
@@ -1,11 +1,3 @@
export { Platform } from '@expo/eas-build-job';
export enum RequestedPlatform {
Android = 'android',
Ios = 'ios',
All = 'all',
}
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
@@ -18,6 +18,8 @@ export enum Event {
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export default {
@@ -3,7 +3,7 @@ import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import Log from '../../log';
import { requestedPlatformDisplayNames } from '../constants';
import { requestedPlatformDisplayNames } from '../../platform';
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
@@ -5,8 +5,8 @@ import {
BuildFragment,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { appPlatformDisplayNames } from '../../platform';
import formatFields from '../../utils/formatFields';
import { appPlatformDisplayNames } from '../constants';
import { getBuildLogsUrl } from './url';
export function formatGraphQLBuild(build: BuildFragment) {
@@ -4,6 +4,7 @@ import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
import {
AppPlatform,
BuildError,
BuildFragment,
BuildStatus,
@@ -12,7 +13,8 @@ import {
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(builds: BuildFragment[]): void {
@@ -28,14 +30,18 @@ export function printLogsUrls(builds: BuildFragment[]): void {
}
}
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
if (json) {
printJsonOnlyOutput(builds);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
}
@@ -64,8 +70,13 @@ function printBuildResult(build: BuildFragment): void {
if (build.distribution === DistributionType.Internal) {
const logsUrl = getBuildLogsUrl(build);
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
// It's tricky to install the .apk file directly on Android so let's fallback
// to the build details page and let people press the button to download there
const qrcodeUrl =
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
Log.log(`${indentString(code, 2)}\n`)
);
Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform]
+5 -3
View File
@@ -3,12 +3,14 @@ import assert from 'assert';
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
}
export function getBuildLogsUrl(build: BuildFragment): string {
const { project } = build;
if (project.__typename === 'App') {
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/projects/${
project.name
}/builds/${build.id}`;
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
} else {
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
}
+52
View File
@@ -0,0 +1,52 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../log';
import vcs from '../vcs';
import { BuildContext } from './context';
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 = path.normalize(await vcs.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await vcs.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
);
Log.warn(
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
)}`
);
Log.warn(
'If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting GOOGLE_SERVICES_FILE in your build profile in eas.json to any non-falsy value.'
);
Log.newLine();
}
}
function isInsideDirectory(file: string, directory: string): boolean {
return file.startsWith(directory);
}
@@ -5,35 +5,42 @@ import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand';
import TestEasCommand from './TestEasCommand';
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
};
});
beforeEach(() => {
(getUserAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(ensureLoggedInAsync as jest.Mock).mockReset().mockImplementation(() => mockJester);
(initAsync as jest.Mock).mockReset();
(flushAsync as jest.Mock).mockReset();
(logEvent as jest.Mock).mockReset();
});
describe(EasCommand.name, () => {
beforeAll(() => {
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User', () => ({ getUserAsync: jest.fn(() => mockJester) }));
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../analytics', () => {
const { AnalyticsEvent } = jest.requireActual('../../analytics');
return {
AnalyticsEvent,
logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
};
});
});
afterEach(() => {
(ensureLoggedInAsync as jest.Mock).mockClear();
(initAsync as jest.Mock).mockClear();
(flushAsync as jest.Mock).mockClear();
(logEvent as jest.Mock).mockClear();
});
describe('without exceptions', () => {
// The first test in this suite should have an increased timeout
// because of the implementation of Command from @oclif/command.
// It seems that loading config takes significant amount of time
// and I'm not sure how to mock it.
//
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user is logged in', async () => {
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveReturnedWith(mockJester);
});
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
it('ensures the user data is read from cache', async () => {
(TestEasCommand.prototype.authValue as jest.Mock).mockImplementationOnce(() => false);
+1 -1
View File
@@ -4,7 +4,7 @@ import Log from '../log';
import UserSettings from '../user/UserSettings';
export default class AnalyticsView extends Command {
static description = 'View or change analytics settings';
static description = 'view or change analytics settings';
static args = [{ name: 'STATUS', options: ['on', 'off'] }];
+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,
@@ -2,8 +2,8 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import { configureAsync } from '../../build/configure';
import { RequestedPlatform } from '../../build/types';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -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',
})}`);
}
+184 -60
View File
@@ -1,4 +1,4 @@
import { Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
@@ -6,26 +6,41 @@ import {
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error, exit } from '@oclif/errors';
import { error } from '@oclif/errors';
import chalk from 'chalk';
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 { BuildFragment, BuildStatus, SubmissionFragment } from '../../graphql/generated';
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
selectRequestedPlatformAsync,
toPlatforms,
} from '../../platform';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import { confirmAsync } from '../../prompts';
import { createSubmissionContext } from '../../submissions/context';
import {
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../../submissions/submit';
import { printSubmissionDetailsUrls } from '../../submissions/utils/urls';
import { enableJsonOutput } from '../../utils/json';
import vcs from '../../vcs';
interface RawBuildFlags {
@@ -37,6 +52,9 @@ interface RawBuildFlags {
local: boolean;
wait: boolean;
'clear-cache': boolean;
json: boolean;
'auto-submit': boolean;
'auto-submit-with-profile'?: string;
}
interface BuildFlags {
@@ -47,17 +65,27 @@ interface BuildFlags {
local: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
}
export default class Build extends EasCommand {
static description = 'Start a build';
static description = 'start a build';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
platform: flags.enum({
char: 'p',
options: ['android', 'ios', 'all'],
}),
'skip-credentials-check': flags.boolean({
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',
@@ -65,10 +93,11 @@ export default class Build extends EasCommand {
profile: flags.string({
default: 'release',
description: 'Name of the build profile from eas.json',
helpValue: 'PROFILE_NAME',
}),
'non-interactive': flags.boolean({
default: false,
description: 'Run command in --non-interactive mode',
description: 'Run command in non-interactive mode',
}),
local: flags.boolean({
default: false,
@@ -83,10 +112,26 @@ export default class Build extends EasCommand {
default: false,
description: 'Clear cache before the build',
}),
'auto-submit': flags.boolean({
default: false,
description:
'Submit on build complete using the submit profile with the same name as the build profile',
exclusive: ['auto-submit-with-profile'],
}),
'auto-submit-with-profile': flags.string({
description: 'Submit on build complete using the submit profile with provided name',
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
};
private metroConfigValidated = false;
async run(): Promise<void> {
const { flags: rawFlags } = this.parse(Build);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
@@ -98,33 +143,16 @@ export default class Build extends EasCommand {
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
const platforms = toPlatforms(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,
for (const platform of platforms) {
const maybeBuild = await this.prepareAndStartBuildAsync({
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
platform,
flags,
moreBuilds: platforms.length > 1,
});
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);
}
@@ -137,22 +165,57 @@ export default class Build extends EasCommand {
printLogsUrls(startedBuilds);
Log.newLine();
if (flags.wait) {
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds);
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
for (const build of startedBuilds) {
const submission = await this.prepareAndStartSubmissionAsync({
projectDir,
build,
flags,
moreBuilds: startedBuilds.length > 1,
});
submissions.push(submission);
}
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
if (!flags.wait) {
return;
}
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds, flags.json);
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
throw new Error('--platform is required when building in non-interactive mode');
error('--platform is required when building in non-interactive mode', { exit: 1 });
}
if (flags.json && !nonInteractive) {
error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
const requestedPlatform =
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
if (flags.local) {
if (flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined) {
// TODO: implement this
error('Auto-submits are not yet supported when building locally', { exit: 1 });
}
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) {
@@ -168,39 +231,65 @@ export default class Build extends EasCommand {
Log.newLine();
}
const profile = flags['profile'];
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile: flags['profile'],
profile,
nonInteractive,
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'],
submitProfile: flags['auto-submit-with-profile'] ?? profile,
};
}
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 },
],
private async prepareAndStartBuildAsync({
projectDir,
platform,
flags,
moreBuilds,
}: {
projectDir: string;
platform: Platform;
flags: BuildFlags;
moreBuilds: boolean;
}): Promise<BuildFragment | undefined> {
const easJsonReader = new EasJsonReader(projectDir);
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
const buildCtx = await createBuildContextAsync({
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile,
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
return platform;
}
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];
if (moreBuilds) {
Log.newLine();
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} build`
)}`
);
}
if (buildCtx.workflow === Workflow.MANAGED && !this.metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
this.metroConfigValidated = true;
}
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
return await this.startBuildAsync(buildCtx);
}
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
@@ -213,12 +302,47 @@ export default class Build extends EasCommand {
return await sendBuildRequestAsync();
}
private async prepareAndStartSubmissionAsync({
projectDir,
build,
flags,
moreBuilds,
}: {
projectDir: string;
build: BuildFragment;
flags: BuildFlags;
moreBuilds: boolean;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = createSubmissionContext({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
});
if (moreBuilds) {
Log.newLine();
Log.log(
`${appPlatformEmojis[build.platform]} ${chalk.bold(
`${appPlatformDisplayNames[build.platform]} submission`
)}`
);
}
return await submitAsync(submissionCtx);
}
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);
process.exit(1);
}
}
}
@@ -232,7 +356,7 @@ export async function handleDeprecatedEasJsonAsync(
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.io/build/eas-json'
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.dev/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
@@ -254,7 +378,7 @@ export async function handleDeprecatedEasJsonAsync(
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.io/build/eas-json and run 'eas build' again"
"Aborting, update your eas.json according to https://docs.expo.dev/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
+18 -5
View File
@@ -3,7 +3,7 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
import { BuildDistributionType, BuildStatus } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import {
AppPlatform,
@@ -12,11 +12,13 @@ import {
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildList extends Command {
static description = 'list all builds for your project';
@@ -25,6 +27,9 @@ export default class BuildList extends Command {
platform: flags.enum({
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
}),
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
status: flags.enum({
options: [
BuildStatus.NEW,
@@ -56,11 +61,15 @@ export default class BuildList extends Command {
async run() {
const { flags } = this.parse(BuildList);
const {
json,
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = flags;
if (json) {
enableJsonOutput();
}
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
@@ -100,11 +109,15 @@ export default class BuildList extends Command {
spinner.succeed(`Showing last ${builds.length} builds for the project ${projectName}`);
}
const list = builds
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
if (json) {
printJsonOnlyOutput(builds);
} else {
const list = builds
.map(build => formatGraphQLBuild(build))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
Log.log(`\n${list}`);
}
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
+20 -3
View File
@@ -1,5 +1,5 @@
import { getConfig } from '@expo/config';
import { Command } from '@oclif/command';
import { Command, flags } from '@oclif/command';
import ora from 'ora';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
@@ -11,14 +11,27 @@ import {
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildView extends Command {
static description = 'view a build for your project';
static args = [{ name: 'BUILD_ID' }];
static flags = {
json: flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
}),
};
async run() {
const { BUILD_ID: buildId } = this.parse(BuildView).args;
const {
args: { BUILD_ID: buildId },
flags,
} = this.parse(BuildView);
if (flags.json) {
enableJsonOutput();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -47,7 +60,11 @@ export default class BuildView extends Command {
spinner.succeed(`Showing the last build for the project ${projectName}`);
}
Log.log(`\n${formatGraphQLBuild(build)}`);
if (flags.json) {
printJsonOnlyOutput(build);
} else {
Log.log(`\n${formatGraphQLBuild(build)}`);
}
} catch (err) {
if (buildId) {
spinner.fail(`Something went wrong and we couldn't fetch the build with id ${buildId}`);
+2 -3
View File
@@ -8,10 +8,9 @@ import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export { Platform } from '@expo/eas-build-job';
export default class Config extends Command {
static description = 'Show the eas.json config';
static description = 'show the eas.json config';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
@@ -53,7 +52,7 @@ export default class Config extends Command {
},
]));
const profile = await reader.readBuildProfileAsync(profileName, platform);
const profile = await reader.readBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
+3 -4
View File
@@ -1,10 +1,9 @@
import { Command } from '@oclif/command';
import EasCommand from '../commandUtils/EasCommand';
import { createCredentialsContextAsync } from '../credentials/context';
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
export default class Credentials extends Command {
static description = 'Manage your credentials';
export default class Credentials extends EasCommand {
static description = 'manage your credentials';
async run() {
const ctx = await createCredentialsContextAsync(process.cwd(), {});
@@ -0,0 +1,34 @@
import { Command } from '@oclif/command';
import envinfo from 'envinfo';
import Log from '../log';
const packageJSON = require('../../package.json');
export default class Diagnostics extends Command {
static description = 'log environment info to the console';
async run() {
const info = await envinfo.run(
{
System: ['OS', 'Shell'],
Binaries: ['Node', 'Yarn', 'npm'],
Utilities: ['Git'],
npmPackages: [
'expo',
'react',
'react-dom',
'react-native',
'react-native-web',
'react-navigation',
'@expo/webpack-config',
],
npmGlobalPackages: ['eas-cli'],
},
{
title: `EAS CLI ${packageJSON.version} environment info`,
}
);
Log.log(info);
}
}
@@ -149,6 +149,7 @@ export default class EnvironmentSecretCreate extends Command {
);
} else if (scope === EnvironmentSecretScope.ACCOUNT) {
const ownerAccount = findAccountByName(actor.accounts, accountName);
if (!ownerAccount) {
Log.warn(
`Your account (${getActorDisplayName(actor)}) doesn't have access to the ${chalk.bold(
@@ -159,11 +160,12 @@ export default class EnvironmentSecretCreate extends Command {
}
if (force) {
const existingSecrets = await EnvironmentSecretsQuery.byAcccountNameAsync(projectId);
const existingSecrets = await EnvironmentSecretsQuery.byAccountNameAsync(ownerAccount.name);
const existingSecret = existingSecrets.find(secret => secret.name === name);
if (existingSecret) {
await EnvironmentSecretMutation.delete(existingSecret.id);
Log.withTick(
`Deleting existing secret ${chalk.bold(name)} on account ${chalk.bold(
ownerAccount.name
+119 -173
View File
@@ -1,168 +1,97 @@
import { getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand';
import { AppPlatform } from '../graphql/generated';
import { learnMore } from '../log';
import { SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
selectRequestedPlatformAsync,
toPlatforms,
} 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 { SubmitArchiveFlags, createSubmissionContext } from '../submissions/context';
import { submitAsync, waitToCompleteAsync } from '../submissions/submit';
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
const COMMON_FLAGS = '';
const ANDROID_FLAGS = 'Android specific options';
const IOS_FLAGS = 'iOS specific options';
interface RawFlags {
platform?: string;
profile?: string;
latest?: boolean;
id?: string;
path?: string;
url?: string;
verbose: boolean;
wait: boolean;
'non-interactive': boolean;
}
export default class BuildSubmit extends EasCommand {
static description = 'Submits build artifact to app store';
static usage = 'submit --platform=(android|ios)';
interface Flags {
requestedPlatform: RequestedPlatform;
profile?: string;
archiveFlags: SubmitArchiveFlags;
verbose: boolean;
wait: boolean;
nonInteractive: boolean;
}
export default class Submit extends EasCommand {
static description = `submit build archive to app store
See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/submit/eas-json/', {
learnMoreMessage: '',
})}`;
static aliases = ['build:submit'];
static examples = [
`$ eas submit --platform=ios
- Fully interactive iOS submission\n`,
`$ eas submit --platform=android
- Fully interactive Android submission\n`,
`$ eas submit -p android --latest --key=/path/to/google-services.json
- Minimal non-interactive Android submission, however it can ask you for other params if not specified\n`,
`$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
and provide its App ID`,
];
static flags = {
platform: flags.enum({
char: 'p',
description: 'For which platform you want to submit a build',
options: ['android', 'ios'],
helpLabel: COMMON_FLAGS,
options: ['android', 'ios', 'all'],
}),
profile: flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.',
}),
/* Common flags for both platforms */
latest: flags.boolean({
description: 'Submit the latest build for specified platform',
exclusive: ['id', 'path', 'url'],
default: false,
helpLabel: COMMON_FLAGS,
}),
id: flags.string({
description: 'ID of the build to submit',
exclusive: ['latest, path, url'],
helpLabel: COMMON_FLAGS,
}),
path: flags.string({
description: 'Path to the .apk/.aab file',
description: 'Path to the .apk/.aab/.ipa file',
exclusive: ['latest', 'id', 'url'],
helpLabel: COMMON_FLAGS,
}),
url: flags.string({
description: 'App archive url',
exclusive: ['latest', 'id', 'path'],
helpLabel: COMMON_FLAGS,
}),
verbose: flags.boolean({
description: 'Always print logs from Submission Service',
default: false,
helpLabel: COMMON_FLAGS,
}),
/* Android specific flags */
type: flags.enum<'apk' | 'aab'>({
description: 'Android archive type',
options: ['apk', 'aab'],
helpLabel: ANDROID_FLAGS,
wait: flags.boolean({
description: 'Wait for submission to complete',
default: true,
allowNo: true,
}),
key: flags.string({
description: 'Path to the JSON key used to authenticate with Google Play',
helpLabel: ANDROID_FLAGS,
}),
'android-package': flags.string({
description: 'Android package name (default: expo.android.package from app config)',
helpLabel: ANDROID_FLAGS,
}),
track: flags.enum({
description: 'The track of the application to use',
default: 'internal',
options: ['production', 'beta', 'alpha', 'internal', 'rollout'],
helpLabel: ANDROID_FLAGS,
}),
'release-status': flags.enum({
description: 'Release status (used when uploading new APKs/AABs)',
default: 'completed',
options: ['completed', 'draft', 'halted', 'inProgress'],
helpLabel: ANDROID_FLAGS,
}),
/* iOS specific flags */
'apple-id': flags.string({
description: 'Your Apple ID username (you can also set EXPO_APPLE_ID env variable)',
helpLabel: IOS_FLAGS,
}),
'asc-app-id': flags.string({
description: `App Store Connect unique application Apple ID number. Providing this param results in skipping app creation step. ${learnMore(
'https://expo.fyi/asc-app-id'
)}`,
helpLabel: IOS_FLAGS,
}),
'apple-team-id': flags.string({
description: 'Your Apple Developer Team ID',
helpLabel: IOS_FLAGS,
}),
'app-name': flags.string({
description:
'The name of your app as it will appear on the App Store (default: expo.name from app config)',
helpLabel: IOS_FLAGS,
}),
'bundle-identifier': flags.string({
description:
'Your iOS Bundle Identifier (default: expo.ios.bundleIdentifier from app config)',
helpLabel: IOS_FLAGS,
}),
sku: flags.string({
description:
'An unique ID for your app that is not visible on the App Store, will be generated unless provided',
helpLabel: IOS_FLAGS,
}),
language: flags.string({
description: 'Primary language (e.g. English, German, ...)',
default: 'en-US',
helpLabel: IOS_FLAGS,
}),
'company-name': flags.string({
description:
'The name of your company, needed only for the first upload of any app to App Store',
helpLabel: IOS_FLAGS,
'non-interactive': flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
async run(): Promise<void> {
const {
flags: {
// android
'android-package': androidPackage,
'release-status': releaseStatus,
// ios
'apple-id': appleId,
'asc-app-id': ascAppId,
'apple-team-id': appleTeamId,
'app-name': appName,
'bundle-identifier': bundleIdentifier,
'company-name': companyName,
// common
...flags
},
} = this.parse(BuildSubmit);
const platform =
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
(await promptForPlatformAsync());
const { flags: rawFlags } = this.parse(Submit);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -174,56 +103,73 @@ export default class BuildSubmit extends EasCommand {
return;
}
// TODO: Make this work outside project dir
if (!projectDir) {
throw new Error("Please run this command inside your project's directory");
const easJsonReader = new EasJsonReader(projectDir);
const platforms = toPlatforms(flags.requestedPlatform);
const submissions: SubmissionFragment[] = [];
for (const platform of platforms) {
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
const ctx = createSubmissionContext({
platform,
projectDir,
projectId,
profile,
archiveFlags: flags.archiveFlags,
nonInteractive: flags.nonInteractive,
});
if (platforms.length > 1) {
Log.newLine();
const appPlatform = toAppPlatform(platform);
Log.log(
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
`${appPlatformDisplayNames[appPlatform]} submission`
)}`
);
}
const submission = await submitAsync(ctx);
submissions.push(submission);
}
if (platform === AppPlatform.Android) {
const options: AndroidSubmitCommandFlags = {
androidPackage,
releaseStatus,
...flags,
};
Log.newLine();
printSubmissionDetailsUrls(submissions);
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,
...flags,
};
const ctx = IosSubmitCommand.createContext(projectDir, projectId, options);
const command = new IosSubmitCommand(ctx);
await command.runAsync();
} else {
throw new Error(`Unsupported platform: ${platform}!`);
if (flags.wait) {
await waitToCompleteAsync(submissions, { verbose: flags.verbose });
}
}
}
async function promptForPlatformAsync(): Promise<AppPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Submit to platform',
name: 'platform',
choices: [
{
title: 'iOS',
value: AppPlatform.Ios,
},
{
title: 'Android',
value: AppPlatform.Android,
},
],
});
return platform;
private async sanitizeFlagsAsync(flags: RawFlags): Promise<Flags> {
const {
platform,
verbose,
wait,
profile,
'non-interactive': nonInteractive,
...archiveFlags
} = flags;
if (!flags.platform && nonInteractive) {
error('--platform is required when building in non-interactive mode', { exit: 1 });
}
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
if (requestedPlatform === RequestedPlatform.All) {
if (archiveFlags.id || archiveFlags.path || archiveFlags.url) {
error(
'--id, --path, and --url params are only supported when performing a single-platform submit',
{ exit: 1 }
);
}
}
return {
archiveFlags,
requestedPlatform,
verbose,
wait,
profile,
nonInteractive,
};
}
}
@@ -15,6 +15,8 @@ import {
testPKCS12KeystoreEmptyPasswordBase64,
} from './fixtures-base64-data';
const now = new Date();
export const testKeystore: Keystore = {
keystore: testKeystoreBase64,
keystorePassword: 'ae6777e9444a436dbe533d2be46c83ba',
@@ -61,8 +63,8 @@ export const testLegacyAndroidFcmFragment: AndroidFcmFragment = {
},
credential: 'abcdxxxxxxefgh',
version: AndroidFcmVersion.Legacy,
createdAt: new Date(),
updatedAt: new Date(),
createdAt: now,
updatedAt: now,
};
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
@@ -75,8 +77,8 @@ export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
md5CertificateFingerprint: 'test-md5',
sha1CertificateFingerprint: 'test-sha1',
sha256CertificateFingerprint: 'test-sha256',
createdAt: new Date(),
updatedAt: new Date(),
createdAt: now,
updatedAt: now,
};
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
@@ -13,6 +13,8 @@ import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Cr
import { Target } from '../ios/types';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
const now = new Date();
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
@@ -39,7 +41,7 @@ export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
appleTeam: { ...testAppleTeamFragment },
updatedAt: new Date(),
updatedAt: now,
iosAppCredentialsList: [],
};
@@ -47,10 +49,10 @@ export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier:
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
expiration: new Date(),
expiration: now,
developerPortalIdentifier: 'test-developer-identifier',
provisioningProfile: testProvisioningProfileBase64,
updatedAt: new Date(),
updatedAt: now,
status: 'Active',
appleTeam: { ...testAppleTeamFragment },
appleDevices: [],
@@ -62,9 +64,9 @@ export const testDistCertFragmentNoDependencies: AppleDistributionCertificateFra
certificatePassword: 'test-password',
serialNumber: 'test-serial',
developerPortalIdentifier: 'test-developer-identifier',
validityNotAfter: new Date(),
validityNotBefore: new Date(),
updatedAt: new Date(),
validityNotAfter: now,
validityNotBefore: now,
updatedAt: now,
appleTeam: { ...testAppleTeamFragment },
iosAppBuildCredentialsList: [],
};
@@ -75,9 +77,9 @@ export const testDistCertFragmentOneDependency: AppleDistributionCertificateFrag
certificatePassword: 'test-password',
serialNumber: 'test-serial',
developerPortalIdentifier: 'test-developer-identifier',
validityNotAfter: new Date(),
validityNotBefore: new Date(),
updatedAt: new Date(),
validityNotAfter: now,
validityNotBefore: now,
updatedAt: now,
appleTeam: { ...testAppleTeamFragment },
iosAppBuildCredentialsList: [
{
@@ -1,5 +1,6 @@
import { AndroidKeystoreFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { Account } from '../../../user/Account';
import { Context } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
@@ -14,19 +15,21 @@ export class CreateKeystore {
if (ctx.nonInteractive) {
throw new Error(`New keystore cannot be created in non-interactive mode.`);
}
const keystore = await this.provideOrGenerateAsync();
const projectId = await getProjectIdAsync(ctx.exp);
const keystore = await this.provideOrGenerateAsync(projectId);
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
Log.succeed('Created keystore');
return keystoreFragment;
}
private async provideOrGenerateAsync(): Promise<KeystoreWithType> {
private async provideOrGenerateAsync(projectId: string): Promise<KeystoreWithType> {
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
if (providedKeystore) {
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
validateKeystore(providedKeystoreWithType);
return providedKeystoreWithType;
}
return await generateRandomKeystoreAsync();
return await generateRandomKeystoreAsync(projectId);
}
}
@@ -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,16 +1,22 @@
import { confirmAsync } from '../../../../prompts';
import { asMock } from '../../../../__tests__/utils';
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../../project/ensureProjectExists');
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../../utils/promptForCredentials');
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('CreateKeystore', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
@@ -18,21 +24,22 @@ describe('CreateKeystore', () => {
await createKeystoreAction.runAsync(ctx);
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toHaveBeenCalled();
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
});
it('creates a keystore by uploading', async () => {
(askForUserProvidedAsync as jest.Mock).mockImplementation(() => testKeystore);
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
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(
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledWith(
appLookupParams.account,
expect.objectContaining(testKeystore)
);
expect(generateRandomKeystoreAsync).not.toHaveBeenCalled();
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,18 +1,26 @@
import { confirmAsync, promptAsync } from '../../../../prompts';
import { asMock } from '../../../../__tests__/utils';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
testJksAndroidKeystoreFragment,
} from '../../../__tests__/fixtures-android';
import { jester as mockJester } from '../../../__tests__/fixtures-constants';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../../project/ensureProjectExists');
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('SetupBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
it('skips setup when there are prior credentials', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -28,10 +36,9 @@ describe('SetupBuildCredentials', () => {
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore not to be created
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
expect(ctx.android.createKeystoreAsync).not.toHaveBeenCalled();
});
it('sets up credentials when there are no prior credentials', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -44,10 +51,10 @@ describe('SetupBuildCredentials', () => {
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync).toHaveBeenCalled();
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
});
it('errors in Non-Interactive Mode', async () => {
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: true,
android: {
@@ -1,4 +1,2 @@
export const keytoolCommandExistsAsync = jest.fn();
export const exportCertificateAsync = jest.fn();
export const logKeystoreHashesAsync = jest.fn();
export const generateRandomKeystoreAsync = jest.fn();
@@ -1,7 +1,10 @@
import mockdate from 'mockdate';
import Log from '../../../../log';
import { testAndroidAppCredentialsFragment } from '../../../__tests__/fixtures-android';
import {
testAndroidAppCredentialsFragment,
testLegacyAndroidFcmFragment,
} from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../../actions/BuildCredentialsUtils';
import { displayAndroidAppCredentials } from '../printCredentials';
@@ -9,7 +12,7 @@ import { displayAndroidAppCredentials } from '../printCredentials';
jest.mock('../../../../log');
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
mockdate.set(new Date('4/20/2021'));
mockdate.set(testLegacyAndroidFcmFragment.updatedAt);
describe('print credentials', () => {
it('prints the AndroidAppCredentials fragment', async () => {
@@ -2,13 +2,13 @@ import spawnAsync from '@expo/spawn-async';
import crypto from 'crypto';
import fs from 'fs-extra';
import path from 'path';
import tempy from 'tempy';
import { v4 as uuidv4 } from 'uuid';
import Analytics, { Event } from '../../../build/utils/analytics';
import { AndroidKeystoreType } from '../../../graphql/generated';
import Log from '../../../log';
import { getTmpDirectory } from '../../../utils/paths';
import { Keystore, KeystoreWithType } from '../credentials';
import { KeystoreWithType } from '../credentials';
export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
@@ -28,63 +28,47 @@ async function ensureKeytoolCommandExistsAsync(): Promise<void> {
}
}
async function writeFileToTmpAsync(base64Data: string, nameSuffix: string = ''): Promise<string> {
await fs.mkdirp(getTmpDirectory());
const filePath = path.join(getTmpDirectory(), `${uuidv4()}${nameSuffix}`);
await fs.writeFile(filePath, base64Data, 'base64');
return filePath;
enum KeystoreCreateStep {
Attempt = 'attempt',
Fail = 'fail',
Success = 'success',
}
export async function exportCertificateAsync(
keystore: Keystore,
options?: { rfcFormat: boolean }
): Promise<string> {
await ensureKeytoolCommandExistsAsync();
const keystorePath = await writeFileToTmpAsync(keystore.keystore, '-keystore.jks');
const certPath = path.join(getTmpDirectory(), `${uuidv4()}.cer`);
export async function generateRandomKeystoreAsync(projectId: string): Promise<KeystoreWithType> {
const keystoreData = {
keystorePassword: crypto.randomBytes(16).toString('hex'),
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
};
return await createKeystoreAsync(keystoreData, projectId);
}
async function createKeystoreAsync(
credentials: {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
},
projectId: string
): Promise<KeystoreWithType> {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Attempt,
type: AndroidKeystoreType.Jks,
});
try {
await spawnAsync('keytool', [
'-exportcert',
...(options?.rfcFormat ? ['-rfc'] : []),
'-keystore',
keystorePath,
'-storepass',
keystore.keystorePassword,
'-alias',
keystore.keyAlias,
'-file',
certPath,
'-noprompt',
'-storetype',
'JKS',
]);
return await fs.readFile(certPath, 'base64');
} finally {
await fs.remove(keystorePath);
await fs.remove(certPath);
await ensureKeytoolCommandExistsAsync();
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
type: AndroidKeystoreType.Jks,
});
throw error;
}
}
export async function logKeystoreHashesAsync(keystore: Keystore, linePrefix: string = '') {
const base64Cert = await exportCertificateAsync(keystore);
const data = Buffer.from(base64Cert, 'base64');
const googleHash = crypto.createHash('sha1').update(data).digest('hex').toUpperCase();
const googleHash256 = crypto.createHash('sha256').update(data).digest('hex').toUpperCase();
const fbHash = crypto.createHash('sha1').update(data).digest('base64');
Log.log(
`${linePrefix}Google Certificate Fingerprint: ${googleHash.replace(/(.{2}(?!$))/g, '$1:')}`
);
Log.log(`${linePrefix}Google Certificate Hash (SHA-1): ${googleHash}`);
Log.log(`${linePrefix}Google Certificate Hash (SHA-256): ${googleHash256}`);
Log.log(`${linePrefix}Facebook Key Hash: ${fbHash}`);
}
async function createKeystoreAsync(credentials: {
keystorePassword: string;
keyAlias: string;
keyPassword: string;
}): Promise<KeystoreWithType> {
await ensureKeytoolCommandExistsAsync();
await fs.mkdirp(getTmpDirectory());
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
try {
@@ -110,42 +94,27 @@ async function createKeystoreAsync(credentials: {
'-dname',
`CN=,OU=,O=,L=,S=,C=US`,
]);
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Success,
type: AndroidKeystoreType.Jks,
});
return {
...credentials,
keystore: await fs.readFile(keystorePath, 'base64'),
type: AndroidKeystoreType.Jks,
};
} catch (error: any) {
Analytics.logEvent(Event.ANDROID_KEYSTORE_CREATE, {
project_id: projectId,
step: KeystoreCreateStep.Fail,
reason: error.message,
type: AndroidKeystoreType.Jks,
});
throw error;
} finally {
await fs.remove(keystorePath);
}
}
export async function generateRandomKeystoreAsync(): Promise<KeystoreWithType> {
const keystoreData = {
keystorePassword: crypto.randomBytes(16).toString('hex'),
keyPassword: crypto.randomBytes(16).toString('hex'),
keyAlias: crypto.randomBytes(16).toString('hex'),
};
return await createKeystoreAsync(keystoreData);
}
export async function validateKeystoreAsync(keystore: Keystore): Promise<void> {
await ensureKeytoolCommandExistsAsync();
try {
await tempy.write.task(Buffer.from(keystore.keystore, 'base64'), async keystorePath => {
await spawnAsync('keytool', [
'-list',
'-keystore',
keystorePath,
'-storepass',
keystore.keystorePassword,
'-alias',
keystore.keyAlias,
]);
});
} catch (e) {
throw new Error(
`An error occurred when validating the Android keystore: ${e.stdout ?? e.message}`
);
}
}
@@ -65,7 +65,7 @@ function getPemFromJksKeystore(keystore: Keystore): string {
const { protectedPrivateKey } = keystoreEntry;
try {
return jks.decrypt(protectedPrivateKey, keystore.keyPassword);
} catch (e) {
} catch (e: any) {
throw new Error(`${e.message}: Check that your key password is correct`);
}
}
@@ -79,7 +79,7 @@ function getX509Asn1FromPKCS12Keystore(
try {
const p12 = parsePKCS12(p12BufferOrBase64String, maybePassword);
x509Asn1 = getX509Asn1ByFriendlyName(p12, keyAlias);
} catch (e) {
} catch (e: any) {
throw new Error(`Invalid PKCS#12 (.p12) keystore: ${e.message}`);
}
if (!x509Asn1) {
@@ -22,15 +22,15 @@ import { updateAndroidCredentialsAsync, updateIosCredentialsAsync } from '../upd
jest.mock('fs');
jest.mock('prompts');
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
describe('update credentials.json', () => {
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
describe(updateAndroidCredentialsAsync, () => {
it('should update keystore in credentials.json if www returns valid credentials', async () => {
const ctx = createCtxMock();
@@ -102,7 +102,7 @@ describe('update credentials.json', () => {
delete buildCredentialsNoKeystore.androidKeystore;
await updateAndroidCredentialsAsync(ctx, buildCredentialsNoKeystore);
throw new Error('updateAndroidCredentialsAsync should throw an error');
} catch (e) {
} catch (e: any) {
expect(e.message).toMatch(
'There are no credentials configured for this project on EAS servers'
);
@@ -278,7 +278,7 @@ describe('update credentials.json', () => {
try {
await updateIosCredentialsAsync(ctx, app, targets, IosDistributionType.AppStore);
throw new Error('updateIosCredentialsAsync should throw na error');
} catch (e) {
} catch (e: any) {
expect(e.message).toMatch('There are no credentials configured');
}
const certP12 = await fs.readFile('./cert.p12', 'base64');
@@ -1,4 +1,4 @@
import Joi from '@hapi/joi';
import Joi from 'joi';
import { Keystore } from '../android/credentials';
@@ -87,7 +87,7 @@ export default class IosCredentialsProvider {
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.succeed(
`Push Notifications setup for ${app.projectName}:${applicationTarget.bundleIdentifier}`
`Push Notifications setup for ${app.projectName}: ${applicationTarget.bundleIdentifier}`
);
return null;
}
@@ -221,7 +221,7 @@ async function generateDistributionCertificateAsync(
`✅ Distribution Certificates can be revoked with no side effects for App Store builds.`
)
);
Log.log(learnMore('https://docs.expo.io/distribution/app-signing/#summary'));
Log.log(learnMore('https://docs.expo.dev/distribution/app-signing/#summary'));
Log.newLine();
const { distCertsToRevoke } = await promptAsync({
@@ -67,7 +67,7 @@ async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<
await ctx.appStore.ensureAuthenticatedAsync();
try {
return await ctx.appStore.createPushKeyAsync();
} catch (e) {
} catch (e: any) {
if (e.message === APPLE_KEYS_TOO_MANY_GENERATED_ERROR) {
const pushKeys = await ctx.appStore.listPushKeysAsync();
Log.warn('Maximum number of Push Notifications Keys generated on Apple Developer Portal.');
@@ -80,7 +80,7 @@ async function generatePushKeyAsync(ctx: Context, accountName: string): Promise<
}
Log.log(chalk.grey(`⚠️ Revoking a Push Key will affect other apps that rely on it`));
Log.log(learnMore('https://docs.expo.io/distribution/app-signing/#push-notification-keys'));
Log.log(learnMore('https://docs.expo.dev/distribution/app-signing/#push-notification-keys'));
Log.log();
const { pushKeysToRevoke } = await promptAsync({
@@ -5,6 +5,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreatePushKey } from '../CreatePushKey';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
@@ -3,7 +3,7 @@
exports[`select credentials select an AppleDistributionCertificate fragment 1`] = `
"Cert ID: 5URSS525PL, Serial number: 3F9CDD6CFC6E52C1, Team ID: 77KQ969CHE
Created: 6 months ago, Updated: 10 months ago,
Expires: Wed, 13 Oct 2021 18:28:44 GMT+0000Cert ID: JRH7292L8R, Serial number: 5544BE191B9F949E, Team ID: 77KQ969CHE
Expires: Wed, 13 Oct 2021 18:28:44 UTCCert ID: JRH7292L8R, Serial number: 5544BE191B9F949E, Team ID: 77KQ969CHE
Created: 10 months ago, Updated: 10 months ago,
Expires: Tue, 22 Jun 2021 20:46:07 GMT+0000"
Expires: Tue, 22 Jun 2021 20:46:07 UTC"
`;
@@ -32,7 +32,7 @@ const IosAppCredentialsQuery = {
},
appSpecificPassword: {
id: '64bdd007-da7b-4190-a571-edb87e9324a1',
appleIdUsername: 'quin@expo.io',
appleIdUsername: 'quin@expo.dev',
passwordLabel: 'description for super secret password',
updatedAt: '2020-10-13T18:39:04.463Z',
__typename: 'AppleAppSpecificPassword',
@@ -178,7 +178,7 @@ export async function authenticateAsync(options: Options = {}): Promise<AuthCtx>
// Defined for legacy usage in Turtle V1 or any other places where Fastlane is used in the servers.
fastlaneSession,
};
} catch (error) {
} catch (error: any) {
if (error.message === 'ABORTED') {
process.exit(1);
}
@@ -25,7 +25,7 @@ export async function getProfilesForBundleIdAsync(
try {
await profile.getBundleIdAsync();
validProfileIds.add(profile.id);
} catch (e) {
} catch (e: any) {
if (
e.name === 'UnexpectedAppleResponse' &&
e.message.includes('The specified resource does not exist - There is no resource of type')
@@ -79,7 +79,7 @@ export async function syncCapabilityIdentifiersForEntitlementsAsync(
remoteIdModel = await CapabilityModel.createAsync(bundleId.context, {
identifier: localId,
});
} catch (error) {
} catch (error: any) {
// Add a more helpful error message.
error.message += `\n\nRemove the value '${localId}' from the array '${classifier.entitlement}' in the iOS project entitlements.\nIf you know that the ID is registered to one of your apps, try again with a different Apple account.`;
throw error;
@@ -11,7 +11,7 @@ async function getContractStatusAsync(
try {
const capabilities = await ITCAgreements.getCapabilitiesAsync(context);
return capabilities?.contractStatus ?? null;
} catch (error) {
} catch (error: any) {
Log.warn(`Failed to get iTunes contract status: ${error.message}`);
return null;
}
@@ -20,7 +20,7 @@ async function getContractStatusAsync(
async function getContractMessagesAsync(context: RequestContext) {
try {
return await ITCAgreements.getContractMessagesAsync(context);
} catch (error) {
} catch (error: any) {
Log.warn(`Failed to get iTunes contract messages: ${error.message}`);
return null;
}
@@ -103,7 +103,7 @@ export async function createDistributionCertificateAsync(
teamId: authCtx.team.id,
teamName: authCtx.team.name,
};
} catch (error) {
} catch (error: any) {
spinner.fail('Failed to create Apple distribution certificate');
// TODO: Move check into apple-utils
if (
@@ -56,7 +56,7 @@ export async function ensureBundleIdExistsWithNameAsync(
});
}
spinner.succeed(`Bundle identifier registered ${chalk.dim(bundleIdentifier)}`);
} catch (err) {
} catch (err: any) {
if (err.message.match(/An App ID with Identifier '(.*)' is not available/)) {
spinner.fail(
`The bundle identifier ${chalk.bold(bundleIdentifier)} is not available to team "${
@@ -152,7 +152,14 @@ export async function ensureAppExistsAsync(
language,
companyName,
bundleIdentifier,
}: { name: string; language?: string; companyName?: string; bundleIdentifier: string }
sku,
}: {
name: string;
language?: string;
companyName?: string;
bundleIdentifier: string;
sku?: string;
}
) {
const context = getRequestContext(authCtx);
const spinner = ora(`Linking to App Store ${chalk.dim(bundleIdentifier)}`).start();
@@ -170,8 +177,9 @@ export async function ensureAppExistsAsync(
name,
primaryLocale: language,
companyName,
sku,
});
} catch (error) {
} catch (error: any) {
if (error.message.match(/An App ID with Identifier '(.*)' is not available/)) {
const providerName = authCtx.authState?.session.provider.name;
throw new Error(
@@ -44,7 +44,7 @@ export async function createPushKeyAsync(
teamId: authCtx.team.id,
teamName: authCtx.team.name,
};
} catch (err) {
} catch (err: any) {
spinner.fail('Failed to create Apple push key');
const resultString = err.rawDump?.resultString;
if (
@@ -81,11 +81,11 @@ export async function promptPasswordAsync({
if (cachedPassword) {
Log.log(`\u203A Using password for ${username} from your local Keychain`);
Log.log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
Log.log(` ${learnMore('https://docs.expo.dev/distribution/security#keychain')}`);
return cachedPassword;
}
// https://docs.expo.io/distribution/security/#apple-developer-account-credentials
// https://docs.expo.dev/distribution/security/#apple-developer-account-credentials
Log.log(
wrapAnsi(
chalk.bold(
@@ -160,7 +160,7 @@ async function cachePasswordAsync({ username, password }: Auth.UserCredentials):
}
Log.log(`\u203A Saving Apple ID password to the local Keychain`);
Log.log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
Log.log(` ${learnMore('https://docs.expo.dev/distribution/security#keychain')}`);
const serviceName = getKeychainServiceName(username);
return Keychain.setPasswordAsync({ username, password, serviceName });
}
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`print credentials prints the IosAppCredentials map 1`] = `"iOS Credentials Project: @quinlanj/test52 Bundle Identifier: com.quinlanj.test52 Apple Team: test-apple-identifier Push Key: Developer Portal ID: 9839M6AY8W Apple Team: 77KQ969CHE Updated 6 months ago App Specific Password: Apple ID Username: quin@expo.io Password Label: description for super secret password Updated 6 months ago Configuration: App Store Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 5 months ago Configuration: Ad Hoc Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 GMT+0000 Apple Team: test-apple-identifier Updated 5 months ago"`;
exports[`print credentials prints the IosAppCredentials map 1`] = `"iOS Credentials Project: @quinlanj/test52 Bundle Identifier: com.quinlanj.test52 Apple Team: test-apple-identifier Push Key: Developer Portal ID: 9839M6AY8W Apple Team: 77KQ969CHE Updated 6 months ago App Specific Password: Apple ID Username: quin@expo.dev Password Label: description for super secret password Updated 6 months ago Configuration: App Store Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 5 months ago Configuration: Ad Hoc Distribution Certificate: Serial Number: test-serial Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 6 months ago Provisioning Profile: Developer Portal ID: test-profile-id Status: active Expiration Date: Wed, 13 Oct 2021 18:28:44 UTC Apple Team: test-apple-identifier Updated 5 months ago"`;
@@ -47,7 +47,7 @@ function getRawCertData(p12Buffer: Buffer | string, passwordRaw: string | null):
} else {
p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1);
}
} catch (_error) {
} catch (_error: any) {
const error: Error = _error;
if (/Invalid password/.exec(error.message)) {
throw new Error(
@@ -55,7 +55,7 @@ function validateProvisioningProfileWithoutApple(
nullthrows(distributionCertificate?.certificateP12),
nullthrows(distributionCertificate?.certificatePassword)
);
} catch (e) {
} catch (e: any) {
Log.warn(`Failed to calculate fingerprint for Distribution Certificate: ${e.toString()}`);
return false;
}
@@ -14,7 +14,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
async runAsync(ctx: Context): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(profileName, this.platform);
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(this.platform, profileName);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
@@ -23,7 +23,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
const buildProfileNames = await this.easJsonReader.getBuildProfileNamesAsync();
if (buildProfileNames.length === 0) {
throw new Error(
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.io/build/eas-json/ for more details'
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details'
);
} else if (buildProfileNames.length === 1) {
return buildProfileNames[0];
@@ -14,7 +14,9 @@ export async function runRegistrationUrlMethodAsync(
): Promise<void> {
const registrationURL = await generateDeviceRegistrationURLAsync(accountId, appleTeam);
Log.newLine();
qrcodeTerminal.generate(registrationURL, code => Log.log(`${indentString(code, 2)}\n`));
qrcodeTerminal.generate(registrationURL, { small: true }, code =>
Log.log(`${indentString(code, 2)}\n`)
);
Log.log(
'Open the following link on your iOS devices (or scan the QR code) and follow the instructions to install the development profile:'
);
+136 -35
View File
@@ -438,6 +438,7 @@ export type App = Project & {
submissions: Array<Submission>;
/** Deployments associated with this app */
deployments: Array<Deployment>;
deployment?: Maybe<Deployment>;
/** iOS app credentials for the project */
iosAppCredentials: Array<IosAppCredentials>;
/** Android app credentials for the project */
@@ -484,6 +485,7 @@ export type App = Project & {
users?: Maybe<Array<Maybe<User>>>;
/** @deprecated Field no longer supported */
releases: Array<Maybe<AppRelease>>;
latestReleaseForReleaseChannel?: Maybe<AppRelease>;
};
@@ -523,8 +525,16 @@ export type AppSubmissionsArgs = {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: Scalars['Int'];
buildListMaxSize: Scalars['Int'];
mostRecentlyUpdatedAt?: Maybe<Scalars['DateTime']>;
options?: Maybe<DeploymentOptions>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentArgs = {
channel: Scalars['String'];
runtimeVersion: Scalars['String'];
options?: Maybe<DeploymentOptions>;
};
@@ -600,6 +610,13 @@ export type AppReleasesArgs = {
limit?: Maybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppLatestReleaseForReleaseChannelArgs = {
platform: AppPlatform;
releaseChannel: Scalars['String'];
};
export type AppIcon = {
__typename?: 'AppIcon';
url: Scalars['String'];
@@ -884,7 +901,7 @@ export type Submission = ActivityTimelineProjectActivity & {
id: Scalars['ID'];
actor?: Maybe<Actor>;
activityTimestamp: Scalars['DateTime'];
app?: Maybe<App>;
app: App;
initiatingActor?: Maybe<Actor>;
submittedBuild?: Maybe<Build>;
platform: AppPlatform;
@@ -898,16 +915,18 @@ export type Submission = ActivityTimelineProjectActivity & {
};
export enum SubmissionStatus {
AwaitingBuild = 'AWAITING_BUILD',
InQueue = 'IN_QUEUE',
InProgress = 'IN_PROGRESS',
Finished = 'FINISHED',
Errored = 'ERRORED'
Errored = 'ERRORED',
Canceled = 'CANCELED'
}
export type AndroidSubmissionConfig = {
__typename?: 'AndroidSubmissionConfig';
applicationIdentifier: Scalars['String'];
archiveType: SubmissionAndroidArchiveType;
archiveType?: Maybe<SubmissionAndroidArchiveType>;
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
};
@@ -969,14 +988,15 @@ export type BuildJob = ActivityTimelineProjectActivity & BuildOrBuildJob & {
export type AppRelease = {
__typename?: 'AppRelease';
id: Scalars['ID'];
hash?: Maybe<Scalars['String']>;
publishedTime?: Maybe<Scalars['DateTime']>;
publishingUsername?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
s3Key?: Maybe<Scalars['String']>;
s3Url?: Maybe<Scalars['String']>;
manifest?: Maybe<Scalars['JSON']>;
hash: Scalars['String'];
publishedTime: Scalars['DateTime'];
publishingUsername: Scalars['String'];
sdkVersion: Scalars['String'];
runtimeVersion?: Maybe<Scalars['String']>;
version: Scalars['String'];
s3Key: Scalars['String'];
s3Url: Scalars['String'];
manifest: Scalars['JSON'];
};
export type BuildArtifact = {
@@ -1010,14 +1030,20 @@ export type SubmissionFilter = {
status?: Maybe<SubmissionStatus>;
};
export type DeploymentOptions = {
/** Max number of associated builds to return */
buildListMaxSize?: Maybe<Scalars['Int']>;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type Deployment = {
__typename?: 'Deployment';
id?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
recentBuilds?: Maybe<Array<Maybe<Build>>>;
branchToReceive?: Maybe<UpdateBranch>;
id: Scalars['ID'];
runtimeVersion: Scalars['String'];
channel: Scalars['String'];
recentBuilds: Array<Build>;
branchMapping?: Maybe<UpdateBranch>;
mostRecentlyUpdatedAt: Scalars['DateTime'];
};
export type UpdateBranch = {
@@ -1028,7 +1054,6 @@ export type UpdateBranch = {
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
updates: Array<Update>;
mostRecentUpdateForEachPlatform: Array<Update>;
};
@@ -1208,6 +1233,7 @@ export type AndroidAppCredentials = {
app: App;
applicationIdentifier?: Maybe<Scalars['String']>;
androidFcm?: Maybe<AndroidFcm>;
googleServiceAccountKeyForSubmissions?: Maybe<GoogleServiceAccountKey>;
androidAppBuildCredentialsList: Array<AndroidAppBuildCredentials>;
isLegacy: Scalars['Boolean'];
/** @deprecated use androidAppBuildCredentialsList instead */
@@ -1250,6 +1276,18 @@ export enum AndroidFcmVersion {
V1 = 'V1'
}
export type GoogleServiceAccountKey = {
__typename?: 'GoogleServiceAccountKey';
id: Scalars['ID'];
account: Account;
projectIdentifier: Scalars['String'];
privateKeyIdentifier: Scalars['String'];
clientEmail: Scalars['String'];
clientIdentifier: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
};
export type AndroidAppBuildCredentials = {
__typename?: 'AndroidAppBuildCredentials';
id: Scalars['ID'];
@@ -2879,10 +2917,15 @@ export type DeleteRobotResult = {
export type SubmissionMutation = {
__typename?: 'SubmissionMutation';
/** Create an EAS Submit submission */
/**
* Create an EAS Submit submission
* @deprecated Use createIosSubmission / createAndroidSubmission instead
*/
createSubmission: CreateSubmissionResult;
/** Create an iOS EAS Submit submission */
createIosSubmission: CreateSubmissionResult;
/** Create an Android EAS Submit submission */
createAndroidSubmission: CreateSubmissionResult;
};
@@ -2895,6 +2938,11 @@ export type SubmissionMutationCreateIosSubmissionArgs = {
input: CreateIosSubmissionInput;
};
export type SubmissionMutationCreateAndroidSubmissionArgs = {
input: CreateAndroidSubmissionInput;
};
export type CreateSubmissionInput = {
appId: Scalars['ID'];
platform: AppPlatform;
@@ -2910,18 +2958,35 @@ export type CreateSubmissionResult = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
config: NewIosSubmissionConfig;
config: IosSubmissionConfigInput;
submittedBuildId?: Maybe<Scalars['ID']>;
};
export type NewIosSubmissionConfig = {
export type IosSubmissionConfigInput = {
appleAppSpecificPasswordId?: Maybe<Scalars['String']>;
appleAppSpecificPassword?: Maybe<Scalars['String']>;
archiveUrl: Scalars['String'];
archiveUrl?: Maybe<Scalars['String']>;
appleIdUsername: Scalars['String'];
ascAppIdentifier: Scalars['String'];
};
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
submittedBuildId?: Maybe<Scalars['ID']>;
};
export type AndroidSubmissionConfigInput = {
googleServiceAccountKeyId?: Maybe<Scalars['String']>;
googleServiceAccountKeyJson?: Maybe<Scalars['String']>;
archiveUrl?: Maybe<Scalars['String']>;
archiveType?: Maybe<SubmissionAndroidArchiveType>;
applicationIdentifier: Scalars['String'];
track: SubmissionAndroidTrack;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
changesNotSentForReview?: Maybe<Scalars['Boolean']>;
};
export type UpdateChannelMutation = {
__typename?: 'UpdateChannelMutation';
/**
@@ -2993,7 +3058,7 @@ export type UpdateBranchMutation = {
/** Delete an EAS branch and all of its updates as long as the branch is not being used by any channels */
deleteUpdateBranch: DeleteUpdateBranchResult;
/** Publish an update group to a branch */
publishUpdateGroup: Array<Update>;
publishUpdateGroups: Array<Update>;
};
@@ -3013,8 +3078,8 @@ export type UpdateBranchMutationDeleteUpdateBranchArgs = {
};
export type UpdateBranchMutationPublishUpdateGroupArgs = {
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
export type UpdateBranchMutationPublishUpdateGroupsArgs = {
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
};
export type EditUpdateBranchInput = {
@@ -3052,7 +3117,7 @@ export type PartialManifestAsset = {
fileSHA256: Scalars['String'];
bundleKey: Scalars['String'];
contentType: Scalars['String'];
storageBucket: Scalars['String'];
fileExtension?: Maybe<Scalars['String']>;
storageKey: Scalars['String'];
};
@@ -3222,6 +3287,7 @@ export type MeMutationLeaveAccountArgs = {
export type MeMutationInitiateSecondFactorAuthenticationArgs = {
deviceConfigurations: Array<Maybe<SecondFactorDeviceConfiguration>>;
recaptchaResponseToken?: Maybe<Scalars['String']>;
};
@@ -4770,7 +4836,7 @@ export type GetSignedUploadMutation = (
);
export type UpdatePublishMutationVariables = Exact<{
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
publishUpdateGroupsInput: Array<PublishUpdateGroupInput>;
}>;
@@ -4778,30 +4844,52 @@ export type UpdatePublishMutation = (
{ __typename?: 'RootMutation' }
& { updateBranch: (
{ __typename?: 'UpdateBranchMutation' }
& { publishUpdateGroup: Array<(
& { publishUpdateGroups: Array<(
{ __typename?: 'Update' }
& Pick<Update, 'id' | 'group'>
& Pick<Update, 'id' | 'group' | 'runtimeVersion' | 'platform'>
)> }
) }
);
export type CreateSubmissionMutationVariables = Exact<{
export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
platform: AppPlatform;
config: Scalars['JSONObject'];
config: AndroidSubmissionConfigInput;
submittedBuildId?: Maybe<Scalars['ID']>;
}>;
export type CreateSubmissionMutation = (
export type CreateAndroidSubmissionMutation = (
{ __typename?: 'RootMutation' }
& { submission: (
{ __typename?: 'SubmissionMutation' }
& { createSubmission: (
& { createAndroidSubmission: (
{ __typename?: 'CreateSubmissionResult' }
& { submission: (
{ __typename?: 'Submission' }
& Pick<Submission, 'id'>
& SubmissionFragment
) }
) }
) }
);
export type CreateIosSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: IosSubmissionConfigInput;
submittedBuildId?: Maybe<Scalars['ID']>;
}>;
export type CreateIosSubmissionMutation = (
{ __typename?: 'RootMutation' }
& { submission: (
{ __typename?: 'SubmissionMutation' }
& { createIosSubmission: (
{ __typename?: 'CreateSubmissionResult' }
& { submission: (
{ __typename?: 'Submission' }
& Pick<Submission, 'id'>
& SubmissionFragment
) }
) }
) }
@@ -5135,7 +5223,20 @@ export type EnvironmentSecretFragment = (
export type SubmissionFragment = (
{ __typename?: 'Submission' }
& Pick<Submission, 'id' | 'status' | 'platform' | 'logsUrl'>
& { error?: Maybe<(
& { app: (
{ __typename?: 'App' }
& Pick<App, 'id' | 'name'>
& { ownerAccount: (
{ __typename?: 'Account' }
& Pick<Account, 'id' | 'name'>
) }
), androidConfig?: Maybe<(
{ __typename?: 'AndroidSubmissionConfig' }
& Pick<AndroidSubmissionConfig, 'applicationIdentifier' | 'track' | 'releaseStatus'>
)>, iosConfig?: Maybe<(
{ __typename?: 'IosSubmissionConfig' }
& Pick<IosSubmissionConfig, 'ascAppIdentifier' | 'appleIdUsername'>
)>, error?: Maybe<(
{ __typename?: 'SubmissionError' }
& Pick<SubmissionError, 'errorCode' | 'message'>
)> }
@@ -48,7 +48,7 @@ export const EnvironmentSecretMutation = {
): Promise<EnvironmentSecretFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<CreateEnvironmentSecretForAppMutation>(
.mutation<CreateEnvironmentSecretForAppMutation>(
gql`
mutation CreateEnvironmentSecretForApp(
$input: CreateEnvironmentSecretInput!
@@ -73,7 +73,7 @@ export const EnvironmentSecretMutation = {
async delete(id: string): Promise<{ id: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<DeleteEnvironmentSecretMutation>(
.mutation<DeleteEnvironmentSecretMutation>(
gql`
mutation DeleteEnvironmentSecret($id: String!) {
environmentSecret {
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { PublishUpdateGroupInput, Update, UpdatePublishMutation } from '../generated';
import { PublishUpdateGroupInput, UpdatePublishMutation } from '../generated';
const PublishMutation = {
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
@@ -30,27 +30,28 @@ const PublishMutation = {
},
async publishUpdateGroupAsync(
publishUpdateGroupInput: PublishUpdateGroupInput
): Promise<Pick<Update, 'group'>> {
publishUpdateGroupsInput: PublishUpdateGroupInput[]
): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdatePublishMutation>(
gql`
mutation UpdatePublishMutation($publishUpdateGroupInput: PublishUpdateGroupInput) {
mutation UpdatePublishMutation($publishUpdateGroupsInput: [PublishUpdateGroupInput!]!) {
updateBranch {
publishUpdateGroup(publishUpdateGroupInput: $publishUpdateGroupInput) {
publishUpdateGroups(publishUpdateGroupsInput: $publishUpdateGroupsInput) {
id
group
runtimeVersion
platform
}
}
}
`,
{ publishUpdateGroupInput }
{ publishUpdateGroupsInput }
)
.toPromise()
);
const { group } = data.updateBranch.publishUpdateGroup[0]!;
return { group };
return data.updateBranch.publishUpdateGroups;
},
};
@@ -1,56 +1,78 @@
import { JSONObject } from '@expo/json-file';
import { print } from 'graphql';
import gql from 'graphql-tag';
import nullthrows from 'nullthrows';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
AppPlatform,
CreateSubmissionMutation,
CreateSubmissionMutationVariables,
Submission,
CreateAndroidSubmissionInput,
CreateAndroidSubmissionMutation,
CreateAndroidSubmissionMutationVariables,
CreateIosSubmissionInput,
CreateIosSubmissionMutation,
CreateIosSubmissionMutationVariables,
SubmissionFragment,
} from '../generated';
interface CreateSubmissionResult {
submission: Pick<Submission, 'id'>;
}
import { SubmissionFragmentNode } from '../types/Submission';
export const SubmissionMutation = {
async createSubmissionAsync(input: {
appId: string;
submittedBuildId?: string;
platform: AppPlatform;
config: JSONObject;
}): Promise<CreateSubmissionResult> {
async createAndroidSubmissionAsync(
input: CreateAndroidSubmissionInput
): Promise<SubmissionFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateSubmissionMutation, CreateSubmissionMutationVariables>(
.mutation<CreateAndroidSubmissionMutation, CreateAndroidSubmissionMutationVariables>(
gql`
mutation CreateSubmissionMutation(
mutation CreateAndroidSubmissionMutation(
$appId: ID!
$platform: AppPlatform!
$config: JSONObject!
$config: AndroidSubmissionConfigInput!
$submittedBuildId: ID
) {
submission {
createSubmission(
input: {
appId: $appId
platform: $platform
config: $config
submittedBuildId: $submittedBuildId
}
createAndroidSubmission(
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
) {
submission {
id
...SubmissionFragment
}
}
}
}
${print(SubmissionFragmentNode)}
`,
input
)
.toPromise()
);
return nullthrows(data.submission?.createSubmission);
return nullthrows(data.submission.createAndroidSubmission.submission);
},
async createIosSubmissionAsync(input: CreateIosSubmissionInput): Promise<SubmissionFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateIosSubmissionMutation, CreateIosSubmissionMutationVariables>(
gql`
mutation CreateIosSubmissionMutation(
$appId: ID!
$config: IosSubmissionConfigInput!
$submittedBuildId: ID
) {
submission {
createIosSubmission(
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
) {
submission {
id
...SubmissionFragment
}
}
}
}
${print(SubmissionFragmentNode)}
`,
input
)
.toPromise()
);
return nullthrows(data.submission.createIosSubmission.submission);
},
};
@@ -19,7 +19,7 @@ export type EnvironmentSecretWithScope = EnvironmentSecretFragment & {
};
export const EnvironmentSecretsQuery = {
async byAcccountNameAsync(accountName: string): Promise<EnvironmentSecretFragment[]> {
async byAccountNameAsync(accountName: string): Promise<EnvironmentSecretFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<EnvironmentSecretsByAccountNameQuery>(
@@ -74,7 +74,7 @@ export const EnvironmentSecretsQuery = {
projectFullName: string
): Promise<EnvironmentSecretWithScope[]> {
const [accountSecrets, appSecrets] = await Promise.all([
this.byAcccountNameAsync(projectAccountName),
this.byAccountNameAsync(projectAccountName),
this.byAppIdAsync(projectFullName),
]);
@@ -0,0 +1,21 @@
import { Platform } from '@expo/eas-build-job';
import { AppPlatform } from '../generated';
export function toAppPlatform(platform: Platform): AppPlatform {
if (platform === Platform.ANDROID) {
return AppPlatform.Android;
} else if (platform === Platform.IOS) {
return AppPlatform.Ios;
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
}
export function toPlatform(appPlatform: AppPlatform): Platform {
if (appPlatform === AppPlatform.Android) {
return Platform.ANDROID;
} else {
return Platform.IOS;
}
}
@@ -5,6 +5,23 @@ export const SubmissionFragmentNode = gql`
id
status
platform
app {
id
name
ownerAccount {
id
name
}
}
androidConfig {
applicationIdentifier
track
releaseStatus
}
iosConfig {
ascAppIdentifier
appleIdUsername
}
error {
errorCode
message
+10 -4
View File
@@ -85,18 +85,24 @@ export default class Log {
/**
* Format links as dim (unless disabled) with an underline.
*
* @example Learn more: https://expo.io
* @example Learn more: https://expo.dev
* @param url
*/
export function learnMore(
url: string,
{ learnMoreMessage, dim = true }: { learnMoreMessage?: string; dim?: boolean } = {}
{
learnMoreMessage: maybeLearnMoreMessage,
dim = true,
}: { learnMoreMessage?: string; dim?: boolean } = {}
): string {
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
if (terminalLink.isSupported) {
const text = terminalLink(learnMoreMessage ?? 'Learn more.', url);
const text = terminalLink(maybeLearnMoreMessage ?? 'Learn more.', url);
return dim ? chalk.dim(text) : text;
}
const text = `${learnMoreMessage ?? 'Learn more'}: ${chalk.underline(url)}`;
const learnMoreMessage = maybeLearnMoreMessage ?? 'Learn more';
const text = `${
learnMoreMessage === '' ? learnMoreMessage : `${learnMoreMessage}: `
}${chalk.underline(url)}`;
return dim ? chalk.dim(text) : text;
}
+58
View File
@@ -0,0 +1,58 @@
import { Platform } from '@expo/eas-build-job';
import { AppPlatform } from './graphql/generated';
import { promptAsync } from './prompts';
export const appPlatformDisplayNames: Record<AppPlatform, string> = {
[AppPlatform.Android]: 'Android',
[AppPlatform.Ios]: 'iOS',
};
export const appPlatformEmojis = {
[AppPlatform.Ios]: '🍎',
[AppPlatform.Android]: '🤖',
};
// for `eas build` and `eas submit`
export enum RequestedPlatform {
Android = 'android',
Ios = 'ios',
All = 'all',
}
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
[RequestedPlatform.Android]: 'Android',
[RequestedPlatform.Ios]: 'iOS',
[RequestedPlatform.All]: 'Android and iOS',
};
export async function selectRequestedPlatformAsync(platform?: string): Promise<RequestedPlatform> {
if (
platform &&
Object.values(RequestedPlatform).includes(platform.toLowerCase() as RequestedPlatform)
) {
return platform.toLowerCase() as RequestedPlatform;
}
const { requestedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'requestedPlatform',
choices: [
{ title: 'All', value: RequestedPlatform.All },
{ title: 'Android', value: RequestedPlatform.Android },
{ title: 'iOS', value: RequestedPlatform.Ios },
],
});
return requestedPlatform;
}
export function toPlatforms(requestedPlatform: RequestedPlatform): Platform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [Platform.ANDROID, Platform.IOS];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [Platform.ANDROID];
} else {
return [Platform.IOS];
}
}
@@ -17,6 +17,7 @@ import {
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../prompts');
jest.mock('../../user/User');
jest.mock('../ensureProjectExists');
@@ -140,14 +141,6 @@ describe(getProjectAccountNameAsync, () => {
expect(projectAccountName).toBe('notnotbrent');
});
it(`throws an error if the user is not logged in`, async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => undefined);
await expect(getProjectAccountNameAsync(expWithOwner)).rejects.toThrow(
/Failed to access user data/
);
});
it(`throws when project owner is undefined for robot actors`, async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'Robot',
@@ -10,7 +10,7 @@ import { PublishQuery } from '../../graphql/queries/PublishQuery';
import {
MetadataJoi,
TIMEOUT_LIMIT,
buildUpdateInfoGroupAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssets,
convertAssetToUpdateInfoGroupFormatAsync,
filterOutAssetsThatAlreadyExistAsync,
@@ -131,23 +131,23 @@ describe(convertAssetToUpdateInfoGroupFormatAsync, () => {
it('resolves to the correct value', async () => {
const path = uuidv4();
fs.writeFileSync(path, 'I am pretending to be a jpeg');
const type = 'jpg';
const fileExtension = '.jpg';
const asset = {
type,
fileExtension,
contentType: 'image/jpeg',
path,
};
await expect(convertAssetToUpdateInfoGroupFormatAsync(asset)).resolves.toEqual({
bundleKey: 'c939e759656f577c058f445bfb19182e',
fileExtension: '.jpg',
contentType: 'image/jpeg',
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
storageBucket: 'update-assets-production',
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
});
});
});
describe(buildUpdateInfoGroupAsync, () => {
describe(buildUnsortedUpdateInfoGroupAsync, () => {
const androidBundlePath = uuidv4();
const assetPath = uuidv4();
fs.writeFileSync(androidBundlePath, 'I am a js bundle');
@@ -155,17 +155,17 @@ describe(buildUpdateInfoGroupAsync, () => {
it('returns the correct value', async () => {
await expect(
buildUpdateInfoGroupAsync(
buildUnsortedUpdateInfoGroupAsync(
{
android: {
launchAsset: {
type: 'bundle',
fileExtension: '.bundle',
contentType: 'bundle/javascript',
path: androidBundlePath,
},
assets: [
{
type: 'jpg',
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: assetPath,
},
@@ -182,17 +182,17 @@ describe(buildUpdateInfoGroupAsync, () => {
assets: [
{
bundleKey: 'c939e759656f577c058f445bfb19182e',
fileExtension: '.jpg',
contentType: 'image/jpeg',
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
storageBucket: 'update-assets-production',
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
},
],
launchAsset: {
bundleKey: 'ec0dd14670aae108f99a810df9c1482c',
fileExtension: '.bundle',
contentType: 'bundle/javascript',
fileSHA256: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
storageBucket: 'update-assets-production',
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
},
extra: {
@@ -232,7 +232,7 @@ describe(collectAssets, () => {
const userDefinedAssets = [
{
type: 'jpg',
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
},
@@ -267,7 +267,7 @@ describe(collectAssets, () => {
expect(collectAssets({ inputDir, platforms: defaultPublishPlatforms })).toEqual({
android: {
launchAsset: {
type: 'bundle',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(`${inputDir}/bundles/android.js`),
},
@@ -275,7 +275,7 @@ describe(collectAssets, () => {
},
ios: {
launchAsset: {
type: 'bundle',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(`${inputDir}/bundles/ios.js`),
},
@@ -286,7 +286,7 @@ describe(collectAssets, () => {
expect(collectAssets({ inputDir, platforms: ['ios'] })).toEqual({
ios: {
launchAsset: {
type: 'bundle',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(`${inputDir}/bundles/ios.js`),
},
@@ -1,7 +1,9 @@
import assert from 'assert';
import chalk from 'chalk';
import ora from 'ora';
import terminalLink from 'terminal-link';
import { getProjectDashboardUrl } from '../build/utils/url';
import { AppPrivacy } from '../graphql/generated';
import { AppMutation } from '../graphql/mutations/AppMutation';
import { ProjectQuery } from '../graphql/queries/ProjectQuery';
@@ -14,24 +16,33 @@ interface ProjectInfo {
privacy?: AppPrivacy;
}
const projectCache: Record<string, string> = {};
/**
* Ensures project exists on EAS servers. Registers it when it doesn't
* @returns The project ID
*/
export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promise<string> {
const { accountName, projectName } = projectInfo;
const projectFullName = `@${accountName}/${projectName}`;
if (projectFullName in projectCache) {
return projectCache[projectFullName];
}
const actor = await ensureLoggedInAsync();
const account = findAccountByName(actor.accounts, accountName);
assert(account, `You must have access to the ${accountName} account to run this command`);
const projectFullName = `@${accountName}/${projectName}`;
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
const projectLink = terminalLink(projectFullName, projectDashboardUrl);
const spinner = ora(`Linking to project ${chalk.bold(projectFullName)}`).start();
const maybeId = await findProjectIdByAccountNameAndSlugNullableAsync(accountName, projectName);
if (maybeId) {
spinner.succeed(`Linked to project ${chalk.bold(projectFullName)}`);
spinner.succeed(`Linked to project ${chalk.bold(projectLink)}`);
projectCache[projectFullName] = maybeId;
return maybeId;
}
@@ -42,7 +53,8 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
projectName,
privacy: projectInfo.privacy,
});
spinner.succeed(`Created ${chalk.bold(projectFullName)} on Expo`);
spinner.succeed(`Created ${chalk.bold(projectLink)} on Expo`);
projectCache[projectFullName] = id;
return id;
} catch (err) {
spinner.fail();
@@ -62,7 +74,7 @@ export async function findProjectIdByAccountNameAndSlugNullableAsync(
): Promise<string | null> {
try {
return await findProjectIdByAccountNameAndSlugAsync(accountName, slug);
} catch (err) {
} catch (err: any) {
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
throw err;
}
@@ -80,8 +92,8 @@ async function findProjectIdByAccountNameAndSlugAsync(
accountName: string,
slug: string
): Promise<string> {
const project = await ProjectQuery.byUsernameAndSlugAsync(accountName, slug);
return project.id;
const { id } = await ProjectQuery.byUsernameAndSlugAsync(accountName, slug);
return id;
}
/**
@@ -9,7 +9,7 @@ import once from 'lodash/once';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { getProjectConfigDescription, getUsername, sanitizedProjectName } from '../projectUtils';
import { getProjectConfigDescription, getUsername } from '../projectUtils';
import { resolveWorkflowAsync } from '../workflow';
const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
@@ -58,7 +58,7 @@ export async function getBundleIdentifierAsync(
} else {
// TODO: the following asserts are only temporary until we support app extensions in managed projects
assert(
!targetName || targetName === sanitizedProjectName(exp.name),
!targetName || targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
'targetName cannot be set to an arbitrary value for managed projects'
);
assert(!buildConfiguration, 'buildConfiguration cannot be passed for managed projects');
+1 -2
View File
@@ -7,7 +7,6 @@ import sortBy from 'lodash/sortBy';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { sanitizedProjectName } from '../projectUtils';
import { resolveWorkflowAsync } from '../workflow';
export interface XcodeBuildContext {
@@ -45,7 +44,7 @@ export async function resolveXcodeBuildContextAsync(
if (!expoName) {
throw new Error('"expo.name" is required in your app.json');
}
const sanitizedExpoName = sanitizedProjectName(expoName);
const sanitizedExpoName = IOSConfig.XcodeUtils.sanitizedName(expoName);
if (!sanitizedExpoName) {
throw new Error('"expo.name" needs to contain some alphanumeric characters');
}
@@ -14,7 +14,7 @@ export async function isEasEnabledForProjectAsync(projectId: string): Promise<bo
data: { enabled },
} = await apiClient.get(`projects/${projectId}/eas-enabled`).json();
return enabled;
} catch (error) {
} catch (error: any) {
if (error.response?.statusCode === 404) {
return true;
} else {
@@ -24,7 +24,7 @@ export async function isEasEnabledForProjectAsync(projectId: string): Promise<bo
}
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
'https://expo.io/eas'
'https://expo.dev/eas'
)}`;
export function warnEasUnavailable() {
+4 -5
View File
@@ -1,10 +1,9 @@
import { exit } from '@oclif/errors';
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import type MetroConfig from 'metro-config';
import resolveFrom from 'resolve-from';
import { BuildContext } from '../build/context';
import { Platform } from '../build/types';
import Log, { learnMore } from '../log';
import { confirmAsync } from '../prompts';
@@ -15,7 +14,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildConte
const metroConfig = await loadConfigAsync(ctx.projectDir);
const hasHashAssetFilesPlugin = metroConfig.transformer?.assetPlugins?.find((plugin: string) =>
plugin.match(/expo-asset\/tools\/hashAssetFiles/)
plugin.match(/expo-asset[/|\\]tools[/|\\]hashAssetFiles/)
);
if (!hasHashAssetFilesPlugin) {
Log.warn(
@@ -28,7 +27,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildConte
);
Log.warn(`We recommend you to abort, fix the ${chalk.bold('metro.config.js')}, and try again.`);
Log.warn(
learnMore('https://docs.expo.io/guides/customizing-metro/', {
learnMore('https://docs.expo.dev/guides/customizing-metro/', {
learnMoreMessage: 'Learn more on customizing Metro',
})
);
@@ -38,7 +37,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildConte
});
if (shouldAbort) {
Log.log('Aborting...');
exit();
process.exit(1);
}
}
}
+3 -12
View File
@@ -24,7 +24,7 @@ export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
case 'Robot':
if (!exp.owner) {
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.io/versions/latest/config/app/#owner'
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
);
}
return exp.owner;
@@ -39,7 +39,7 @@ export function getUsername(exp: ExpoConfig, user: Actor): string | undefined {
// owner field is necessary to run `expo prebuild`
if (!exp.owner) {
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.io/versions/latest/config/app/#owner'
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
);
}
// robot users don't have usernames
@@ -82,7 +82,7 @@ export async function setProjectIdAsync(
Log.warn('It looks like you are using a dynamic configuration!');
Log.log(
chalk.dim(
'https://docs.expo.io/workflow/configuration/#dynamic-configuration-with-appconfigjs)\n'
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)\n'
)
);
Log.warn(
@@ -224,15 +224,6 @@ export async function getBranchByNameAsync({
return data.app.byId.updateBranchByName;
}
// copy-pasted from expo-cli
// https://github.com/expo/expo-cli/blob/master/packages/expo-cli/src/utils/extractTemplateAppAsync.ts#L15
export function sanitizedProjectName(name: string) {
return name
.replace(/[\W_]+/g, '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
// return project id of existing/newly created project, or null if user declines
export async function promptToCreateProjectIfNotExistsAsync(
exp: ExpoConfig
+16 -16
View File
@@ -1,9 +1,9 @@
import { ExpoConfig, Platform } from '@expo/config';
import JsonFile from '@expo/json-file';
import spawnAsync from '@expo/spawn-async';
import Joi from '@hapi/joi';
import crypto from 'crypto';
import fs from 'fs';
import Joi from 'joi';
import { uniqBy } from 'lodash';
import mime from 'mime';
import path from 'path';
@@ -16,15 +16,6 @@ import Log from '../log';
import { uploadWithPresignedPostAsync } from '../uploads';
export const TIMEOUT_LIMIT = 60_000; // 1 minute
const STORAGE_BUCKET = getStorageBucket();
function getStorageBucket(): string {
if (process.env.EXPO_STAGING || process.env.EXPO_LOCAL) {
return 'update-assets-staging';
} else {
return 'update-assets-production';
}
}
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
type Metadata = {
@@ -35,7 +26,7 @@ type Metadata = {
};
};
export type RawAsset = {
type: string;
fileExtension?: string;
contentType: string;
path: string;
};
@@ -117,20 +108,24 @@ export async function convertAssetToUpdateInfoGroupFormatAsync(
asset: RawAsset
): Promise<PartialManifestAsset> {
const fileSHA256 = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
const contentType = asset.contentType;
const { contentType, fileExtension } = asset;
const storageKey = getStorageKey(contentType, fileSHA256);
const bundleKey = (await calculateFileHashAsync(asset.path, 'md5')).toString('hex');
return {
fileSHA256,
contentType,
storageBucket: STORAGE_BUCKET,
storageKey,
bundleKey,
fileExtension,
};
}
export async function buildUpdateInfoGroupAsync(
/**
* This will be sorted later based on the platform's runtime versions.
*/
export async function buildUnsortedUpdateInfoGroupAsync(
assets: CollectedAssets,
exp: ExpoConfig
): Promise<UpdateInfoGroup> {
@@ -230,13 +225,18 @@ export function collectAssets({
for (const platform of platforms) {
assetsFinal[platform] = {
launchAsset: {
type: 'bundle',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
},
assets: metadata.fileMetadata[platform].assets.map(asset => {
let fileExtension;
if (asset.ext) {
// ensure the file extension has a '.' prefix
fileExtension = asset.ext.startsWith('.') ? asset.ext : `.${asset.ext}`;
}
return {
type: asset.ext,
fileExtension,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(distRoot, asset.path),
};
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more