Compare commits

..
54 Commits
Author SHA1 Message Date
Szymon Dziedzic 1de239b6ba v3.2.0 2023-01-09 10:45:11 +01:00
Simon Blake 4515d29907 Add --json flag to webhook:list command (#1605) 2023-01-04 13:41:10 +01:00
Szymon Dziedzic 7591cac883 [ENG-7069][eas-cli] Use pagianted select prompt for eas build:run command (#1601)
* [eas-cli] Use pagianted select prompt for eas build:run command

* apply suggestions from code review

* apply improvements from code review

* update CHANGELOG.md
2023-01-04 11:32:17 +01:00
Szymon Dziedzic 9765f75a7d [eas-cli] use completedAt timestamp as build finish date instead of updatedAt in eas build:run command (#1604)
* [eas-cli] use `completedAt` timestamp as build finish date instead of `updatedAt` in `eas build:run` command

* update CHANGELOG.md
2023-01-04 11:22:10 +01:00
Szymon Dziedzic 1f1b58db82 [ENG-7070][eas-cli] fix issues with invisible build info in some terminals while using submit command (#1603)
* [eas-cli] fix issues with invisible build info in some terminals while using submit command

* update CHANGELOG.md
2023-01-04 11:18:10 +01:00
Bartosz KaszubowskiandDominik Sokal b0c88a37a9 [eas-cli] ensure that all links are clickable, unify display (#1591)
Co-authored-by: Dominik Sokal <[email protected]>
2023-01-04 11:09:35 +01:00
Jon Samp 5694bb9fdc [eas-cli] Makes eas.json configuration to only run on update:configure (#1598)
* Makes eas.json configuration to only run on update:configure

* update CHANGELOG.md
2023-01-03 22:37:49 -06:00
Szymon Dziedzic 9245657e23 [ENG-7070][eas-cli] Fix issues with invisible build info in some terminals in build:run and build:resign commands (#1602)
* [eas-cli] Fix issues with invisible build info in some terminals

* update CHANGELOG.md
2023-01-03 13:41:25 +01:00
Jon Samp 9ef8483882 [eas-cli] use appVersion instead of sdkVersion (#1588)
* use appVersion instead of sdkVersion

* update CHANGELOG.md
2022-12-29 00:00:23 -06:00
Dominik Sokal b859e11126 update CONTRIBUTING.md + add RELEASING.md (#1583)
* update CONTRIBUTING.md

* address PR feedback
2022-12-21 11:48:55 +01:00
James Ide cee499e0e6 [ui] Rename "queue" to "Free tier queue" (#1582) 2022-12-20 15:17:28 -08:00
Cedric van Putten 8702f165f3 [eas-cli] Add --channel to update republish (#1580)
* [eas-cli] Add test cases for republishing updates

* [eas-cli] Tweak the channel -> branch method errors to reuse in republish

* [eas-cli] Add support for --channel in update:republish

* [eas-cli] Add test cases to prevent using --group, --channel, or --branch together

* update CHANGELOG.md

* [eas-cli] Update the test case for resolving branch from channel
2022-12-19 22:35:14 +01:00
Cedric van Putten a6bea7e234 [eas-cli] Increase the timeout for loading the command config (#1581) 2022-12-19 21:47:13 +01:00
Expo CI 37343dd586 update CHANGELOG.md 2022-12-19 16:24:12 +00:00
Cedric van Putten 058183f202 v3.1.1 2022-12-19 16:57:33 +01:00
Cedric van Putten 7ee50de78e [eas-cli] Fix update --channel and --branch flag validation (#1579)
* [eas-cli] Add initial test cases for the update command

* [eas-cli] Add runtimeVersion option when mocking exports

* [eas-cli] Fix update flag validation for --branch and --channel

* [eas-cli] Add test case for --non-interactive, --channel, and --message

* update CHANGELOG.md

* [eas-cli] Fix linting issue

* [eas-cli] Reuse the input dir from the command

* [eas-cli] Explicitly mention the allowed flag combinations to avoid ambiguity

* [eas-cli] Move --auto to a distinct location in the error message

* [eas-cli] Fix linting issue
2022-12-19 16:36:18 +01:00
Expo CI ca11aa9e41 update CHANGELOG.md 2022-12-17 00:00:52 +00:00
Brent Vatne 64d985fcda v3.1.0 2022-12-16 15:54:02 -08:00
Jon Samp bd2bb4dcf6 [eas-cli] Add --channel flag to eas update command (#1567)
* Add --channel flag to eas update command

* use existing method

* adds undefined to return type

* move to getBranchNameFromChannelNameAsync.ts

* renames the function to singular "update"
2022-12-15 10:56:30 -06:00
Wojciech Kozyra 17db6c4cf1 [eas-cli] Add build:resign command (#1575) 2022-12-15 14:23:10 +01:00
Jon SampandDominik Sokal 5203096aca [eas-cli] Adds channels to build profiles on eas update:configure (#1570)
* Adds eas.json configuration during eas update configure

* updates next steps message

* renames eas updates to eas update

* self feedback

* update CHANGELOG.md

* Update packages/eas-cli/src/commands/update/configure.ts

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

* Update packages/eas-cli/src/commands/update/configure.ts

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

* code style feedback incorp

* use EasJsonAccessor

* Update packages/eas-cli/src/update/configure.ts

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

* lint

Co-authored-by: Dominik Sokal <[email protected]>
2022-12-12 10:59:41 -06:00
Expo CI a53e1f916f update CHANGELOG.md 2022-12-07 10:15:44 +00:00
Szymon Dziedzic 09a4b3d387 v3.0.0 2022-12-07 11:01:22 +01:00
Szymon Dziedzic 24e7de5bfb [eas-cli] Fix eas cli node:assert import error by using imports from assert instead of node:assert (#1569)
* [eas-cli] use import from 'assert' instead from 'node:assert'

* update CHANGELOG.md
2022-12-07 10:57:45 +01:00
Szymon Dziedzic 613fb5b6ea [eas-json] Make not recognized image error handler run only if image is present under valid key with invalid value (#1565)
* [eas-json] Make not recognized image error handler fire only if image is present under valid key with invalid value

* revert changes to error message

* update CHANGELOG.md
2022-12-06 14:08:03 +01:00
Cedric van Putten 4e117075fa refactor(update): remove republish from update command (#1535)
* fix(update): add code signing support to republish and block different branches

* refactor(update): move codesigning info into update fragment

* update CHANGELOG.md

* fix(update): add code signing support to republish and block different branches

* refactor(update): mark group and republish flags as deprecated

* refactor(update): drop republish and group flags

* refactor(update): drop republish flag from command

* refactor(update): remove last republish code from update command

* refactor(update): either use --group or --branch in deprecation warning

* refactor(update): drop old runtime version from update command

* refactor(update): move asset variables closer to populated area

* fix(update): fix republish typo in deprecation warning

* fix(update): correct a merge conflict error related to publish query changes
2022-12-05 13:25:52 +01:00
Cedric van Putten 33f4ae7a05 [eas-cli] Split republish command from update command (#1533)
* refactor(update): split out republish to separate command

* fix(update): add code signing support to republish and block different branches

* test(update): add missing republish command tests

* test(update): add test case without codesigning

* refactor(update): move codesigning info into update fragment

* refactor(update): make branch and group exclusive and simplify code

* update CHANGELOG.md
2022-12-05 04:08:56 -08:00
Expo CI caba28901d update CHANGELOG.md 2022-12-05 12:03:51 +00:00
Szymon Dziedzic 77e02f5759 v2.9.0 2022-12-05 12:55:30 +01:00
Szymon Dziedzic fa2717aada [ENG-6238][eas-cli] prompt developer to download the simulator app after build is finished when in interactive mode (#1554)
* [eas-cli] prompt developer to download the simulator app when in interactive mode

* check if build status is finished

* improvements after code review

* fix iOS simulator select prompt

* fix typo

* improve prompt message

* update CHANGELOG.md
2022-12-05 12:54:08 +01:00
Szymon Dziedzic 322f797cbe [ENG-7007][eas-cli] change Android start intent to MAIN instead of RUN in eas build:run (#1556)
* [eas-cli] change am start intent to MAIN instead of RUN in eas build:run command

* update CHANGELOG.md
2022-12-01 17:35:10 +01:00
Szymon Dziedzic 2a64b099ed [ENG-7006][eas-cli] Indicate in eas build:run when build artifacts has expired (#1547)
* [eas-cli] inidicate in `eas build:run` when build artifacts has expired

* minor changes after code review

* update CHANGELOG.md
2022-12-01 11:25:40 +01:00
Juwan WheatleyandDominik Sokal 81465733e8 [eas-cli] add isGitWorkingTreeDirty to EAS Update publish command (#1550)
* [eas-cli] add isGitWorkingTreeDirty to EAS Update publish command

* update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2022-11-30 15:10:46 -05:00
Szymon Dziedzic 9b22056205 [ENG-6238][eas-cli] add caching for eas build run command (#1542)
* [eas-cli] add caching for `eas build:run`

* cache in tmp directory

* use path from `path` instead of `node:path`

* don't hide eas build:run command

* apply feedback from review

* improvements after code review

* update CHANGELOG.md
2022-11-29 12:12:32 +01:00
Wojciech Kozyra 24841f82e0 [eas-cli] Consistent prompt for application identifier for both platforms (#1543) 2022-11-29 11:38:47 +01:00
Expo CI a8fae470d0 update CHANGELOG.md 2022-11-28 11:25:27 +00:00
Dominik Sokal 72283cdda2 v2.8.0 2022-11-28 12:13:38 +01:00
Dominik Sokal 9874ff26b3 upgrade eas-cli-local-build-plugin to 0.0.120 2022-11-28 12:13:16 +01:00
Dominik Sokal 85d64b7ad3 [ENG-6491] remove internal expo id from device:list (#1541)
* remove internal expo id from device:list

* update CHANGELOG.md
2022-11-28 09:34:36 +01:00
Szymon Dziedzic 2dba2705b7 [ENG-6238][eas-cli] add build:run support for Android (#1485)
* [eas-cli] add build:run support for Android

* add assertions about android executables

* use spawnAsync instead of spawn

* minor refactor

* minor improvements to prompts

* improve download function name

* refactor code after the review

* apply suggestions from the review

* fix commit message formatting

* use progress tracker instead of progress bar

* improve formatting of build choices

* fix typo in function name

* requested improvements

* test using path.join for fast-lob

* fix windows glob issues

* fix glob on windows

* allow users to run ios builds only on apple device

* use path.join instead of interpolation

* address feedback

* auto select android for non mac users

* minor improvements

* improve error messages

* update CHANGELOG.md
2022-11-25 13:57:19 +01:00
Cedric van Putten d1bec6e00a [eas-cli] Only create a channel with branch name when branch is created (#1507)
* fix(update): only create a channel with branch name when branch is created

* refactor(update): rename branchIsCreated to createdBranch

* update CHANGELOG.md
2022-11-23 18:28:08 +01:00
Szymon Dziedzic 597e012ca3 [ENG-6941][eas-json] add better error message for when invalid image is set in the eas.json (#1531)
* [eas-json] add custom error message handler for invalid image validation error

* update CHANGELOG.md
2022-11-21 15:52:30 +01:00
Wojciech Kozyra 8632b8ada7 [eas-cli] Upload project sources and submit archives to GCS (#1524) 2022-11-17 11:57:41 +01:00
Wojciech Kozyra b288879838 [eas-cli] Add SDK version to tracking context (#1529) 2022-11-17 11:34:24 +01:00
Dominik Sokal 8beb8f46da change wording: "Submission Service" -> "EAS Submit" (#1527)
* change wording: "Submission Service" -> "EAS Submit"

* update CHANGELOG.md
2022-11-17 09:53:44 +01:00
Expo CI 9120bf5777 update CHANGELOG.md 2022-11-16 14:58:36 +00:00
Szymon Dziedzic 01619d4182 v2.7.1 2022-11-16 15:49:27 +01:00
Szymon Dziedzic aa4ea4ae2a update eas-build-job version in yarn.lock 2022-11-16 15:48:54 +01:00
Szymon Dziedzic c236c2ffb5 upgrade eas-cli-local-build-plugin to 0.0.119 2022-11-16 15:45:52 +01:00
Szymon Dziedzic b5a3a513de [ENG-6892][eas-json] add Xcode 14.1 image (#1511)
* [eas-json] add Xcode 14.1 image

* bump build tools version

* update CHANGELOG.md
2022-11-16 15:30:15 +01:00
Wojciech Kozyra 6058183c45 [eas-cli] Show what prompt failed when running in non interactive shell. (#1523) 2022-11-16 09:57:22 +01:00
Wojciech Kozyra a042d27573 [eas-cli] Pass env variables when resolving config in credentials manager. (#1520) 2022-11-15 11:19:26 +01:00
Dominik Sokal 88b21e476d [scripts] run yarn install before running scripts (#1519) 2022-11-14 10:34:02 +01:00
Expo CI 3d497d4f05 update CHANGELOG.md 2022-11-11 17:26:16 +00:00
95 changed files with 5344 additions and 816 deletions

No files matched your search

+93 -1
View File
@@ -8,6 +8,98 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `--channel` option to `update:republish`. ([#1580](https://github.com/expo/eas-cli/pull/1580) by [@byCedric](https://github.com/byCedric))
- Use `appVersion` as default runtime version policy when running `eas update:configure`. ([#1588](https://github.com/expo/eas-cli/pull/1588) by [@jonsamp](https://github.com/jonsamp))
- Support `--json` flag in webhook list command. ([#1605](https://github.com/expo/eas-cli/pull/1605) by [@sheddy7](https://github.com/sheddy7))
### 🐛 Bug fixes
- Makes eas.json configuration to only run on `update:configure`. ([#1598](https://github.com/expo/eas-cli/pull/1598) by [@jonsamp](https://github.com/jonsamp))
- Fix issue with invisible build info in some terminals in the `eas build:run` and `eas build:resign` commands. ([#1602](https://github.com/expo/eas-cli/pull/1602) by [@szdziedzic](https://github.com/szdziedzic))
- Fix issues with invisible build info in some terminals while using the `eas submit` command. ([#1603](https://github.com/expo/eas-cli/pull/1603) by [@szdziedzic](https://github.com/szdziedzic))
- Use the `completedAt` timestamp as the build finish date instead of the `updatedAt` timestamp in the `eas build:run` command. ([#1604](https://github.com/expo/eas-cli/pull/1604) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Make all URLs in logs clickable in terminals supporting hyperlinks. ([#1591](https://github.com/expo/eas-cli/pull/1591) by [@Simek](https://github.com/Simek))
- Use paginated select prompt for the `eas build:run` command. ([#1601](https://github.com/expo/eas-cli/pull/1601) by [@szdziedzic](https://github.com/szdziedzic))
## [3.1.1](https://github.com/expo/eas-cli/releases/tag/v3.1.1) - 2022-12-19
### 🐛 Bug fixes
- Fix update `--channel` and `--branch` flag validation. ([#1579](https://github.com/expo/eas-cli/pull/1579) by [@byCedric](https://github.com/byCedric))
## [3.1.0](https://github.com/expo/eas-cli/releases/tag/v3.1.0) - 2022-12-17
### 🎉 New features
- Adds `--channel` flag to `eas update`. ([#1567](https://github.com/expo/eas-cli/pull/1567) by [@jonsamp](https://github.com/jonsamp))
- Add channel configurations to **eas.json** during `eas update:configure`. ([#1570](https://github.com/expo/eas-cli/pull/1570) by [@jonsamp](https://github.com/jonsamp))
- Add `build:resign` command. ([#1575](https://github.com/expo/eas-cli/pull/1575) by [@wkozyra95](https://github.com/wkozyra95))
## [3.0.0](https://github.com/expo/eas-cli/releases/tag/v3.0.0) - 2022-12-07
### 🛠 Breaking changes
- Move update republish to separate command. ([#1533](https://github.com/expo/eas-cli/pull/1533) by [@byCedric](https://github.com/byCedric))
### 🐛 Bug fixes
- Make the not recognized image error handler run only if the image is present under a valid key and has an invalid value. ([#1565](https://github.com/expo/eas-cli/pull/1565) by [@szdziedzic](https://github.com/szdziedzic))
- Fix the `node:assert` import error by using imports from `assert` instead of from `node:assert`. ([#1569](https://github.com/expo/eas-cli/pull/1569) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Remove old republish code from the update command. ([#1535](https://github.com/expo/eas-cli/pull/1535) by [@byCedric](https://github.com/byCedric))
## [2.9.0](https://github.com/expo/eas-cli/releases/tag/v2.9.0) - 2022-12-05
### 🎉 New features
- Add caching for `eas build:run` command. ([#1542](https://github.com/expo/eas-cli/pull/1542) by [@szdziedzic](https://github.com/szdziedzic))
- Add `isGitWorkingTreeDirty` to EAS Update records. ([#1550](https://github.com/expo/eas-cli/pull/1550) by [@FiberJW](https://github.com/FiberJW))
- Prompt developer to download the app after the simulator build is finished. ([#1554](https://github.com/expo/eas-cli/pull/1554) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Ask for the missing application identifier consistently in `eas build:version:sync` command. ([#1543](https://github.com/expo/eas-cli/pull/1543) by [@wkozyra95](https://github.com/wkozyra95))
- Disable selecting builds whose artifacts have expired in the `eas build:run` command. ([#1547](https://github.com/expo/eas-cli/pull/1547) by [@szdziedzic](https://github.com/szdziedzic))
- Change intent with which the `eas build:run` command opens an Android app to `android.intent.action.MAIN`. ([#1556](https://github.com/expo/eas-cli/pull/1556) by [@szdziedzic](https://github.com/szdziedzic))
## [2.8.0](https://github.com/expo/eas-cli/releases/tag/v2.8.0) - 2022-11-28
### 🎉 New features
- Add `eas build:run` support for Android. ([#1485](https://github.com/expo/eas-cli/pull/1485) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Only create a new channel when update branch is new. ([#1507](https://github.com/expo/eas-cli/pull/1507) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
- Add a better error message for when an invalid `image` is set in `eas-json`. ([#1531](https://github.com/expo/eas-cli/pull/1531) by [@szdziedzic](https://github.com/szdziedzic))
- Remove internal expo id from device formatter. ([#1541](https://github.com/expo/eas-cli/pull/1541) by [@dsokal](https://github.com/dsokal))
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
### 🐛 Bug fixes
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
### 🎉 New features
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
@@ -272,7 +364,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
+47 -3
View File
@@ -1,4 +1,48 @@
## Releasing a new version
# Development
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
Open your `.zshrc` or other config file and add:
```
alias easd="/PATH/TO/eas-cli/bin/run"
```
Then use it with `easd` like `easd build`.
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
```
yarn start
```
## Format
- Be sure to update the [`CHANGELOG.md`](./CHANGELOG.md) with changes for every PR. There is a changelog bot that can generate the proper entry for you. The instructions on how to use it are in the PR description placeholder.
- End `async` functions with `Async` like `runAsync`. This is just how we format functions at Expo.
- Utilize the unified `Log` module instead of `console.log`.
## Using local and staging API
For development against local API:
- Set up the API server as described at https://github.com/expo/universe/tree/main/server/www.
- Set the `EXPO_LOCAL=1` env variable either on every command run (e.g. `EXPO_LOCAL=1 easd ...`) or more permamently with `export EXPO_LOCAL=1` (works in a current shell session).
For development against staging API:
- Set the `EXPO_STAGING=1` env variable either on every command run (e.g. `EXPO_STAGING=1 easd ...`) or more permamently with `export EXPO_STAGING=1` (works in a current shell session).
## Working on local builds (`eas build --local`)
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
## Testing
Run `yarn test` either in the repository root or in a package directory that you're working on.
## Releasing
See [RELEASING.md](./RELEASING.md).
+14
View File
@@ -0,0 +1,14 @@
# Releasing EAS CLI
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
## Choosing the next version
As stated above, the next EAS CLI version is chosen automatically based on the existing changelog entries under the unreleased section in **CHANGELOG.md**.
The algorithm works as follows:
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
- Otherwise, bump the PATCH version.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.7.0"
"version": "3.2.0"
}
+1 -1
View File
@@ -8,7 +8,7 @@
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck",
"start": "lerna run watch --parallel",
"start": "lerna run watch --stream --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
File diff suppressed because it is too large. Load diff
+747 -56
View File
@@ -2258,22 +2258,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -2438,22 +2422,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -2742,22 +2710,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -5129,6 +5081,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectArchive",
"description": "",
@@ -5435,6 +5399,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannel",
"description": "",
@@ -6623,6 +6599,51 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deploymentNew",
"description": "",
"args": [
{
"name": "channel",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeVersion",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "OBJECT",
"name": "DeploymentNew",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deployments",
"description": "Deployments associated with this app",
@@ -12012,7 +12033,20 @@
{
"name": "canRetry",
"description": "",
"args": [],
"args": [
{
"name": "newMode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
@@ -13860,6 +13894,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildMode",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "BUILD",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "RESIGN",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildMutation",
@@ -14713,6 +14770,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "BuildResignInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "applicationArchiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "ProjectArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildResourceClass",
@@ -14849,6 +14929,12 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNKNOWN",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -16499,6 +16585,224 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentBuildEdge",
"description": "",
"fields": [
{
"name": "cursor",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Build",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentBuildsConnection",
"description": "",
"fields": [
{
"name": "edges",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeploymentBuildEdge",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentNew",
"description": "Represents a Deployment - a set of Builds with the same Runtime Version and Channel",
"fields": [
{
"name": "builds",
"description": "",
"args": [
{
"name": "after",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "before",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "first",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "last",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeploymentBuildsConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "channel",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UpdateChannel",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtime",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Runtime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DeploymentOptions",
@@ -17413,6 +17717,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "GitHubAppEnvironment",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DEVELOPMENT",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRODUCTION",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STAGING",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitHubAppInstallation",
@@ -17551,13 +17884,9 @@
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
@@ -17870,6 +18199,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environment",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "GitHubAppEnvironment",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "installation",
"description": "",
@@ -17903,6 +18248,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "searchRepositories",
"description": "",
@@ -18066,6 +18427,18 @@
"name": "GitHubRepositoryMetadata",
"description": "",
"fields": [
{
"name": "defaultBranch",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "githubRepoDescription",
"description": "",
@@ -18158,6 +18531,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "openGraphImageUrl",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "private",
"description": "",
@@ -20665,6 +21050,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectArchive",
"description": "",
@@ -20936,6 +21333,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannel",
"description": "",
@@ -20948,6 +21357,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resign",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "BuildResignInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheme",
"description": "",
@@ -20984,6 +21405,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildWorkflow",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updates",
"description": "",
@@ -22394,6 +22827,73 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "PageInfo",
"description": "",
"fields": [
{
"name": "endCursor",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasNextPage",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasPreviousPage",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startCursor",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "PartialManifest",
@@ -22847,6 +23347,18 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "GCS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "NONE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "S3",
"description": "",
@@ -23176,6 +23688,26 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "excludedAssets",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "PartialManifestAsset",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitCommitHash",
"description": "",
@@ -23188,6 +23720,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isGitWorkingTreeDirty",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "",
@@ -25007,6 +25551,81 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Runtime",
"description": "",
"fields": [
{
"name": "app",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "App",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstBuildCreatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "version",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SecondFactorBooleanResult",
@@ -27568,6 +28187,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isGitWorkingTreeDirty",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "manifestFragment",
"description": "",
@@ -29909,6 +30544,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accountProfilePhoto",
"description": "If the invite is for a personal team, the profile photo of account owner",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "created",
"description": "",
@@ -29973,6 +30620,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isForOrganization",
"description": "If the invite is for an organization or a personal team",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "permissions",
"description": "Account permissions to be granted upon acceptance of this invitation",
@@ -30285,6 +30948,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accountProfilePhoto",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "created",
"description": "",
@@ -30348,6 +31023,22 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isForOrganization",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.7.0",
"version": "3.2.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -13,8 +13,8 @@
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.96",
"@expo/eas-json": "2.7.0",
"@expo/eas-build-job": "0.2.97",
"@expo/eas-json": "3.0.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
@@ -1,17 +0,0 @@
import { fixS3Url } from '../uploads';
describe(fixS3Url, () => {
it('fixes broken links', () => {
const input =
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
const expectedOutput =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixS3Url(input)).toBe(expectedOutput);
});
it('does not change correct urls', () => {
const correctUrl =
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
expect(fixS3Url(correctUrl)).toBe(correctUrl);
});
});
+6 -5
View File
@@ -15,6 +15,7 @@ import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
SelectPromptEntry,
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
@@ -31,7 +32,7 @@ export async function selectBranchOnAppAsync(
paginatedQueryOptions,
}: {
projectId: string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
promptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
@@ -48,7 +49,7 @@ export async function selectBranchOnAppAsync(
promptOptions: {
title: promptTitle,
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
makePartialChoiceObject: displayTextForListItem,
},
});
if (!selectedBranch) {
@@ -160,7 +161,7 @@ export async function ensureBranchExistsAsync(
appId: string;
branchName: string;
}
): Promise<{ branchId: string }> {
): Promise<{ branchId: string; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
@@ -168,14 +169,14 @@ export async function ensureBranchExistsAsync(
});
const { id } = updateBranch;
return { branchId: id };
return { branchId: id, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id };
return { branchId: newUpdateBranch.id, createdBranch: true };
} else {
throw error;
}
+9 -9
View File
@@ -26,7 +26,7 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadFileAtPathToS3Async } from '../uploads';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -113,7 +113,7 @@ export async function prepareBuildRequestForPlatformAsync<
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
} as const);
@@ -219,9 +219,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
projectTarballPath = projectTarball.path;
const { bucketKey } = await uploadFileAtPathToS3Async(
const bucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildProjectSources,
UploadSessionType.EasBuildGcsProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
@@ -281,7 +281,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
);
}
type MaybeBuildFragment = BuildFragment | null;
export type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
@@ -362,7 +362,7 @@ async function handleSingleBuildProgressAsync(
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
formatAccountSubscriptionsUrl(accountName)
)}.`;
break;
@@ -383,7 +383,7 @@ async function handleSingleBuildProgressAsync(
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${chalk.underline(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountSubscriptionsUrl(accountName)
)}`
);
@@ -423,8 +423,8 @@ async function handleSingleBuildProgressAsync(
}
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
[BuildPriority.Normal]: 'queue',
[BuildPriority.NormalPlus]: 'queue',
[BuildPriority.Normal]: 'Free tier queue',
[BuildPriority.NormalPlus]: 'Free tier queue',
[BuildPriority.High]: 'priority queue',
};
@@ -78,6 +78,7 @@ export async function createBuildContextAsync<T extends Platform>({
const analyticsEventProperties = {
tracking_id: uuidv4(),
platform,
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
...(accountId && { account_id: accountId }),
project_id: projectId,
project_type: workflow,
+5
View File
@@ -16,6 +16,11 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
type: ProjectArchiveSourceType.S3,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.GCS) {
return {
type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.URL) {
return {
type: ProjectArchiveSourceType.Url,
@@ -1,5 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { CredentialsContext } from '../../credentials/context';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { IosCredentials, Target } from '../../credentials/ios/types';
@@ -31,6 +33,27 @@ export async function ensureIosCredentialsAsync(
};
}
export async function ensureIosCredentialsForBuildResignAsync(
credentialsCtx: CredentialsContext,
targets: Target[],
buildProfile: BuildProfile<Platform.IOS>
): Promise<CredentialsResult<IosCredentials>> {
const provider = new IosCredentialsProvider(credentialsCtx, {
app: await getAppFromContextAsync(credentialsCtx),
targets,
distribution: 'internal',
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
});
const { credentialsSource } = buildProfile;
logCredentialsSource(credentialsSource, Platform.IOS);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
};
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator;
}
+1 -1
View File
@@ -26,7 +26,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
};
}
function transformIosSecrets(secrets: {
export function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
}): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
@@ -1,8 +1,10 @@
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import path from 'path';
import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -79,6 +81,22 @@ export async function prepareJobAsync(
return sanitizeJob(job);
}
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
for (const targetName of Object.keys(credentials ?? {})) {
buildCredentials.push({
targetName,
provisioningProfileBase64: nullthrows(credentials?.[targetName].provisioningProfile),
distributionCertificate: {
dataBase64: nullthrows(credentials?.[targetName].distributionCertificate.certificateP12),
password: nullthrows(credentials?.[targetName].distributionCertificate.certificatePassword),
},
});
}
return { buildCredentials };
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.118';
const PLUGIN_PACKAGE_VERSION = '0.0.120';
export interface LocalBuildOptions {
enable: boolean;
+73 -31
View File
@@ -2,13 +2,15 @@ import chalk from 'chalk';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { BuildFilter, BuildFragment } from '../graphql/generated';
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { appPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { fromNow } from '../utils/date';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
export const BUILDS_LIMIT = 50;
@@ -55,43 +57,83 @@ export async function listAndRenderBuildsOnAppAsync(
}
}
export async function listAndSelectBuildsOnAppAsync(
export async function listAndSelectBuildOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
projectDisplayName,
title,
filter,
queryOptions,
paginatedQueryOptions,
selectPromptDisabledFunction,
selectPromptWarningMessage,
}: {
projectId: string;
projectDisplayName: string;
title: string;
filter?: BuildFilter;
queryOptions: PaginatedQueryOptions;
paginatedQueryOptions: PaginatedQueryOptions;
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
selectPromptWarningMessage?: string;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit: queryOptions.limit ?? BUILDS_LIMIT,
offset: queryOptions.offset,
filter,
});
if (builds.length === 0) {
throw new Error('Found no builds matching the provided criteria.');
): Promise<BuildFragment | void> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a build in non-interactive mode.');
} else {
return await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit,
offset,
filter,
}),
promptOptions: {
title,
getIdentifierForQueryItem: build => build.id,
makePartialChoiceObject: createBuildToPartialChoiceMaker(selectPromptDisabledFunction),
selectPromptWarningMessage,
},
});
}
}
const { selectedSimulatorBuild } = await promptAsync({
type: 'select',
message: `Select simulator build to run for ${projectDisplayName} app`,
name: 'selectedSimulatorBuild',
choices: builds.map(build => ({
title: `id: ${build.id}, platform: ${appPlatformDisplayNames[build.platform]}, version: ${
build.appVersion
}, build number: ${build.appBuildVersion}`,
value: build,
})),
});
return selectedSimulatorBuild;
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : 'Unknown';
}
function createBuildToPartialChoiceMaker(
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
) {
return (
build: BuildFragment
): {
title: string;
description: string;
disabled?: boolean;
} => {
const splitCommitMessage = build.gitCommitMessage?.split('\n');
const formattedCommitData =
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: 'Unknown';
return {
title: `${chalk.bold(`ID:`)} ${build.id} (${chalk.bold(
`${fromNow(new Date(build.completedAt))} ago`
)})`,
description: [
`\t${chalk.bold(`Version:`)} ${formatBuildChoiceValue(build.appVersion)}`,
`\t${chalk.bold(
build.platform === AppPlatform.Ios ? 'Build number:' : 'Version code:'
)} ${formatBuildChoiceValue(build.appBuildVersion)}`,
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
].join('\n'),
disabled: selectPromptDisabledFunction?.(build),
};
};
}
export async function getLatestBuildAsync(
@@ -7,6 +7,7 @@ import {
EasJsonUtils,
SubmitProfile,
} from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -37,6 +38,9 @@ import {
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { runAsync } from '../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
import { createSubmissionContextAsync } from '../submit/context';
import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
@@ -46,11 +50,13 @@ import {
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { BuildRequestSender, waitForBuildEndAsync } from './build';
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build';
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
@@ -242,6 +248,9 @@ export async function runBuildAndSubmitAsync(
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
printJsonOnlyOutput(builds);
@@ -409,3 +418,35 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
process.exit(1);
}
}
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
assert(build.artifacts?.applicationArchiveUrl);
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform
);
await runAsync(buildPath, build.platform);
}
async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[],
flags: BuildFlags
): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
if (simBuilds.length > 0 && !flags.autoSubmit && !flags.nonInteractive) {
for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine();
const confirm = await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
});
if (confirm) {
await downloadAndRunAsync(simBuild);
}
}
}
}
}
@@ -1,5 +1,4 @@
import assert from 'assert';
import chalk from 'chalk';
import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
@@ -12,20 +11,20 @@ import {
EasBuildDeprecationInfo,
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import Log, { learnMore, link } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build details: ${chalk.underline(logsUrl)}`
} build details: ${link(logsUrl)}`
);
});
}
@@ -79,13 +78,13 @@ function printBuildResult(build: BuildFragment): void {
appPlatformDisplayNames[build.platform]
} devices (or scan the QR code) to install the app:`
);
Log.log(`${chalk.underline(logsUrl)}`);
Log.log(`${link(logsUrl)}`);
} else {
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
// we should account for this case better if it is possible
const url = build.artifacts?.buildUrl ?? '';
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
Log.log(`${chalk.underline(url)}`);
Log.log(`${link(url)}`);
}
}
+5
View File
@@ -0,0 +1,5 @@
export class ChannelNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Channel not found.');
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ export async function selectChannelOnAppAsync(
queryChannelsOnAppAsync(graphqlClient, { appId: projectId, limit, offset }),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
makePartialChoiceObject: updateChannel => ({ title: updateChannel.name }),
getIdentifierForQueryItem: updateChannel => updateChannel.id,
},
});
@@ -59,7 +59,7 @@ describe(EasCommand.name, () => {
const sessionManagerSpy = jest.spyOn(SessionManager.prototype, 'getUserAsync');
expect(sessionManagerSpy).toBeCalledTimes(1);
}, 15_000);
}, 30_000);
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand();
@@ -120,7 +120,7 @@ export default class BranchDelete extends EasCommand {
}
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
promptTitle: 'Which branch would you like to delete?',
paginatedQueryOptions,
}));
+3 -1
View File
@@ -57,7 +57,9 @@ export default class BranchView extends EasCommand {
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: paginatedQueryOptions.json,
+1 -1
View File
@@ -245,7 +245,7 @@ export default class Build extends EasCommand {
}
}
async function handleDeprecatedEasJsonAsync(
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
@@ -0,0 +1,296 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import { handleDeprecatedEasJsonAsync } from '.';
import { waitForBuildEndAsync } from '../../build/build';
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags } from '../../commandUtils/pagination';
import { CredentialsContext } from '../../credentials/context';
import {
BuildFragment,
BuildMode,
BuildStatus,
BuildWorkflow,
DistributionType,
IosJobOverridesInput,
ProjectArchiveSourceType,
StatuspageServiceName,
} from '../../graphql/generated';
import { BuildMutation } from '../../graphql/mutations/BuildMutation';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { requestedPlatformDisplayNames, selectPlatformAsync } from '../../platform';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
interface BuildResignFlags {
json: boolean;
nonInteractive: boolean;
offset?: number;
limit?: number;
platform?: Platform;
profile?: string;
maybeBuildId?: string;
wait: boolean;
}
interface RawBuildResignFlags {
json: boolean;
'non-interactive': boolean;
offset: number | undefined;
limit: number | undefined;
platform: 'android' | 'ios' | undefined;
profile: string | undefined;
wait: boolean;
id: string | undefined;
}
export default class BuildResign extends EasCommand {
static override description = 're-sign a build archive';
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
wait: Flags.boolean({
default: true,
allowNo: true,
description: 'Wait for build(s) to complete.',
}),
id: Flags.string({
description: 'ID of the build to re-sign.',
}),
...EasPaginatedQueryFlags,
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(BuildResign);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = this.sanitizeFlags(rawFlags as RawBuildResignFlags);
const { limit, offset, nonInteractive } = flags;
const {
loggedIn: { actor, graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
analytics,
} = await this.getContextAsync(BuildResign, {
nonInteractive: flags.nonInteractive,
});
const maybeBuild = flags.maybeBuildId
? await this.maybeGetBuildAsync(graphqlClient, flags.maybeBuildId)
: undefined;
const platform =
(maybeBuild?.platform.toLowerCase() as Platform | undefined) ??
(await selectPlatformAsync(flags.platform));
if (platform === Platform.ANDROID) {
throw new Error('Re-signing archives is not supported on Android yet.');
}
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
flags.profile ?? 'production'
);
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const build = await this.ensureBuildSelectedAsync(
{ graphqlClient, projectId, platform, nonInteractive, limit, offset },
maybeBuild
);
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
nonInteractive,
projectDir,
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
easJsonCliConfig,
});
if (buildProfile.credentialsSource !== CredentialsSource.LOCAL && !nonInteractive) {
await credentialsCtx.appStore.ensureAuthenticatedAsync();
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir,
nonInteractive,
exp,
},
buildProfile
);
const targets = await resolveTargetsAsync({
projectDir,
exp,
xcodeBuildContext,
env: buildProfile.env,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
credentialsCtx,
targets,
buildProfile
);
assert(build.artifacts?.applicationArchiveUrl, 'Missing application archive.');
const jobOverrides: IosJobOverridesInput = {
mode: BuildMode.Resign,
type: BuildWorkflow.Unknown,
resign: {
applicationArchiveSource: {
type: ProjectArchiveSourceType.Url,
url: build.artifacts?.applicationArchiveUrl,
},
},
secrets: prepareCredentialsToResign(credentialsResult.credentials),
builderEnvironment: { image: 'default' },
};
const newBuild = await BuildMutation.retryIosBuildAsync(graphqlClient, {
buildId: build.id,
jobOverrides,
});
Log.addNewLineIfNone();
printLogsUrls([newBuild]);
Log.newLine();
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput(newBuild);
}
return;
}
const buildResult = await waitForBuildEndAsync(graphqlClient, {
buildIds: [newBuild.id],
accountName: account.name,
});
if (!flags.json) {
printBuildResults(buildResult);
} else {
assert(buildResult[0], 'missing build results');
printJsonOnlyOutput(buildResult[0]);
}
}
sanitizeFlags(flags: RawBuildResignFlags): BuildResignFlags {
const nonInteractive = flags['non-interactive'];
if (nonInteractive && !flags.id) {
throw new Error(
`${chalk.bold('--id')} is required when running with ${chalk.bold(
'--non-interactive'
)} flag.`
);
}
return {
json: flags.json,
nonInteractive,
offset: flags.offset,
limit: flags.limit,
platform: flags.platform as Platform | undefined,
profile: flags.profile,
maybeBuildId: flags.id,
wait: flags.wait,
};
}
async ensureBuildSelectedAsync(
{
graphqlClient,
projectId,
platform,
nonInteractive,
limit,
offset,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
nonInteractive: boolean;
limit?: number;
offset?: number;
},
maybeBuild?: BuildFragment
): Promise<BuildFragment> {
if (maybeBuild) {
return maybeBuild;
}
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
projectId,
title: 'Which build would you like to re-sign with new credentials?',
paginatedQueryOptions: {
limit,
offset: offset ?? 0,
nonInteractive,
json: false,
},
filter: {
distribution: DistributionType.Internal,
platform: toAppPlatform(platform),
status: BuildStatus.Finished,
},
});
if (!build) {
throw new Error('There are no builds that can be re-signed on this project.');
}
return build;
}
async maybeGetBuildAsync(
graphqlClient: ExpoGraphqlClient,
maybeBuildId?: string,
maybePlatform?: Platform
): Promise<BuildFragment | undefined> {
if (maybeBuildId) {
const build = await BuildQuery.byIdAsync(graphqlClient, maybeBuildId);
if (build.distribution !== DistributionType.Internal) {
throw new Error('This is not an internal distribution build.');
}
if (build.status !== BuildStatus.Finished) {
throw new Error('Only builds that finished successfully can be re-signed.');
}
if (maybePlatform && build.platform !== toAppPlatform(maybePlatform)) {
throw new Error(
`Build with ID ${maybeBuildId} was not created for platform ${requestedPlatformDisplayNames[maybePlatform]}.`
);
}
return build;
}
return undefined;
}
}
+70 -25
View File
@@ -1,9 +1,9 @@
import { Errors, Flags } from '@oclif/core';
import { existsSync } from 'fs-extra';
import assert from 'node:assert';
import assert from 'assert';
import { pathExists } from 'fs-extra';
import path from 'path';
import { getLatestBuildAsync, listAndSelectBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType } from '../../build/types';
import { getLatestBuildAsync, listAndSelectBuildOnAppAsync } from '../../build/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -11,13 +11,18 @@ import {
PaginatedQueryOptions,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, runAsync } from '../../run/run';
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
import { downloadAndExtractAppAsync, extractAppFromLocalArchiveAsync } from '../../utils/download';
import { isRunnableOnSimulatorOrEmulator } from '../../run/utils';
import {
downloadAndMaybeExtractAppAsync,
extractAppFromLocalArchiveAsync,
} from '../../utils/download';
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
interface RawRunFlags {
latest?: boolean;
@@ -37,8 +42,6 @@ interface RunCommandFlags {
}
export default class Run extends EasCommand {
static override hidden = true;
static override description = 'run simulator/emulator builds from eas-cli';
static override flags = {
@@ -51,7 +54,7 @@ export default class Run extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
path: Flags.string({
description: 'Path to the simulator/emulator build archive or simulator build app',
description: 'Path to the simulator/emulator build archive or app',
exclusive: ['latest', 'id', 'url'],
}),
id: Flags.string({
@@ -97,13 +100,19 @@ export default class Run extends EasCommand {
const selectedPlatform = await resolvePlatformAsync(platform);
if (platform === 'ios' && process.platform !== 'darwin') {
Errors.error('You can only use an iOS simulator to run apps on macOS devices', {
exit: 1,
});
}
if (
runArchiveFlags.path &&
!(
(runArchiveFlags.path.endsWith('.tar.gz') ||
runArchiveFlags.path.endsWith('.app') ||
runArchiveFlags.path.endsWith('.apk')) &&
existsSync(runArchiveFlags.path)
(await pathExists(runArchiveFlags.path))
)
) {
Errors.error('The path must point to a .tar.gz archive, .apk file, or .app directory', {
@@ -121,6 +130,10 @@ export default class Run extends EasCommand {
}
async function resolvePlatformAsync(platform?: string): Promise<AppPlatform> {
if (process.platform !== 'darwin') {
return AppPlatform.Android;
}
if (platform && Object.values(AppPlatform).includes(platform.toUpperCase() as AppPlatform)) {
return platform.toUpperCase() as AppPlatform;
}
@@ -143,6 +156,9 @@ async function maybeGetBuildAsync(
projectId: string,
paginatedQueryOptions: PaginatedQueryOptions
): Promise<BuildFragment | null> {
const distributionType =
flags.selectedPlatform === AppPlatform.Ios ? DistributionType.Simulator : undefined;
if (flags.runArchiveFlags.id) {
return BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
} else if (
@@ -151,26 +167,29 @@ async function maybeGetBuildAsync(
!flags.runArchiveFlags.url &&
!flags.runArchiveFlags.latest
) {
return await listAndSelectBuildsOnAppAsync(graphqlClient, {
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
projectId,
projectDisplayName: await getDisplayNameForProjectIdAsync(graphqlClient, projectId),
title: `Select ${flags.selectedPlatform === AppPlatform.Ios ? 'iOS' : 'Android'} ${
flags.selectedPlatform === AppPlatform.Ios ? 'simulator' : 'emulator'
} build to run for ${await getDisplayNameForProjectIdAsync(graphqlClient, projectId)} app`,
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
distribution: distributionType,
status: BuildStatus.Finished,
},
queryOptions: paginatedQueryOptions,
paginatedQueryOptions,
selectPromptDisabledFunction: build => !isRunnableOnSimulatorOrEmulator(build),
selectPromptWarningMessage:
'Artifacts for this build have expired and are no longer available, or this is not a simulator/emulator build.',
});
return build ?? null;
} else if (flags.runArchiveFlags.latest) {
return await getLatestBuildAsync(graphqlClient, {
projectId,
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
distribution: distributionType,
status: BuildStatus.Finished,
},
});
@@ -179,6 +198,17 @@ async function maybeGetBuildAsync(
}
}
function getEasBuildRunCachedAppPath(
projectId: string,
buildId: string,
platform: AppPlatform
): string {
return path.join(
getEasBuildRunCacheDirectoryPath(),
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
);
}
async function getPathToSimulatorBuildAppAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
@@ -186,25 +216,40 @@ async function getPathToSimulatorBuildAppAsync(
queryOptions: PaginatedQueryOptions
): Promise<string> {
const maybeBuild = await maybeGetBuildAsync(graphqlClient, flags, projectId, queryOptions);
const appExtension = flags.selectedPlatform === AppPlatform.Ios ? 'app' : 'apk';
if (maybeBuild) {
const cachedAppPath = getEasBuildRunCachedAppPath(
projectId,
maybeBuild.id,
flags.selectedPlatform
);
if (await pathExists(cachedAppPath)) {
Log.newLine();
Log.log(`Using cached app...`);
return cachedAppPath;
}
if (!maybeBuild.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndExtractAppAsync(
return await downloadAndMaybeExtractAppAsync(
maybeBuild.artifacts.applicationArchiveUrl,
appExtension
flags.selectedPlatform,
cachedAppPath
);
}
if (flags.runArchiveFlags.url) {
return await downloadAndExtractAppAsync(flags.runArchiveFlags.url, appExtension);
return await downloadAndMaybeExtractAppAsync(flags.runArchiveFlags.url, flags.selectedPlatform);
}
if (flags.runArchiveFlags.path?.endsWith('.tar.gz')) {
return await extractAppFromLocalArchiveAsync(flags.runArchiveFlags.path!, appExtension);
return await extractAppFromLocalArchiveAsync(
flags.runArchiveFlags.path,
flags.selectedPlatform
);
}
// this should never fail, due to the validation in sanitizeFlagsAsync
@@ -107,7 +107,7 @@ export default class ChannelEdit extends EasCommand {
: await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like ${existingChannel.name} to point at?`,
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
paginatedQueryOptions: {
json,
nonInteractive,
@@ -13,6 +13,7 @@ export default class Credentials extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.OptionalProjectConfig,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.Analytics,
};
@@ -21,6 +22,7 @@ export default class Credentials extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig,
getDynamicProjectConfigAsync,
analytics,
} = await this.getContextAsync(Credentials, {
nonInteractive: false,
@@ -31,6 +33,7 @@ export default class Credentials extends EasCommand {
graphqlClient,
analytics,
projectConfig ?? null,
getDynamicProjectConfigAsync,
flags.platform
).runAsync();
}
@@ -3,7 +3,6 @@ import { ExpoConfig } from '@expo/config-types';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import terminalLink from 'terminal-link';
import { getProjectDashboardUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
@@ -12,7 +11,7 @@ import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contex
import { AppPrivacy, Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log from '../../log';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
@@ -303,10 +302,7 @@ export default class ProjectInit extends EasCommand {
}
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
const projectLink = terminalLink(projectFullName, projectDashboardUrl, {
// https://github.com/sindresorhus/terminal-link/issues/18#issuecomment-1068020361
fallback: () => `${projectFullName} (${projectDashboardUrl})`,
});
const projectLink = link(projectDashboardUrl, { text: projectFullName });
const account = nullthrows(allAccounts.find(a => a.name === accountName));
+1 -1
View File
@@ -75,7 +75,7 @@ export default class Submit extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
verbose: Flags.boolean({
description: 'Always print logs from Submission Service',
description: 'Always print logs from EAS Submit',
default: false,
}),
wait: Flags.boolean({
@@ -0,0 +1,248 @@
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import UpdatePublish from '..';
import { ensureBranchExistsAsync } from '../../../branch/queries';
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('@expo/config-plugins');
jest.mock('../../../branch/queries');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
jest.mock('../../../project/publish', () => ({
...jest.requireActual('../../../project/publish'),
buildBundlesAsync: jest.fn(),
collectAssetsAsync: jest.fn(),
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
uploadAssetsAsync: jest.fn(),
}));
describe(UpdatePublish.name, () => {
afterEach(() => vol.reset());
// Deprecated and split to a new command: update:republish
it('errors with --republish', async () => {
await expect(new UpdatePublish(['--republish'], commandOptions).run()).rejects.toThrow(
'--group and --republish flags are deprecated'
);
});
// Deprecated and split to a new command: update:republish
it('errors with --group', async () => {
await expect(new UpdatePublish(['--group=abc123'], commandOptions).run()).rejects.toThrow(
'--group and --republish flags are deprecated'
);
});
it('errors with both --channel and --branch', async () => {
const flags = ['--channel=channel123', '--branch=branch123'];
mockTestProject();
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
});
it('creates a new update with --non-interactive, --branch, and --message', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
mockTestProject();
const { platforms, runtimeVersion } = mockTestExport();
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
platforms.map(platform => ({
id: 'update123',
group: 'group123',
runtimeVersion,
platform,
manifestPermalink: 'https://example.com/update123/manifest',
}))
);
await new UpdatePublish(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a new update with --non-interactive, --channel, and --message', async () => {
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
const { projectId } = mockTestProject();
const { platforms, runtimeVersion } = mockTestExport();
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
platforms.map(platform => ({
id: 'update123',
group: 'group123',
runtimeVersion,
platform,
manifestPermalink: 'https://example.com/update123/manifest',
}))
);
await new UpdatePublish(flags, commandOptions).run();
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
{
appId: projectId,
branchName: 'branchFromChannel',
}
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
});
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
function mockTestProject({
configuredProjectId = '1234',
}: {
configuredProjectId?: string;
} = {}): { projectId: string } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const appJSON: AppJSONConfig = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: jester.accounts[0].name,
extra: {
eas: {
projectId: configuredProjectId,
},
},
},
};
vol.fromJSON(
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
},
projectRoot
);
const mockManifest = { exp: appJSON.expo };
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => ({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
}));
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId };
}
/** Create a new in-memory export of the project */
function mockTestExport({
exportDir = 'dist',
platforms = ['android', 'ios'],
runtimeVersion = 'exposdk:47.0.0',
}: {
exportDir?: string;
platforms?: Platform[];
runtimeVersion?: string;
} = {}): {
inputDir: string;
platforms: Platform[];
runtimeVersion: string;
} {
/* eslint-disable node/no-sync */
vol.mkdirpSync(path.join(projectRoot, exportDir, 'bundles'));
for (const platform of platforms) {
vol.writeFileSync(
path.join(projectRoot, exportDir, 'bundles', `${platform}-fake.js`),
`console.log("fake bundle for ${platform}");`
);
}
jest.mocked(collectAssetsAsync).mockResolvedValue(
Object.fromEntries(
platforms.map(platform => [
platform,
{
assets: [],
launchAsset: {
contentType: 'application/javascript',
path: path.join(projectRoot, exportDir, 'bundles', `${platform}-fake.js`),
},
},
])
)
);
jest.mocked(uploadAssetsAsync).mockResolvedValue({
// platforms are mocked all containing only the launch asset
assetCount: platforms.length,
uniqueAssetCount: platforms.length,
uniqueUploadedAssetCount: platforms.length,
assetLimitPerUpdateGroup: 9001,
});
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
return { inputDir: exportDir, platforms, runtimeVersion };
}
@@ -0,0 +1,343 @@
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import ProjectConfigContextField from '../../../commandUtils/context/ProjectConfigContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { CodeSigningInfo, UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { selectUpdateGroupOnBranchAsync } from '../../../update/queries';
import UpdateRepublish from '../republish';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
const updateStub: UpdateFragment = {
id: 'update-1234',
group: 'group-1234',
branch: { id: 'branch-1234', name: 'main' },
message: 'test message',
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
};
const codeSigningStub: CodeSigningInfo = {
keyid: 'keyid',
alg: 'alg',
sig: 'sig',
};
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../update/queries');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
describe(UpdateRepublish.name, () => {
afterEach(() => vol.reset());
it('errors without --channel, --branch, or --group', async () => {
await expect(new UpdateRepublish([], commandOptions).run()).rejects.toThrow(
'--channel, --branch, or --group must be specified'
);
});
it('errors when providing both --group and --branch', async () => {
const flags = ['--group=1234', '--branch=main'];
mockTestProject();
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
/--branch=main cannot also be provided when using --group/
);
});
it('errors when providing both --channel and --branch', async () => {
const flags = ['--channel=main', '--branch=main'];
mockTestProject();
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
/--branch=main cannot also be provided when using --channel/
);
});
it('errors when providing both --group and --channel', async () => {
const flags = ['--group=1234', '--channel=main'];
mockTestProject();
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
/--channel=main cannot also be provided when using --group/
);
});
it('errors when update group is empty', async () => {
const flags = ['--group=1234'];
mockTestProject();
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([]);
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
'There are no published updates found'
);
});
it('errors when update group has no update for requested platform', async () => {
const flags = ['--group=1234', '--platform=android'];
mockTestProject();
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
platform: 'ios',
},
]);
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
'There are no updates on branch "main" published for the platform(s) "android" with group ID "1234". Did you mean to publish a new update instead?'
);
});
it('re-creates update with --group and --message', async () => {
const flags = ['--group=1234', '--message=test-republish'];
mockTestProject();
// Mock queries to retrieve the update and code signing info
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
await new UpdateRepublish(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: updateStub.branch.id,
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
expect(PublishMutation.setCodeSigningInfoAsync).not.toHaveBeenCalled();
});
it('re-creates update using codesigning with --group and --message', async () => {
const flags = ['--group=1234', '--message=test-republish'];
const codeSigning = {
alg: 'alg',
keyid: 'keyid',
sig: 'sig',
};
mockTestProject();
// Mock queries to retrieve the update and code signing info
jest
.mocked(UpdateQuery.viewUpdateGroupAsync)
.mockResolvedValue([{ ...updateStub, codeSigningInfo: codeSigningStub }]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
jest.mocked(PublishMutation.setCodeSigningInfoAsync).mockResolvedValue({} as any);
await new UpdateRepublish(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: updateStub.branch.id,
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: true,
}),
])
);
expect(PublishMutation.setCodeSigningInfoAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
'update-new',
expect.objectContaining(codeSigning)
);
});
it('re-creates update with --branch and --message', async () => {
const flags = ['--branch=branch123', '--message=test-republish'];
mockTestProject();
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
await new UpdateRepublish(flags, commandOptions).run();
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.objectContaining({ branchName: 'branch123' })
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: updateStub.branch.id,
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
});
it('re-creates update with --channel and --message', async () => {
const flags = ['--channel=channel123', '--message=test-republish'];
mockTestProject();
// Mock resolving the channel to branch name, only valid for a single branch connected
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
await new UpdateRepublish(flags, commandOptions).run();
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.objectContaining({ branchName: 'branchFromChannel' })
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: updateStub.branch.id,
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
});
});
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
function mockTestProject({
configuredProjectId = '1234',
}: {
configuredProjectId?: string;
} = {}): void {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const appJSON: AppJSONConfig = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: jester.accounts[0].name,
extra: {
eas: {
projectId: configuredProjectId,
},
},
},
};
vol.fromJSON(
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
},
projectRoot
);
const mockManifest = { exp: appJSON.expo };
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest.spyOn(ProjectConfigContextField.prototype, 'getValueAsync').mockResolvedValue({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
});
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
}
@@ -1,10 +1,14 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import {
ensureEASUpdateIsConfiguredAsync,
ensureEASUpdateIsConfiguredInEasJsonAsync,
} from '../../update/configure';
export default class UpdateConfigure extends EasCommand {
static override description = 'configure the project to support EAS Update';
@@ -38,14 +42,33 @@ export default class UpdateConfigure extends EasCommand {
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId,
projectDir,
platform,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
Log.addNewLineIfNone();
Log.log(`🎉 Your app is configured to run EAS Update!`);
Log.log(`🎉 Your app is configured with EAS Update!`);
Log.newLine();
Log.log(`${chalk.bold('Next steps')}:`);
Log.newLine();
Log.log('Update a production build:');
Log.log(`1. Create a new build. Example: ${chalk.bold('eas build --profile production')}.`);
Log.log('2. Make changes in your project.');
Log.log(`3. Publish an update. Example: ${chalk.bold('eas update --channel production')}.`);
Log.log('4. Force close and reopen the app at least twice to view the update.');
Log.newLine();
Log.log('Preview an update:');
Log.log(
`1. Publish an update to a branch. Example: ${chalk.bold('eas update --branch new-feature')}.`
);
Log.log(
'2. In Expo Go or a development build, navigate to Projects > [project name] > Branch > Open.'
);
}
}
+133 -186
View File
@@ -17,12 +17,10 @@ import fetch from '../../fetch';
import {
PublishUpdateGroupInput,
StatuspageServiceName,
Update,
UpdateInfoGroup,
UpdatePublishMutation,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
@@ -39,8 +37,8 @@ import {
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
@@ -59,24 +57,26 @@ export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', '
type RawUpdateFlags = {
auto: boolean;
branch?: string;
channel?: string;
message?: string;
group?: string;
republish?: boolean;
platform: string;
'input-dir': string;
'skip-bundler': boolean;
'private-key-path'?: string;
'non-interactive': boolean;
json: boolean;
/** @deprecated see UpdateRepublish command */
group?: string;
/** @deprecated see UpdateRepublish command */
republish?: boolean;
};
type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
channelName?: string;
updateMessage?: string;
republish: boolean;
groupId?: string;
inputDir: string;
skipBundler: boolean;
privateKeyPath?: string;
@@ -107,16 +107,20 @@ export default class UpdatePublish extends EasCommand {
description: 'Branch to publish the update group on',
required: false,
}),
channel: Flags.string({
description: 'Channel that the published update should affect',
required: false,
}),
message: Flags.string({
description: 'A short message describing the update',
required: false,
}),
republish: Flags.boolean({
description: 'Republish an update group',
description: 'Republish an update group (deprecated, see republish command)',
exclusive: ['input-dir', 'skip-bundler'],
}),
group: Flags.string({
description: 'Update group to republish',
description: 'Update group to republish (deprecated, see republish command)',
exclusive: ['input-dir', 'skip-bundler'],
}),
'input-dir': Flags.string({
@@ -161,16 +165,15 @@ export default class UpdatePublish extends EasCommand {
let {
auto: autoFlag,
platform: platformFlag,
branchName,
channelName,
updateMessage,
republish,
groupId,
inputDir,
skipBundler,
privateKeyPath,
json: jsonFlag,
nonInteractive,
} = this.sanitizeFlags(rawFlags);
let branchName = this.sanitizeFlags(rawFlags).branchName;
const {
getDynamicProjectConfigAsync,
@@ -197,7 +200,7 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
projectDir,
@@ -209,22 +212,31 @@ export default class UpdatePublish extends EasCommand {
let realizedPlatforms: PublishPlatform[] = [];
if (channelName && branchName) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
}
if (channelName) {
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
}
if (!branchName) {
if (autoFlag) {
branchName = await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
} else {
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to ${
republish ? 'republish' : 'publish'
} on?`,
displayTextForListItem: updateBranch =>
`${updateBranch.name} ${chalk.grey(
promptTitle: `Which branch would you like to publish on?`,
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
@@ -243,156 +255,72 @@ export default class UpdatePublish extends EasCommand {
}
}
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
try {
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
bundleSpinner.fail('Export failed');
throw e;
}
}
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
// Instead we get the `updateInfoGroup` from the update we wish to republish.
let updatesToRepublish: Pick<
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (groupId) {
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId,
});
updatesToRepublish = updatesByGroup;
} else {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
const uploadResults = await uploadAssetsAsync(
graphqlClient,
assets,
projectId,
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
}
updatesToRepublish = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions,
});
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
groupId ? groupId : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length < defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same platforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(', ')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
realizedPlatforms = updatesToRepublishFilteredByPlatform.map(
update => update.platform as PublishPlatform
);
// These are the same for each member of an update group
groupId = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: `Republish "${oldMessage!}" - group: ${groupId}`,
validate: (value: any) => (value ? true : validationMessage),
}));
}
} else {
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
try {
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
bundleSpinner.fail('Export failed');
throw e;
}
}
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
const uploadResults = await uploadAssetsAsync(
graphqlClient,
assets,
projectId,
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
}
);
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
}
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
}
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
@@ -412,18 +340,24 @@ export default class UpdatePublish extends EasCommand {
}
}
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await getVcsClient().getCommitHashAsync();
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
@@ -435,17 +369,13 @@ export default class UpdatePublish extends EasCommand {
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Cannot find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtimeVersion,
runtimeVersion,
message: truncatedMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -547,7 +477,14 @@ export default class UpdatePublish extends EasCommand {
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: truncatedMessage },
...(gitCommitHash ? [{ label: 'Commit', value: gitCommitHash }] : []),
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'Website link', value: updateGroupLink },
])
);
@@ -571,26 +508,36 @@ export default class UpdatePublish extends EasCommand {
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && updateMessage)) {
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
Errors.error(
'--auto or both --branch and --message are required when updating in non-interactive mode',
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
{ exit: 1 }
);
}
const groupId = flags.group;
const republish = flags.republish || !!groupId; // When --group is defined, we are republishing
if (nonInteractive && republish && !groupId) {
Errors.error(`--group is required when updating in non-interactive mode`, { exit: 1 });
if (flags.group || flags.republish) {
// Pick the first flag set that is defined, in this specific order
const args = [
['--group', flags.group],
['--branch', flags.branch],
].filter(([_, value]) => value)[0];
Log.newLine();
Log.warn(
'The --group and --republish flags are deprecated, use the republish command instead:'
);
Log.warn(` ${chalk.bold([`eas update:republish`, ...(args ?? [])].join(' '))}`);
Log.newLine();
Errors.error('--group and --republish flags are deprecated', { exit: 1 });
}
return {
auto,
branchName,
channelName,
updateMessage,
groupId,
republish,
inputDir: flags['input-dir'],
skipBundler: flags['skip-bundler'],
platform: flags.platform as RequestedPlatform,
+3 -1
View File
@@ -70,7 +70,9 @@ export default class UpdateList extends EasCommand {
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
paginatedQueryOptions:
// discard limit and offset because this query is not those flag's intended target
{
@@ -0,0 +1,377 @@
import { Platform } from '@expo/config';
import { Flags } from '@oclif/core';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { Update } from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { truncateString as truncateUpdateMessage } from '../../update/utils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
const defaultRepublishPlatforms: Platform[] = ['android', 'ios'];
type UpdateRepublishRawFlags = {
branch?: string;
channel?: string;
group?: string;
message?: string;
platform: string;
'non-interactive': boolean;
json?: boolean;
};
type UpdateRepublishFlags = {
branchName?: string;
channelName?: string;
groupId?: string;
updateMessage?: string;
platform: Platform[];
nonInteractive: boolean;
json: boolean;
};
type UpdateToRepublish = {
groupId: string;
branchId: string;
branchName: string;
} & Pick<
Update,
| 'message'
| 'runtimeVersion'
| 'manifestFragment'
| 'platform'
| 'gitCommitHash'
| 'codeSigningInfo'
>;
export default class UpdateRepublish extends EasCommand {
static override description = 'rollback to an existing update';
static override flags = {
channel: Flags.string({
description: 'Channel name to select an update to republish from',
exclusive: ['branch', 'group'],
}),
branch: Flags.string({
description: 'Branch name to select an update to republish from',
exclusive: ['channel', 'group'],
}),
group: Flags.string({
description: 'Update group ID to republish',
exclusive: ['branch', 'channel'],
}),
message: Flags.string({
description: 'Short message describing the republished update',
required: false,
}),
platform: Flags.enum({
char: 'p',
options: [...defaultRepublishPlatforms, 'all'],
default: 'all',
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdateRepublish);
const flags = this.sanitizeFlags(rawFlags);
const {
projectConfig: { exp, projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(UpdateRepublish, {
nonInteractive: flags.nonInteractive,
});
if (flags.json) {
enableJsonOutput();
}
const existingUpdates = await getOrAskUpdatesAsync(graphqlClient, projectId, flags);
const updatesToPublish = existingUpdates.filter(update =>
flags.platform.includes(update.platform as Platform)
);
if (existingUpdates.length === 0) {
throw new Error(`There are no published updates found`);
}
if (updatesToPublish.length === 0) {
throw new Error(
`There are no updates on branch "${
existingUpdates[0].branchName
}" published for the platform(s) "${rawFlags.platform}" with group ID "${
flags.groupId ? flags.groupId : updatesToPublish[0].groupId
}". Did you mean to publish a new update instead?`
);
}
if (rawFlags.platform === 'all') {
Log.withTick(`The republished update will appear only on: ${rawFlags.platform}`);
} else {
const platformsFromUpdates = updatesToPublish.map(update => update.platform);
if (platformsFromUpdates.length < defaultRepublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
Log.withTick(
`The republished update will appear on the same platforms it was originally published on: ${platformsFromUpdates.join(
', '
)}`
);
}
// This command only republishes a single update group
// The update group properties are the same for all updates
const { branchId, branchName, runtimeVersion } = updatesToPublish[0];
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
// If codesigning was created for the original update, we need to add it to the republish
const shouldRepublishWithCodesigning = updatesToPublish.some(update => update.codeSigningInfo);
if (shouldRepublishWithCodesigning) {
Log.withTick(
`The republished update will be signed with the same codesigning as the original update.`
);
}
const publishIndicator = ora('Republishing...').start();
let updatesRepublished: Awaited<ReturnType<typeof PublishMutation.publishUpdateGroupAsync>>;
try {
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
{
branchId,
runtimeVersion,
message: updateMessage,
updateInfoGroup: Object.fromEntries(
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
),
gitCommitHash: updatesToPublish[0].gitCommitHash,
awaitingCodeSigningInfo: shouldRepublishWithCodesigning,
},
]);
if (shouldRepublishWithCodesigning) {
const codeSigningByPlatform = Object.fromEntries(
updatesToPublish.map(update => [update.platform, update.codeSigningInfo])
);
await Promise.all(
updatesRepublished.map(async update => {
const codeSigning = codeSigningByPlatform[update.platform];
if (codeSigning) {
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, update.id, codeSigning);
}
})
);
}
publishIndicator.succeed('Republished update');
} catch (error: any) {
publishIndicator.fail('Failed to republish update');
throw error;
}
if (flags.json) {
return printJsonOnlyOutput(updatesRepublished);
}
const updatesRepublishedByPlatform = Object.fromEntries(
updatesRepublished.map(update => [update.platform, update])
);
const updateGroupUrl = getUpdateGroupUrl(
(await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name,
exp.slug,
updatesRepublished[0].group
);
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: updatesRepublished[0].runtimeVersion },
{ label: 'Platform', value: updatesRepublished.map(update => update.platform).join(', ') },
{ label: 'Update Group ID', value: updatesRepublished[0].id },
...(updatesRepublishedByPlatform.android
? [{ label: 'Android update ID', value: updatesRepublishedByPlatform.android.id }]
: []),
...(updatesRepublishedByPlatform.ios
? [{ label: 'iOS update ID', value: updatesRepublishedByPlatform.ios.id }]
: []),
{ label: 'Message', value: updateMessage },
{ label: 'Website link', value: link(updateGroupUrl, { dim: false }) },
])
);
}
sanitizeFlags(rawFlags: UpdateRepublishRawFlags): UpdateRepublishFlags {
const branchName = rawFlags.branch;
const channelName = rawFlags.channel;
const groupId = rawFlags.group;
const nonInteractive = rawFlags['non-interactive'];
if (nonInteractive && !groupId) {
throw new Error('Only --group can be used in non-interactive mode');
}
if (!groupId && !(branchName || channelName)) {
throw new Error(`--channel, --branch, or --group must be specified`);
}
const platform =
rawFlags.platform === 'all' ? defaultRepublishPlatforms : ([rawFlags.platform] as Platform[]);
return {
branchName,
channelName,
groupId,
platform,
updateMessage: rawFlags.message,
json: rawFlags.json ?? false,
nonInteractive,
};
}
}
/** Retrieve the update group from either the update group id, or select from branch name. */
async function getOrAskUpdatesAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: UpdateRepublishFlags
): Promise<UpdateToRepublish[]> {
if (flags.groupId) {
const updateGroups = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: flags.groupId,
});
return updateGroups.map(group => ({
...group,
groupId: group.group,
branchId: group.branch.id,
branchName: group.branch.name,
}));
}
if (flags.branchName) {
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
branchName: flags.branchName,
projectId,
});
}
if (flags.channelName) {
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...flags,
channelName: flags.channelName,
projectId,
});
}
throw new Error('--channel, --branch, or --group is required');
}
/** Ask the user which update needs to be republished by branch name, this requires interactive mode */
async function askUpdatesFromBranchNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
branchName,
json,
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const updateGroups = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
});
return updateGroups.map(group => ({
...group,
groupId: group.id,
branchId: group.branch.id,
branchName: group.branch.name,
}));
}
/** Ask the user which update needs to be republished by channel name, this requires interactive mode */
async function askUpdatesFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
channelName,
json,
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
projectId,
branchName,
json,
nonInteractive,
});
}
/** Get or ask the user for the update (group) message for the republish */
async function getOrAskUpdateMessageAsync(
updates: UpdateToRepublish[],
flags: UpdateRepublishFlags
): Promise<string> {
if (flags.updateMessage) {
return sanitizeUpdateMessage(flags.updateMessage);
}
if (flags.nonInteractive || flags.json) {
throw new Error('Must supply --message when in non-interactive mode');
}
// This command only uses a single update group to republish, meaning these values are always identical
const oldGroupId = updates[0].groupId;
const oldUpdateMessage = updates[0].message;
const { updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: 'Provide an update message.',
initial: `Republish "${oldUpdateMessage!}" - group: ${oldGroupId}`,
validate: (value: any) => (value ? true : 'Update message may not be empty.'),
});
return sanitizeUpdateMessage(updateMessage);
}
function sanitizeUpdateMessage(updateMessage: string): string {
if (updateMessage !== truncateUpdateMessage(updateMessage, 1024)) {
Log.warn('Update message exceeds the allowed 1024 character limit, truncated update message.');
return truncateUpdateMessage(updateMessage, 1024);
}
return updateMessage;
}
+17 -5
View File
@@ -2,11 +2,13 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { WebhookType } from '../../graphql/generated';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookList extends EasCommand {
@@ -17,6 +19,7 @@ export default class WebhookList extends EasCommand {
description: 'Event type that triggers the webhook',
options: [WebhookType.Build, WebhookType.Submit],
}),
...EasJsonOnlyFlag,
};
static override contextDefinition = {
@@ -26,8 +29,12 @@ export default class WebhookList extends EasCommand {
async runAsync(): Promise<void> {
const {
flags: { event },
flags: { event, json },
} = await this.parse(WebhookList);
if (json) {
enableJsonOutput();
}
const {
projectConfig: { projectId },
loggedIn: { graphqlClient },
@@ -48,10 +55,15 @@ export default class WebhookList extends EasCommand {
spinner.fail(`There are no webhooks on project ${projectDisplayName}`);
} else {
spinner.succeed(`Found ${webhooks.length} webhooks on project ${projectDisplayName}`);
const list = webhooks
.map(webhook => formatWebhook(webhook))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
if (json) {
printJsonOnlyOutput(webhooks);
} else {
const list = webhooks
.map(webhook => formatWebhook(webhook))
.join(`\n\n${chalk.dim('———')}\n\n`);
Log.log(`\n${list}`);
}
}
} catch (err) {
spinner.fail(
@@ -1,9 +1,10 @@
import chalk from 'chalk';
import terminalLink from 'terminal-link';
// @ts-ignore
import TurndownService from 'turndown';
import wrapAnsi from 'wrap-ansi';
import { link } from '../../../log';
const turndownServices: Record<string, any> = {};
function getService(rootUrl: string): TurndownService {
@@ -44,10 +45,7 @@ function getService(rootUrl: string): TurndownService {
if (href.startsWith('/')) {
href = `${rootUrl}${href}`;
}
if (terminalLink.isSupported) {
return chalk.cyan(terminalLink(content, href));
}
return `${chalk.cyan(content)} (${chalk.underline(href)})`;
return chalk.cyan(link(href, { text: content }));
},
});
@@ -1,4 +1,5 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../../log';
import { pressAnyKeyToContinueAsync } from '../../prompts';
@@ -10,6 +11,7 @@ export interface Action<T = void> {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
projectInfo: CredentialsContextProjectInfo | null;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
runAsync(ctx: CredentialsContext): Promise<T>;
}
@@ -27,7 +27,7 @@ import {
displayAndroidAppCredentials,
displayEmptyAndroidCredentials,
} from '../android/utils/printCredentials';
import { CredentialsContext } from '../context';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
@@ -52,9 +52,16 @@ export class ManageAndroid {
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
: null;
let projectInfo: CredentialsContextProjectInfo | null = null;
if (hasProjectContext) {
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
env: buildProfile?.env,
});
projectInfo = { exp, projectId };
}
const ctx = new CredentialsContext({
projectDir: process.cwd(),
projectInfo: this.callingAction.projectInfo,
projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
analytics: this.callingAction.analytics,
@@ -15,7 +15,7 @@ import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { ensureActorHasPrimaryAccount } from '../../user/actions';
import { CredentialsContext } from '../context';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
import {
AppStoreApiKeyPurpose,
selectAscApiKeysFromAccountAsync,
@@ -61,9 +61,18 @@ export class ManageIos {
const buildProfile = this.callingAction.projectInfo
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
: null;
let projectInfo: CredentialsContextProjectInfo | null = null;
if (this.callingAction.projectInfo) {
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
env: buildProfile?.env,
});
projectInfo = { exp, projectId };
}
const ctx = new CredentialsContext({
projectDir: process.cwd(),
projectInfo: this.callingAction.projectInfo,
projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
analytics: this.callingAction.analytics,
@@ -1,4 +1,5 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectPlatformAsync } from '../../platform';
import { Actor } from '../../user/User';
@@ -12,6 +13,7 @@ export class SelectPlatform {
public readonly graphqlClient: ExpoGraphqlClient,
public readonly analytics: Analytics,
public readonly projectInfo: CredentialsContextProjectInfo | null,
public readonly getDynamicProjectConfigAsync: DynamicConfigContextFn,
private readonly flagPlatform?: string
) {}
+6 -3
View File
@@ -44,10 +44,11 @@ export async function selectAppleTeamOnAccountAsync(
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleTeam =>
appleTeam.appleTeamName
makePartialChoiceObject: appleTeam => ({
title: appleTeam.appleTeamName
? `${appleTeam.appleTeamName} (ID: ${appleTeam.appleTeamIdentifier})`
: appleTeam.appleTeamIdentifier,
}),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
@@ -87,7 +88,9 @@ export async function selectAppleDeviceOnAppleTeamAsync(
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleDevice => formatDeviceLabel(appleDevice),
makePartialChoiceObject: appleDevice => ({
title: formatDeviceLabel(appleDevice),
}),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
@@ -23,13 +23,12 @@ function formatDeviceClass(device: Device | NewDevice): string {
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
const fields: FormatFieldsItem[] = [
{ label: 'ID', value: device.id },
{ label: 'UDID', value: device.identifier },
{ label: 'Name', value: device.name ?? 'Unknown' },
{
label: 'Class',
value: formatDeviceClass(device),
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
+125 -21
View File
@@ -400,7 +400,6 @@ export type AccountSsoConfiguration = {
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
@@ -415,7 +414,6 @@ export type AccountSsoConfigurationData = {
clientSecret: Scalars['String'];
issuer?: InputMaybe<Scalars['String']>;
jwksEndpoint?: InputMaybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: InputMaybe<Scalars['String']>;
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
@@ -458,7 +456,6 @@ export type AccountSsoConfigurationPublicData = {
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
userInfoEndpoint?: Maybe<Scalars['String']>;
@@ -787,6 +784,7 @@ export type AndroidJobInput = {
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
mode?: InputMaybe<BuildMode>;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: InputMaybe<Scalars['String']>;
@@ -815,6 +813,7 @@ export type AndroidJobOverridesInput = {
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
mode?: InputMaybe<BuildMode>;
releaseChannel?: InputMaybe<Scalars['String']>;
secrets?: InputMaybe<AndroidJobSecretsInput>;
updates?: InputMaybe<BuildUpdatesInput>;
@@ -922,6 +921,7 @@ export type App = Project & {
/** Classic update release channel names that have at least one build */
buildsReleaseChannels: Array<Scalars['String']>;
deployment?: Maybe<Deployment>;
deploymentNew?: Maybe<DeploymentNew>;
/** Deployments associated with this app */
deployments: Array<Deployment>;
description: Scalars['String'];
@@ -1051,6 +1051,13 @@ export type AppDeploymentArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentNewArgs = {
channel: Scalars['String'];
runtimeVersion: Scalars['String'];
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: Scalars['Int'];
@@ -1800,6 +1807,12 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
workerStartedAt?: Maybe<Scalars['DateTime']>;
};
/** Represents an EAS Build */
export type BuildCanRetryArgs = {
newMode?: InputMaybe<BuildMode>;
};
export type BuildArtifact = {
__typename?: 'BuildArtifact';
manifestPlistUrl?: Maybe<Scalars['String']>;
@@ -1960,6 +1973,11 @@ export type BuildMetrics = {
buildWaitTime?: Maybe<Scalars['Int']>;
};
export enum BuildMode {
Build = 'BUILD',
Resign = 'RESIGN'
}
export type BuildMutation = {
__typename?: 'BuildMutation';
/**
@@ -2116,6 +2134,10 @@ export type BuildQueryByIdArgs = {
buildId: Scalars['ID'];
};
export type BuildResignInput = {
applicationArchiveSource?: InputMaybe<ProjectArchiveSourceInput>;
};
export enum BuildResourceClass {
AndroidDefault = 'ANDROID_DEFAULT',
AndroidLarge = 'ANDROID_LARGE',
@@ -2141,7 +2163,8 @@ export type BuildUpdatesInput = {
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
Managed = 'MANAGED',
Unknown = 'UNKNOWN'
}
export enum CacheControlScope {
@@ -2381,6 +2404,36 @@ export type Deployment = {
runtimeVersion: Scalars['String'];
};
export type DeploymentBuildEdge = {
__typename?: 'DeploymentBuildEdge';
cursor: Scalars['String'];
node: Build;
};
export type DeploymentBuildsConnection = {
__typename?: 'DeploymentBuildsConnection';
edges: Array<DeploymentBuildEdge>;
pageInfo: PageInfo;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNew = {
__typename?: 'DeploymentNew';
builds: DeploymentBuildsConnection;
channel: UpdateChannel;
id: Scalars['ID'];
runtime: Runtime;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNewBuildsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type DeploymentOptions = {
/** Max number of associated builds to return */
buildListMaxSize?: InputMaybe<Scalars['Int']>;
@@ -2523,6 +2576,12 @@ export type GetSignedAssetUploadSpecificationsResult = {
specifications: Array<Scalars['String']>;
};
export enum GitHubAppEnvironment {
Development = 'DEVELOPMENT',
Production = 'PRODUCTION',
Staging = 'STAGING'
}
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
accessibleRepositories: GitHubRepositoryPaginationResult;
@@ -2541,7 +2600,7 @@ export type GitHubAppInstallationAccessibleRepositoriesArgs = {
export type GitHubAppInstallationAccessibleRepository = {
__typename?: 'GitHubAppInstallationAccessibleRepository';
defaultBranch: Scalars['String'];
defaultBranch?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
id: Scalars['Int'];
name: Scalars['String'];
@@ -2586,7 +2645,9 @@ export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
environment: GitHubAppEnvironment;
installation: GitHubAppInstallation;
name: Scalars['String'];
searchRepositories: GitHubRepositoryPaginationResult;
};
@@ -2613,12 +2674,14 @@ export type GitHubRepository = {
export type GitHubRepositoryMetadata = {
__typename?: 'GitHubRepositoryMetadata';
defaultBranch?: Maybe<Scalars['String']>;
githubRepoDescription?: Maybe<Scalars['String']>;
githubRepoName: Scalars['String'];
githubRepoOwnerName: Scalars['String'];
githubRepoUrl: Scalars['String'];
lastPushed: Scalars['DateTime'];
lastUpdated: Scalars['DateTime'];
openGraphImageUrl?: Maybe<Scalars['String']>;
private: Scalars['Boolean'];
};
@@ -2972,6 +3035,7 @@ export type IosJobInput = {
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
experimental?: InputMaybe<Scalars['JSONObject']>;
mode?: InputMaybe<BuildMode>;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: InputMaybe<Scalars['String']>;
@@ -2998,10 +3062,13 @@ export type IosJobOverridesInput = {
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
experimental?: InputMaybe<Scalars['JSONObject']>;
mode?: InputMaybe<BuildMode>;
releaseChannel?: InputMaybe<Scalars['String']>;
resign?: InputMaybe<BuildResignInput>;
scheme?: InputMaybe<Scalars['String']>;
secrets?: InputMaybe<IosJobSecretsInput>;
simulator?: InputMaybe<Scalars['Boolean']>;
type?: InputMaybe<BuildWorkflow>;
updates?: InputMaybe<BuildUpdatesInput>;
username?: InputMaybe<Scalars['String']>;
version?: InputMaybe<IosJobVersionInput>;
@@ -3239,6 +3306,14 @@ export enum Order {
Desc = 'DESC'
}
export type PageInfo = {
__typename?: 'PageInfo';
endCursor?: Maybe<Scalars['String']>;
hasNextPage: Scalars['Boolean'];
hasPreviousPage: Scalars['Boolean'];
startCursor?: Maybe<Scalars['String']>;
};
export type PartialManifest = {
assets: Array<InputMaybe<PartialManifestAsset>>;
extra?: InputMaybe<Scalars['JSONObject']>;
@@ -3287,6 +3362,8 @@ export type ProjectArchiveSourceInput = {
};
export enum ProjectArchiveSourceType {
Gcs = 'GCS',
None = 'NONE',
S3 = 'S3',
Url = 'URL'
}
@@ -3336,7 +3413,9 @@ export type PublicArtifacts = {
export type PublishUpdateGroupInput = {
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']>;
branchId: Scalars['String'];
excludedAssets?: InputMaybe<Array<PartialManifestAsset>>;
gitCommitHash?: InputMaybe<Scalars['String']>;
isGitWorkingTreeDirty?: InputMaybe<Scalars['Boolean']>;
message?: InputMaybe<Scalars['String']>;
runtimeVersion: Scalars['String'];
updateInfoGroup: UpdateInfoGroup;
@@ -3619,6 +3698,14 @@ export type RootQueryUserByUsernameArgs = {
username: Scalars['String'];
};
export type Runtime = {
__typename?: 'Runtime';
app: App;
firstBuildCreatedAt: Scalars['DateTime'];
id: Scalars['ID'];
version: Scalars['String'];
};
export type SecondFactorBooleanResult = {
__typename?: 'SecondFactorBooleanResult';
success: Scalars['Boolean'];
@@ -3996,6 +4083,7 @@ export type Update = ActivityTimelineProjectActivity & {
gitCommitHash?: Maybe<Scalars['String']>;
group: Scalars['String'];
id: Scalars['ID'];
isGitWorkingTreeDirty: Scalars['Boolean'];
manifestFragment: Scalars['String'];
manifestPermalink: Scalars['String'];
message?: Maybe<Scalars['String']>;
@@ -4320,11 +4408,15 @@ export type UserDataInput = {
export type UserInvitation = {
__typename?: 'UserInvitation';
accountName: Scalars['String'];
/** If the invite is for a personal team, the profile photo of account owner */
accountProfilePhoto?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
/** Email to which this invitation was sent */
email: Scalars['String'];
expires: Scalars['DateTime'];
id: Scalars['ID'];
/** If the invite is for an organization or a personal team */
isForOrganization: Scalars['Boolean'];
/** Account permissions to be granted upon acceptance of this invitation */
permissions: Array<Permission>;
/** Role to be granted upon acceptance of this invitation */
@@ -4393,11 +4485,13 @@ export type UserInvitationMutationResendUserInvitationArgs = {
export type UserInvitationPublicData = {
__typename?: 'UserInvitationPublicData';
accountName: Scalars['String'];
accountProfilePhoto?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
email: Scalars['String'];
expires: Scalars['DateTime'];
/** Email to which this invitation was sent */
id: Scalars['ID'];
isForOrganization: Scalars['Boolean'];
};
export type UserInvitationPublicDataQuery = {
@@ -5006,7 +5100,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
@@ -5016,7 +5110,15 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type RetryIosBuildMutationVariables = Exact<{
buildId: Scalars['ID'];
jobOverrides: IosJobOverridesInput;
}>;
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -5072,6 +5174,7 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5081,6 +5184,7 @@ export type CreateIosSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5154,7 +5258,7 @@ export type BranchesByAppQueryVariables = Exact<{
}>;
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }> }> } } };
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }> } } };
export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5164,21 +5268,21 @@ export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
}>;
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> }> } | null } } };
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
export type BuildsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5188,7 +5292,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
}>;
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5196,7 +5300,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> }> } | null } } };
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5205,7 +5309,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> }> }> } } };
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> }> } } };
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5258,7 +5362,7 @@ export type ViewUpdatesByGroupQueryVariables = Exact<{
}>;
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }> };
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5269,7 +5373,7 @@ export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
}>;
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> } | null } } };
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } | null } } };
export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5279,7 +5383,7 @@ export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> } } };
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } } };
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5305,9 +5409,9 @@ export type AccountFragment = { __typename?: 'Account', id: string, name: string
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -5315,9 +5419,9 @@ export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id:
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null };
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }> };
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
export type WebhookFragment = { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any };
@@ -15,6 +15,9 @@ import {
CreateIosBuildMutationVariables,
EasBuildDeprecationInfo,
IosJobInput,
IosJobOverridesInput,
RetryIosBuildMutation,
RetryIosBuildMutationVariables,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
@@ -116,4 +119,32 @@ export const BuildMutation = {
);
return nullthrows(data.build?.createIosBuild);
},
async retryIosBuildAsync(
graphqlClient: ExpoGraphqlClient,
input: {
buildId: string;
jobOverrides: IosJobOverridesInput;
}
): Promise<BuildFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<RetryIosBuildMutation, RetryIosBuildMutationVariables>(
gql`
mutation RetryIosBuildMutation($buildId: ID!, $jobOverrides: IosJobOverridesInput!) {
build {
retryIosBuild(buildId: $buildId, jobOverrides: $jobOverrides) {
id
...BuildFragment
}
}
}
${print(BuildFragmentNode)}
`,
input,
{ noRetry: true }
)
.toPromise()
);
return nullthrows(data.build?.retryIosBuild);
},
};
@@ -28,10 +28,16 @@ export const SubmissionMutation = {
$appId: ID!
$config: AndroidSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createAndroidSubmission(
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
) {
submission {
id
@@ -60,10 +66,16 @@ export const SubmissionMutation = {
$appId: ID!
$config: IosSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createIosSubmission(
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
) {
submission {
id
@@ -8,16 +8,17 @@ import {
UploadSessionType,
} from '../generated';
export interface PresignedPost {
export interface SignedUrl {
url: string;
fields: Record<string, string>;
headers: Record<string, string>;
bucketKey: string;
}
export const UploadSessionMutation = {
async createUploadSessionAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType
): Promise<PresignedPost> {
): Promise<SignedUrl> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
@@ -1,6 +1,7 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ChannelNotFoundError } from '../../channel/errors';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
@@ -59,7 +60,7 @@ export const ChannelQuery = {
const { updateChannelByName } = response.app.byId;
if (!updateChannelByName) {
throw new Error(`Could not find channel with the name ${channelName}`);
throw new ChannelNotFoundError(`Could not find channel with the name ${channelName}`);
}
return updateChannelByName;
@@ -52,6 +52,7 @@ export const BuildFragmentNode = gql`
priority
createdAt
updatedAt
completedAt
}
`;
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
runtimeVersion
platform
manifestFragment
gitCommitHash
actor {
__typename
id
@@ -23,5 +24,10 @@ export const UpdateFragmentNode = gql`
id
name
}
codeSigningInfo {
keyid
sig
alg
}
}
`;
+3 -6
View File
@@ -92,13 +92,10 @@ export function link(
url: string,
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
): string {
let output: string;
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
if (terminalLink.isSupported) {
output = terminalLink(text, url);
} else {
output = `${text === url ? '' : text + ': '}${chalk.underline(url)}`;
}
const output = terminalLink(text, url, {
fallback: () => (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
});
return dim ? chalk.dim(output) : output;
}
@@ -8,7 +8,10 @@ import {
getApplicationIdAsync,
} from './android/applicationId';
import { resolveGradleBuildContextAsync } from './android/gradle';
import { getBundleIdentifierAsync } from './ios/bundleIdentifier';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
getBundleIdentifierAsync,
} from './ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from './ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from './ios/target';
import { resolveWorkflowAsync } from './workflow';
@@ -23,15 +26,20 @@ export async function getApplicationIdentifierAsync(
if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
if (workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
return await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
}
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
return await getApplicationIdAsync(projectDir, exp, gradleContext);
} else {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const profile = buildProfile as BuildProfile<Platform.IOS>;
if (workflow === Workflow.MANAGED) {
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync(projectDir, exp, actor);
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{ exp, projectDir, nonInteractive: false },
profile
@@ -1,11 +1,11 @@
import chalk from 'chalk';
import terminalLink from 'terminal-link';
import { getProjectDashboardUrl } from '../build/utils/url';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPrivacy, Role } from '../graphql/generated';
import { AppMutation } from '../graphql/mutations/AppMutation';
import { AppQuery } from '../graphql/queries/AppQuery';
import { link } from '../log';
import { ora } from '../ora';
import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
@@ -83,10 +83,7 @@ export async function fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsyn
}
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
const projectLink = terminalLink(projectFullName, projectDashboardUrl, {
// https://github.com/sindresorhus/terminal-link/issues/18#issuecomment-1068020361
fallback: () => `${projectFullName} (${projectDashboardUrl})`,
});
const projectLink = link(projectDashboardUrl, { text: projectFullName });
const spinner = ora(`Creating ${chalk.bold(projectFullName)} on Expo`).start();
try {
+2 -2
View File
@@ -10,10 +10,9 @@ import promiseLimit from 'promise-limit';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log from '../log';
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import uniqBy from '../utils/expodash/uniqBy';
@@ -32,6 +31,7 @@ export type RawAsset = {
contentType: string;
path: string;
};
type CollectedAssets = {
[platform in Platform]?: {
launchAsset: RawAsset;
+10 -3
View File
@@ -14,7 +14,10 @@ export async function promptAsync<T extends string = string>(
options: Options = {}
): Promise<Answers<T>> {
if (!process.stdin.isTTY && !global.test) {
throw new Error('Input is required, but stdin is not readable.');
const message = Array.isArray(questions) ? questions[0]?.message : questions.message;
throw new Error(
`Input is required, but stdin is not readable. Failed to display prompt: ${message}`
);
}
return await prompts<T>(questions, {
onCancel() {
@@ -42,7 +45,10 @@ export async function confirmAsync(
export async function selectAsync<T>(
message: string,
choices: ExpoChoice<T>[],
options?: Options
config?: {
options?: Options;
warningMessageForDisabledEntries?: string;
}
): Promise<T> {
const { value } = await promptAsync(
{
@@ -50,8 +56,9 @@ export async function selectAsync<T>(
choices,
name: 'value',
type: 'select',
warn: config?.warningMessageForDisabledEntries,
},
options
config?.options ?? {}
);
return value ?? null;
}
+57
View File
@@ -0,0 +1,57 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import glob from 'fast-glob';
import path from 'path';
import Log from '../../log';
import { getAndroidSdkRootAsync } from './sdk';
async function aaptAsync(...options: string[]): Promise<SpawnResult> {
try {
return await spawnAsync(await getAaptExecutableAsync(), options);
} catch (error: any) {
if (error.stderr) {
Log.error(error.stderr);
}
throw error;
}
}
export async function getAaptExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (!sdkRoot) {
Log.debug('Failed to resolve the Android SDK path, falling back to global aapt executable');
return 'aapt';
}
const aaptPaths = await glob(
path.posix.join('build-tools/**', process.platform === 'win32' ? 'aapt.exe' : 'aapt'),
{ cwd: sdkRoot, absolute: true }
);
if (aaptPaths.length === 0) {
throw new Error('Failed to resolve the Android aapt path');
}
const sorted = aaptPaths.sort();
return sorted[sorted.length - 1];
}
export async function getAptParametersAsync(
appPath: string
): Promise<{ packageName: string; activityName: string }> {
const { stdout } = await aaptAsync('dump', 'badging', appPath);
const packageNameMatch = stdout.match(/package: name='([^']+)'/);
if (!packageNameMatch) {
throw new Error(`Could not read package name from ${appPath}`);
}
// get activity name
const activityNameMatch = stdout.match(/launchable-activity: name='([^']+)'/);
if (!activityNameMatch) {
throw new Error(`Could not read activity name from ${appPath}`);
}
return {
packageName: packageNameMatch[1],
activityName: activityNameMatch[1],
};
}
+132
View File
@@ -0,0 +1,132 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import os from 'os';
import path from 'path';
import Log from '../../log';
import { truthy } from '../../utils/expodash/filter';
import { sleepAsync } from '../../utils/promise';
import { getAndroidSdkRootAsync } from './sdk';
export interface AndroidEmulator {
pid?: string;
name: string;
}
const BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: ';
export async function adbAsync(...args: string[]): Promise<SpawnResult> {
const adbExecutable = await getAdbExecutableAsync();
try {
return await spawnAsync(adbExecutable, args);
} catch (error: any) {
let errorMessage = (error.stderr || error.stdout || error.message).trim();
if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) {
errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length);
}
error.message = errorMessage;
throw error;
}
}
export async function getAdbExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (!sdkRoot) {
Log.debug('Failed to resolve the Android SDK path, falling back to global adb executable');
return 'adb';
}
return path.join(sdkRoot, 'platform-tools/adb');
}
export function sanitizeAdbDeviceName(deviceName: string): string | undefined {
return deviceName.trim().split(/[\r\n]+/)[0];
}
/**
* Return the Emulator name for an emulator ID, this can be used to determine if an emulator is booted.
*
* @param devicePid a value like `emulator-5554` from `abd devices`
*/
export async function getAdbNameForDeviceIdAsync(emulatorPid: string): Promise<string | null> {
const { stdout } = await adbAsync('-s', emulatorPid, 'emu', 'avd', 'name');
if (stdout.match(/could not connect to TCP port .*: Connection refused/)) {
// Can also occur when the emulator does not exist.
throw new Error(`Emulator not found: ${stdout}`);
}
return sanitizeAdbDeviceName(stdout) ?? null;
}
// TODO: This is very expensive for some operations.
export async function getRunningEmulatorsAsync(): Promise<AndroidEmulator[]> {
const { stdout } = await adbAsync('devices', '-l');
const splitItems = stdout.trim().split(os.EOL);
const attachedDevices: {
pid: string;
type: string;
}[] = splitItems
// First line is `"List of devices attached"`, remove it
.slice(1, splitItems.length)
.map(line => {
// unauthorized: ['FA8251A00719', 'unauthorized', 'usb:338690048X', 'transport_id:5']
// authorized: ['FA8251A00719', 'device', 'usb:336592896X', 'product:walleye', 'model:Pixel_2', 'device:walleye', 'transport_id:4']
// emulator: ['emulator-5554', 'offline', 'transport_id:1']
const [pid] = line.split(' ').filter(truthy);
const type = line.includes('emulator') ? 'emulator' : 'device';
return { pid, type };
})
.filter(({ pid, type }) => !!pid && type === 'emulator');
const devicePromises = attachedDevices.map<Promise<AndroidEmulator>>(async ({ pid }) => {
const name = (await getAdbNameForDeviceIdAsync(pid)) ?? '';
return {
pid,
name,
};
});
return Promise.all(devicePromises);
}
export async function getFirstRunningEmulatorAsync(): Promise<AndroidEmulator | null> {
const emulators = await getRunningEmulatorsAsync();
return emulators[0] ?? null;
}
/**
* Returns true if emulator is booted
*
* @param emulatorPid
*/
export async function isEmulatorBootedAsync(emulatorPid: string): Promise<boolean> {
try {
const { stdout } = await adbAsync('-s', emulatorPid, 'shell', 'getprop', 'sys.boot_completed');
if (stdout.trim() === '1') {
return true;
}
return false;
} catch {
return false;
}
}
export async function waitForEmulatorToBeBootedAsync(
maxWaitTimeMs: number,
intervalMs: number
): Promise<AndroidEmulator> {
Log.newLine();
Log.log('Waiting for the Android emulator to start...');
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTimeMs) {
const emulator = await getFirstRunningEmulatorAsync();
if (emulator?.pid && (await isEmulatorBootedAsync(emulator.pid))) {
return emulator;
}
await sleepAsync(intervalMs);
}
throw new Error('Timed out waiting for the Android emulator to start.');
}
@@ -0,0 +1,160 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import assert from 'assert';
import chalk from 'chalk';
import os from 'os';
import path from 'path';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { truthy } from '../../utils/expodash/filter';
import {
AndroidEmulator,
adbAsync,
getFirstRunningEmulatorAsync,
isEmulatorBootedAsync,
waitForEmulatorToBeBootedAsync,
} from './adb';
import { getAndroidSdkRootAsync } from './sdk';
export const EMULATOR_MAX_WAIT_TIMEOUT_MS = 60 * 1000 * 3;
export async function getEmulatorExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (sdkRoot) {
return path.join(sdkRoot, 'emulator', 'emulator');
}
return 'emulator';
}
async function emulatorAsync(...options: string[]): Promise<SpawnResult> {
const emulatorExecutable = await getEmulatorExecutableAsync();
try {
return await spawnAsync(emulatorExecutable, options);
} catch (error: any) {
if (error.stderr) {
Log.error(error.stderr);
}
throw error;
}
}
async function getAvaliableAndroidEmulatorsAsync(): Promise<AndroidEmulator[]> {
try {
const { stdout } = await emulatorAsync('-list-avds');
return stdout
.split(os.EOL)
.filter(truthy)
.map(name => ({
name,
}));
} catch {
return [];
}
}
/** Start an Android device and wait until it is booted. */
async function bootEmulatorAsync(
emulator: AndroidEmulator,
{
timeout = EMULATOR_MAX_WAIT_TIMEOUT_MS,
interval = 1000,
}: {
/** Time in milliseconds to wait before asserting a timeout error. */
timeout?: number;
interval?: number;
} = {}
): Promise<AndroidEmulator> {
Log.newLine();
Log.log(`Opening emulator ${chalk.bold(emulator.name)}`);
const emulatorExecutable = await getEmulatorExecutableAsync();
// Start a process to open an emulator
const emulatorProcess = spawnAsync(emulatorExecutable, [`@${emulator.name}`], {
stdio: 'ignore',
detached: true,
});
// we don't want to wait for the emulator process to exit before we can finish `eas build:run` command
// https://github.com/expo/eas-cli/pull/1485#discussion_r1007935871
emulatorProcess.child.unref();
return await waitForEmulatorToBeBootedAsync(timeout, interval);
}
export async function selectEmulatorAsync(): Promise<AndroidEmulator> {
const runningEmulator = await getFirstRunningEmulatorAsync();
if (runningEmulator) {
Log.newLine();
Log.log(`Using open emulator: ${chalk.bold(runningEmulator.name)}`);
return runningEmulator;
}
const emulators = await getAvaliableAndroidEmulatorsAsync();
Log.newLine();
const { selectedEmulator } = await promptAsync({
type: 'select',
message: `Select an emulator to run your app on`,
name: 'selectedEmulator',
choices: emulators.map(emulator => ({
title: emulator.name,
value: emulator,
})),
});
return selectedEmulator;
}
export async function ensureEmulatorBootedAsync(
emulator: AndroidEmulator
): Promise<AndroidEmulator> {
if (!emulator.pid || !(await isEmulatorBootedAsync(emulator.pid))) {
return await bootEmulatorAsync(emulator);
}
return emulator;
}
export async function installAppAsync(
emulator: AndroidEmulator,
apkFilePath: string
): Promise<void> {
Log.newLine();
Log.log('Installing your app...');
assert(emulator.pid);
await adbAsync('-s', emulator.pid, 'install', '-r', '-d', apkFilePath);
Log.succeed('Successfully installed your app!');
}
export async function startAppAsync(
emulator: AndroidEmulator,
packageName: string,
activityName: string
): Promise<void> {
Log.newLine();
Log.log('Starting your app...');
assert(emulator.pid);
await adbAsync(
'-s',
emulator.pid,
'shell',
'am',
'start',
'-a',
'android.intent.action.MAIN',
'-f',
'0x20000000', // FLAG_ACTIVITY_SINGLE_TOP -- If set, the activity will not be launched if it is already running at the top of the history stack.
'-n',
`${packageName}/${activityName}`
);
Log.succeed('Successfully started your app!');
}
+10 -3
View File
@@ -1,5 +1,12 @@
import Log from '../../log';
import { getAptParametersAsync } from './aapt';
import * as emulator from './emulator';
import { assertExecutablesExistAsync } from './systemRequirements';
export async function runAppOnAndroidEmulatorAsync(_appPath: string): Promise<void> {
Log.error('Not implemented!');
export async function runAppOnAndroidEmulatorAsync(appPath: string): Promise<void> {
await assertExecutablesExistAsync();
const selectedEmulator = await emulator.selectEmulatorAsync();
const bootedEmulator = await emulator.ensureEmulatorBootedAsync(selectedEmulator);
await emulator.installAppAsync(bootedEmulator, appPath);
const { packageName, activityName } = await getAptParametersAsync(appPath);
await emulator.startAppAsync(bootedEmulator, packageName, activityName);
}
+26
View File
@@ -0,0 +1,26 @@
import { pathExists } from 'fs-extra';
import os from 'os';
import path from 'path';
export const ANDROID_DEFAULT_LOCATION: Readonly<Partial<Record<NodeJS.Platform, string>>> = {
darwin: path.join(os.homedir(), 'Library', 'Android', 'sdk'),
linux: path.join(os.homedir(), 'Android', 'Sdk'),
win32: path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
};
const ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM = ANDROID_DEFAULT_LOCATION[process.platform];
export async function getAndroidSdkRootAsync(): Promise<string | null> {
if (process.env.ANDROID_HOME && (await pathExists(process.env.ANDROID_HOME))) {
return process.env.ANDROID_HOME;
} else if (process.env.ANDROID_SDK_ROOT && (await pathExists(process.env.ANDROID_SDK_ROOT))) {
return process.env.ANDROID_SDK_ROOT;
} else if (
ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM &&
(await pathExists(ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM))
) {
return ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM;
} else {
return null;
}
}
@@ -0,0 +1,27 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { getAaptExecutableAsync } from './aapt';
import { getAdbExecutableAsync } from './adb';
import { getEmulatorExecutableAsync } from './emulator';
async function assertExecutableExistsAsync(executable: string, options?: string[]): Promise<void> {
try {
await spawnAsync(executable, options);
} catch (err: any) {
throw new Error(
`${chalk.bold(
executable
)} executable doesn't seem to work. Please make sure Android Studio is installed on your device and ${chalk.bold(
'ANDROID_HOME'
)} or ${chalk.bold('ANDROID_SDK_ROOT')} env variables are set.
${err.message}`
);
}
}
export async function assertExecutablesExistAsync(): Promise<void> {
await assertExecutableExistsAsync(await getAdbExecutableAsync(), ['--version']);
await assertExecutableExistsAsync(await getEmulatorExecutableAsync(), ['-list-avds']);
await assertExecutableExistsAsync(await getAaptExecutableAsync(), ['version']);
}
+1 -1
View File
@@ -33,7 +33,7 @@ export async function selectSimulatorAsync(): Promise<IosSimulator> {
message: `Select a simulator to run your app on`,
name: 'selectedSimulator',
choices: simulators.map(simulator => ({
title: `ios ${simulator.osVersion} ${simulator.name}`,
title: `iOS ${simulator.osVersion} ${simulator.name}`,
value: simulator,
})),
});
+19
View File
@@ -0,0 +1,19 @@
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../graphql/generated';
function isAab(build: BuildFragment): boolean {
return build.artifacts?.applicationArchiveUrl?.endsWith('.aab') ?? false;
}
function didArtifactsExpire(build: BuildFragment): boolean {
return new Date().getTime() - new Date(build.completedAt).getTime() > 30 * 24 * 60 * 60 * 1000; // 30 days
}
export function isRunnableOnSimulatorOrEmulator(build: BuildFragment): boolean {
return (
build.status === BuildStatus.Finished &&
!!build.artifacts?.applicationArchiveUrl &&
((build.platform === AppPlatform.Ios && build.distribution === DistributionType.Simulator) ||
(build.platform === AppPlatform.Android && !isAab(build))) &&
!didArtifactsExpire(build)
);
}
+95 -86
View File
@@ -22,15 +22,21 @@ export enum ArchiveSourceType {
latest,
path,
buildId,
build,
buildList,
prompt,
gcs,
}
export interface ArchiveResolverContext {
platform: Platform;
projectId: string;
nonInteractive: boolean;
graphqlClient: ExpoGraphqlClient;
}
interface ArchiveSourceBase {
sourceType: ArchiveSourceType;
platform: Platform;
projectId: string;
nonInteractive: boolean;
}
interface ArchiveUrlSource extends ArchiveSourceBase {
@@ -52,6 +58,11 @@ interface ArchiveBuildIdSource extends ArchiveSourceBase {
id: string;
}
interface ArchiveBuildSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.build;
build: BuildFragment;
}
interface ArchiveBuildListSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.buildList;
}
@@ -60,10 +71,10 @@ interface ArchivePromptSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.prompt;
}
export interface Archive {
build?: BuildFragment;
source: ArchiveSource;
url?: string;
interface ArchiveGCSSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.gcs;
bucketKey: string;
localSource: ArchivePathSource;
}
export type ArchiveSource =
@@ -71,54 +82,62 @@ export type ArchiveSource =
| ArchiveLatestSource
| ArchivePathSource
| ArchiveBuildIdSource
| ArchiveBuildSource
| ArchiveBuildListSource
| ArchivePromptSource;
| ArchivePromptSource
| ArchiveGCSSource;
export type ResolvedArchiveSource = ArchiveUrlSource | ArchiveGCSSource | ArchiveBuildSource;
export async function getArchiveAsync(
graphqlClient: ExpoGraphqlClient,
ctx: ArchiveResolverContext,
source: ArchiveSource
): Promise<Archive> {
): Promise<ResolvedArchiveSource> {
switch (source.sourceType) {
case ArchiveSourceType.prompt: {
return await handlePromptSourceAsync(graphqlClient, source);
return await handlePromptSourceAsync(ctx);
}
case ArchiveSourceType.url: {
return await handleUrlSourceAsync(graphqlClient, source);
return await handleUrlSourceAsync(ctx, source);
}
case ArchiveSourceType.latest: {
return await handleLatestSourceAsync(graphqlClient, source);
return await handleLatestSourceAsync(ctx);
}
case ArchiveSourceType.path: {
return await handlePathSourceAsync(graphqlClient, source);
return await handlePathSourceAsync(ctx, source);
}
case ArchiveSourceType.buildId: {
return await handleBuildIdSourceAsync(graphqlClient, source);
return await handleBuildIdSourceAsync(ctx, source);
}
case ArchiveSourceType.buildList: {
return await handleBuildListSourceAsync(graphqlClient, source);
return await handleBuildListSourceAsync(ctx);
}
case ArchiveSourceType.gcs: {
return source;
}
case ArchiveSourceType.build: {
return source;
}
}
}
async function handleUrlSourceAsync(
graphqlClient: ExpoGraphqlClient,
ctx: ArchiveResolverContext,
source: ArchiveUrlSource
): Promise<Archive> {
): Promise<ResolvedArchiveSource> {
const { url } = source;
if (!validateUrl(url)) {
Log.error(chalk.bold(`The URL you provided is invalid: ${url}`));
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
const maybeBuildId = isBuildDetailsPage(url);
if (maybeBuildId) {
if (await askIfUseBuildIdFromUrlAsync(source, maybeBuildId)) {
return getArchiveAsync(graphqlClient, {
...source,
if (await askIfUseBuildIdFromUrlAsync(ctx, source, maybeBuildId)) {
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.buildId,
id: maybeBuildId,
});
@@ -126,20 +145,19 @@ async function handleUrlSourceAsync(
}
return {
sourceType: ArchiveSourceType.url,
url,
source,
};
}
async function handleLatestSourceAsync(
graphqlClient: ExpoGraphqlClient,
source: ArchiveLatestSource
): Promise<Archive> {
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
try {
const [latestBuild] = await getRecentBuildsForSubmissionAsync(
graphqlClient,
toAppPlatform(source.platform),
source.projectId
ctx.graphqlClient,
toAppPlatform(ctx.platform),
ctx.projectId
);
if (!latestBuild) {
@@ -148,15 +166,14 @@ async function handleLatestSourceAsync(
"Couldn't find any builds for this project on EAS servers. It looks like you haven't run 'eas build' yet."
)
);
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build: latestBuild,
source,
};
} catch (err) {
Log.error(err);
@@ -165,34 +182,37 @@ async function handleLatestSourceAsync(
}
async function handlePathSourceAsync(
graphqlClient: ExpoGraphqlClient,
ctx: ArchiveResolverContext,
source: ArchivePathSource
): Promise<Archive> {
): Promise<ResolvedArchiveSource> {
if (!(await isExistingFileAsync(source.path))) {
Log.error(chalk.bold(`${source.path} doesn't exist`));
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
Log.log('Uploading your app archive to the Expo Submission Service');
const uploadUrl = await uploadAppArchiveAsync(graphqlClient, source.path);
Log.log('Uploading your app archive to EAS Submit');
const bucketKey = await uploadAppArchiveAsync(ctx.graphqlClient, source.path);
return {
url: uploadUrl,
source,
sourceType: ArchiveSourceType.gcs,
bucketKey,
localSource: {
sourceType: ArchiveSourceType.path,
path: source.path,
},
};
}
async function handleBuildIdSourceAsync(
graphqlClient: ExpoGraphqlClient,
ctx: ArchiveResolverContext,
source: ArchiveBuildIdSource
): Promise<Archive> {
): Promise<ResolvedArchiveSource> {
try {
const build = await BuildQuery.byIdAsync(graphqlClient, source.id);
const build = await BuildQuery.byIdAsync(ctx.graphqlClient, source.id);
if (build.platform !== toAppPlatform(source.platform)) {
const expectedPlatformName = appPlatformDisplayNames[toAppPlatform(source.platform)];
if (build.platform !== toAppPlatform(ctx.platform)) {
const expectedPlatformName = appPlatformDisplayNames[toAppPlatform(ctx.platform)];
const receivedPlatformName = appPlatformDisplayNames[build.platform];
Log.error(
chalk.bold(
@@ -200,15 +220,14 @@ async function handleBuildIdSourceAsync(
)
);
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build,
source,
};
} catch (err) {
Log.error(chalk.bold(`Could not find build with ID ${source.id}`));
@@ -220,26 +239,24 @@ async function handleBuildIdSourceAsync(
);
Log.debug('Original error:', err);
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
}
async function handleBuildListSourceAsync(
graphqlClient: ExpoGraphqlClient,
source: ArchiveBuildListSource
): Promise<Archive> {
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
try {
const appPlatform = toAppPlatform(source.platform);
const appPlatform = toAppPlatform(ctx.platform);
const expiryDate = new Date(); // artifacts expire after 30 days
expiryDate.setDate(expiryDate.getDate() - 30);
const recentBuilds = await getRecentBuildsForSubmissionAsync(
graphqlClient,
ctx.graphqlClient,
appPlatform,
source.projectId,
ctx.projectId,
{
limit: BUILD_LIST_ITEM_COUNT,
}
@@ -252,8 +269,7 @@ async function handleBuildListSourceAsync(
"It looks like you haven't run 'eas build' yet."
)
);
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
@@ -265,8 +281,7 @@ async function handleBuildListSourceAsync(
'EAS keeps your build artifacts only for 30 days.'
)
);
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
@@ -286,15 +301,14 @@ async function handleBuildListSourceAsync(
});
if (selectedBuild == null) {
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build: selectedBuild,
source,
};
} catch (err) {
Log.error(err);
@@ -306,23 +320,22 @@ function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choi
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel } =
build;
const formatValue = (field?: string | null): string =>
field ? chalk.bold(field) : chalk.dim('Unknown');
const formatValue = (field?: string | null): string => (field ? chalk.bold(field) : 'Unknown');
const buildDate = new Date(updatedAt);
const formattedCommitData =
gitCommitHash && gitCommitMessage
? `${chalk.dim(gitCommitHash.slice(0, 7))} "${chalk.bold(gitCommitMessage)}"`
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(gitCommitMessage)}"`
: 'Unknown';
const title = `ID: ${chalk.dim(id)} (${chalk.dim(`${fromNow(buildDate)} ago`)})`;
const title = `${chalk.bold(`ID:`)} ${id} (${chalk.bold(`${fromNow(buildDate)} ago`)})`;
const description = [
`\tProfile: ${formatValue(buildProfile)}`,
`\tChannel: ${formatValue(channel)}`,
`\tRuntime version: ${formatValue(runtimeVersion)}`,
`\tCommit: ${formattedCommitData}`,
`\t${chalk.bold(`Profile:`)} ${formatValue(buildProfile)}`,
`\t${chalk.bold(`Channel:`)} ${formatValue(channel)}`,
`\t${chalk.bold(`Runtime version:`)} ${formatValue(runtimeVersion)}`,
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
].join('\n');
return {
@@ -334,9 +347,8 @@ function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choi
}
async function handlePromptSourceAsync(
graphqlClient: ExpoGraphqlClient,
source: ArchivePromptSource
): Promise<Archive> {
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
type: 'select',
@@ -360,31 +372,27 @@ async function handlePromptSourceAsync(
const sourceType = sourceTypeRaw as ArchiveSourceType;
switch (sourceType) {
case ArchiveSourceType.url: {
const url = await askForArchiveUrlAsync(source.platform);
return getArchiveAsync(graphqlClient, {
...source,
const url = await askForArchiveUrlAsync(ctx.platform);
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.url,
url,
});
}
case ArchiveSourceType.path: {
const path = await askForArchivePathAsync(source.platform);
return getArchiveAsync(graphqlClient, {
...source,
const path = await askForArchivePathAsync(ctx.platform);
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.path,
path,
});
}
case ArchiveSourceType.buildList: {
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.buildList,
});
}
case ArchiveSourceType.buildId: {
const id = await askForBuildIdAsync();
return getArchiveAsync(graphqlClient, {
...source,
return getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.buildId,
id,
});
@@ -454,13 +462,14 @@ async function askForBuildIdAsync(): Promise<string> {
}
async function askIfUseBuildIdFromUrlAsync(
ctx: ArchiveResolverContext,
source: ArchiveUrlSource,
buildId: string
): Promise<boolean> {
const { url } = source;
Log.warn(`It seems that you provided a build details page URL: ${url}`);
Log.warn('We expected to see the build artifact URL.');
if (!source.nonInteractive) {
if (!ctx.nonInteractive) {
const useAsBuildId = await confirmAsync({
message: `Do you want to submit build ${buildId} instead?`,
});
@@ -5,12 +5,15 @@ import { withAnalyticsAsync } from '../analytics/common';
import {
AndroidSubmissionConfigInput,
IosSubmissionConfigInput,
SubmissionArchiveSourceInput,
SubmissionArchiveSourceType,
SubmissionFragment,
} from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { ora } from '../ora';
import { appPlatformDisplayNames } from '../platform';
import { ArchiveSourceType, ResolvedArchiveSource } from './ArchiveSource';
import { SubmissionContext } from './context';
export interface SubmissionInput<P extends Platform> {
@@ -19,6 +22,7 @@ export interface SubmissionInput<P extends Platform> {
? AndroidSubmissionConfigInput
: IosSubmissionConfigInput;
buildId?: string;
archiveSource?: SubmissionArchiveSourceInput;
}
interface AnalyticEvents {
@@ -71,6 +75,27 @@ export default abstract class BaseSubmitter<
resolvedOptions: ResolvedSourceOptions
): Promise<SubmissionInput<P>>;
public formatArchive(
archive: ResolvedArchiveSource
): Pick<SubmissionInput<P>, 'archiveSource' | 'buildId'> {
switch (archive.sourceType) {
case ArchiveSourceType.url: {
return { archiveSource: { type: SubmissionArchiveSourceType.Url, url: archive.url } };
}
case ArchiveSourceType.gcs: {
return {
archiveSource: {
type: SubmissionArchiveSourceType.GcsSubmitArchive,
bucketKey: archive.bucketKey,
},
};
}
case ArchiveSourceType.build: {
return { buildId: archive.build.id };
}
}
}
private async createSubmissionAsync(
submissionInput: SubmissionInput<P>
): Promise<SubmissionFragment> {
@@ -4,17 +4,17 @@ import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
SubmissionArchiveSourceType,
UploadSessionType,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import { confirmAsync, promptAsync } from '../../prompts';
import { uploadFileAtPathToS3Async } from '../../uploads';
import {
Archive,
ArchiveSourceType,
BUILD_LIST_ITEM_COUNT,
getArchiveAsync,
} from '../ArchiveSource';
import { uploadFileAtPathToGCSAsync } from '../../uploads';
import { ArchiveSourceType, BUILD_LIST_ITEM_COUNT, getArchiveAsync } from '../ArchiveSource';
import { getRecentBuildsForSubmissionAsync } from '../utils/builds';
jest.mock('fs');
@@ -28,12 +28,15 @@ jest.mock('../../graphql/queries/BuildQuery', () => ({
},
}));
const ARCHIVE_URL = 'https://url.to/archive.tar.gz';
const ARCHIVE_SOURCE = {
type: SubmissionArchiveSourceType.Url,
url: 'https://url.to/archive.tar.gz',
};
const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_URL,
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
@@ -67,72 +70,82 @@ describe(getArchiveAsync, () => {
});
it('handles URL source', async () => {
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.url,
url: ARCHIVE_URL,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.url,
url: ARCHIVE_SOURCE.url,
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('prompts again if provided URL is invalid', async () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.url,
url: 'invalid',
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.url,
url: 'invalid',
}
);
expect(promptAsync).toHaveBeenCalledTimes(2);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('asks the user if use build id instead of build details page url', async () => {
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
jest.mocked(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT as BuildFragment);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.url,
url: 'https://expo.dev/accounts/turtle/projects/blah/builds/81da6b36-efe4-4262-8970-84f03efeec81',
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.url,
url: 'https://expo.dev/accounts/turtle/projects/blah/builds/81da6b36-efe4-4262-8970-84f03efeec81',
}
);
expect(confirmAsync).toHaveBeenCalled();
assertArchiveResult(archive, ArchiveSourceType.buildId);
expect((archive.source as any).id).toBe('81da6b36-efe4-4262-8970-84f03efeec81');
expect(archive.sourceType).toBe(ArchiveSourceType.build);
expect((archive as any).build.id).toBe(MOCK_BUILD_FRAGMENT.id);
});
it('handles prompt source', async () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.prompt,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.prompt,
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('handles Build ID source', async () => {
const buildId = uuidv4();
jest.mocked(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT as BuildFragment);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildId,
id: buildId,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildId,
id: buildId,
}
);
expect(BuildQuery.byIdAsync).toBeCalledWith(graphqlClient, buildId);
assertArchiveResult(archive, ArchiveSourceType.buildId);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('prompts again if build with provided ID doesnt exist', async () => {
@@ -140,15 +153,17 @@ describe(getArchiveAsync, () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildId,
id: uuidv4(),
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildId,
id: uuidv4(),
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('handles latest build source', async () => {
@@ -157,18 +172,19 @@ describe(getArchiveAsync, () => {
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.latest,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.latest,
}
);
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId
);
assertArchiveResult(archive, ArchiveSourceType.latest);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('prompts again if no builds exists when selected latest', async () => {
@@ -176,14 +192,16 @@ describe(getArchiveAsync, () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.latest,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.latest,
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('handles build-list-select source', async () => {
@@ -193,11 +211,12 @@ describe(getArchiveAsync, () => {
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
jest.mocked(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.buildList,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.buildList,
}
);
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
@@ -205,7 +224,7 @@ describe(getArchiveAsync, () => {
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
assertArchiveResult(archive, ArchiveSourceType.buildList);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('prompts again if all builds have expired', async () => {
@@ -218,14 +237,16 @@ describe(getArchiveAsync, () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildList,
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('falls back to prompt if user selected "None of the above"', async () => {
@@ -236,62 +257,62 @@ describe(getArchiveAsync, () => {
.mocked(promptAsync)
.mockResolvedValueOnce({ selectedBuild: null })
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildList,
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('handles path source', async () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
jest
.mocked(uploadFileAtPathToS3Async)
.mockResolvedValueOnce({ url: ARCHIVE_URL, bucketKey: 'wat' });
jest.mocked(uploadFileAtPathToGCSAsync).mockResolvedValueOnce('wat');
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.path,
path,
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.path,
path,
}
);
expect(uploadFileAtPathToS3Async).toBeCalledWith(
expect(uploadFileAtPathToGCSAsync).toBeCalledWith(
graphqlClient,
UploadSessionType.EasSubmitAppArchive,
UploadSessionType.EasSubmitGcsAppArchive,
path,
expect.anything()
);
assertArchiveResult(archive, ArchiveSourceType.path);
expect(archive).toMatchObject({
sourceType: ArchiveSourceType.gcs,
bucketKey: 'wat',
localSource: {
sourceType: ArchiveSourceType.path,
path,
},
});
});
it('prompts again if provided path doesnt exist', async () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.path,
path: './doesnt/exist.aab',
});
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.path,
path: './doesnt/exist.aab',
}
);
assertArchiveResult(archive, ArchiveSourceType.url);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
});
function assertArchiveResult(
archive: Archive,
expectedSourceType: ArchiveSourceType,
expectedUrl: string = ARCHIVE_URL
): void {
expect(archive.source.sourceType).toBe(expectedSourceType);
if (archive.url) {
expect(archive.url).toBe(expectedUrl);
}
}
@@ -114,7 +114,7 @@ export default class AndroidSubmitCommand {
private resolveArchiveSource(): Result<ArchiveSource> {
try {
return result(resolveArchiveSource(this.ctx, Platform.ANDROID));
return result(resolveArchiveSource(this.ctx));
} catch (err: any) {
return result(err);
}
@@ -10,7 +10,7 @@ import {
} from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import { ArchiveSource, ResolvedArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import { SubmissionContext } from '../context';
import {
@@ -35,7 +35,7 @@ export interface AndroidSubmissionOptions
}
interface ResolvedSourceOptions {
archive: Archive;
archive: ResolvedArchiveSource;
serviceAccountKeyResult: ServiceAccountKeyResult;
}
@@ -47,7 +47,16 @@ export default class AndroidSubmitter extends BaseSubmitter<
constructor(ctx: SubmissionContext<Platform.ANDROID>, options: AndroidSubmissionOptions) {
const sourceOptionsResolver = {
// eslint-disable-next-line async-protect/async-suffix
archive: async () => await getArchiveAsync(ctx.graphqlClient, this.options.archiveSource),
archive: async () =>
await getArchiveAsync(
{
graphqlClient: ctx.graphqlClient,
platform: Platform.ANDROID,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
},
this.options.archiveSource
),
// eslint-disable-next-line async-protect/async-suffix
serviceAccountKeyResult: async () => {
return await getServiceAccountKeyResultAsync(this.ctx, this.options.serviceAccountSource);
@@ -71,7 +80,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
public async createSubmissionInputAsync(
resolvedSourceOptions: ResolvedSourceOptions
): Promise<SubmissionInput<Platform.ANDROID>> {
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
const submissionConfig = this.formatSubmissionConfig(this.options, resolvedSourceOptions);
printSummary(
this.prepareSummaryData(this.options, resolvedSourceOptions),
@@ -80,7 +89,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
return {
projectId: this.options.projectId,
submissionConfig,
buildId: resolvedSourceOptions.archive.build?.id,
...this.formatArchive(resolvedSourceOptions.archive),
};
}
@@ -88,21 +97,22 @@ export default class AndroidSubmitter extends BaseSubmitter<
projectId,
submissionConfig,
buildId,
archiveSource,
}: SubmissionInput<Platform.ANDROID>): Promise<SubmissionFragment> {
return await SubmissionMutation.createAndroidSubmissionAsync(this.ctx.graphqlClient, {
appId: projectId,
config: submissionConfig,
submittedBuildId: buildId,
archiveSource,
});
}
private formatSubmissionConfig(
options: AndroidSubmissionOptions,
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
{ serviceAccountKeyResult }: ResolvedSourceOptions
): AndroidSubmissionConfigInput {
const { track, releaseStatus, changesNotSentForReview } = options;
return {
archiveUrl: archive.url,
track,
changesNotSentForReview,
releaseStatus,
@@ -15,6 +15,7 @@ import {
BuildFragment,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
SubmissionArchiveSourceType,
} from '../../../graphql/generated';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils';
@@ -104,7 +105,7 @@ describe(AndroidSubmitCommand, () => {
});
describe('sending submission', () => {
it('sends a request to Submission Service', async () => {
it('sends a request to EAS Submit', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
@@ -134,8 +135,8 @@ describe(AndroidSubmitCommand, () => {
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.apk' },
config: {
archiveUrl: 'http://expo.dev/fake.apk',
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
+1 -19
View File
@@ -3,10 +3,7 @@ import { Platform } from '@expo/eas-build-job';
import { ArchiveSource, ArchiveSourceType, isUuidV4 } from './ArchiveSource';
import { SubmissionContext } from './context';
export function resolveArchiveSource<T extends Platform>(
ctx: SubmissionContext<T>,
platform: T
): ArchiveSource {
export function resolveArchiveSource<T extends Platform>(ctx: SubmissionContext<T>): ArchiveSource {
const { url, path, id, latest } = ctx.archiveFlags;
const chosenOptions = [url, path, id, latest];
if (chosenOptions.filter(opt => opt).length > 1) {
@@ -17,17 +14,11 @@ export function resolveArchiveSource<T extends Platform>(
return {
sourceType: ArchiveSourceType.url,
url,
platform,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
};
} else if (path) {
return {
sourceType: ArchiveSourceType.path,
path,
platform,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
};
} else if (id) {
if (!isUuidV4(id)) {
@@ -36,25 +27,16 @@ export function resolveArchiveSource<T extends Platform>(
return {
sourceType: ArchiveSourceType.buildId,
id,
platform,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
};
} else if (latest) {
return {
sourceType: ArchiveSourceType.latest,
platform,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
};
} else if (ctx.nonInteractive) {
throw new Error('You need to specify the archive source when running in non-interactive mode ');
} else {
return {
sourceType: ArchiveSourceType.prompt,
platform,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
};
}
}
@@ -196,7 +196,7 @@ export default class IosSubmitCommand {
private resolveArchiveSource(): Result<ArchiveSource> {
try {
return result(resolveArchiveSource(this.ctx, Platform.IOS));
return result(resolveArchiveSource(this.ctx));
} catch (err: any) {
return result(err);
}
@@ -6,7 +6,7 @@ import { MinimalAscApiKey } from '../../credentials/ios/credentials';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import { ArchiveSource, ResolvedArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import { SubmissionContext } from '../context';
import {
@@ -40,7 +40,7 @@ export interface IosSubmissionOptions
}
interface ResolvedSourceOptions {
archive: Archive;
archive: ResolvedArchiveSource;
credentials: {
appSpecificPassword?: AppSpecificPasswordCredentials;
ascApiKeyResult?: AscApiKeyResult;
@@ -55,7 +55,16 @@ export default class IosSubmitter extends BaseSubmitter<
constructor(ctx: SubmissionContext<Platform.IOS>, options: IosSubmissionOptions) {
const sourceOptionsResolver = {
// eslint-disable-next-line async-protect/async-suffix
archive: async () => await getArchiveAsync(ctx.graphqlClient, this.options.archiveSource),
archive: async () =>
await getArchiveAsync(
{
graphqlClient: ctx.graphqlClient,
platform: Platform.IOS,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
},
this.options.archiveSource
),
// eslint-disable-next-line async-protect/async-suffix
credentials: async () => {
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
@@ -105,7 +114,7 @@ export default class IosSubmitter extends BaseSubmitter<
return {
projectId: this.options.projectId,
submissionConfig,
buildId: resolvedSourceOptions.archive.build?.id,
...this.formatArchive(resolvedSourceOptions.archive),
};
}
@@ -113,24 +122,25 @@ export default class IosSubmitter extends BaseSubmitter<
projectId,
submissionConfig,
buildId,
archiveSource,
}: SubmissionInput<Platform.IOS>): Promise<SubmissionFragment> {
return await SubmissionMutation.createIosSubmissionAsync(this.ctx.graphqlClient, {
appId: projectId,
config: submissionConfig,
submittedBuildId: buildId,
archiveSource,
});
}
private formatSubmissionConfig(
options: IosSubmissionOptions,
{ archive, credentials }: ResolvedSourceOptions
{ credentials }: ResolvedSourceOptions
): IosSubmissionConfigInput {
const { appSpecificPassword, ascApiKeyResult } = credentials;
const { appleIdUsername, ascAppIdentifier } = options;
return {
ascAppIdentifier,
appleIdUsername,
archiveUrl: archive.url,
...(appSpecificPassword ? this.formatAppSpecificPassword(appSpecificPassword) : null),
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
};
@@ -9,6 +9,7 @@ import {
jester as mockJester,
testProjectId,
} from '../../../credentials/__tests__/fixtures-constants';
import { SubmissionArchiveSourceType } from '../../../graphql/generated';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
@@ -80,7 +81,7 @@ describe(IosSubmitCommand, () => {
});
describe('sending submission', () => {
it('sends a request to Submission Service', async () => {
it('sends a request to EAS Submit', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
@@ -110,8 +111,8 @@ describe(IosSubmitCommand, () => {
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.ipa' },
config: {
archiveUrl: 'http://expo.dev/fake.ipa',
appleIdUsername: '[email protected]',
appleAppSpecificPassword: 'supersecret',
ascAppIdentifier: '12345678',
+4 -4
View File
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { UploadSessionType } from '../../graphql/generated';
import { uploadFileAtPathToS3Async } from '../../uploads';
import { uploadFileAtPathToGCSAsync } from '../../uploads';
import { createProgressTracker } from '../../utils/progress';
export async function isExistingFileAsync(filePath: string): Promise<boolean> {
@@ -19,9 +19,9 @@ export async function uploadAppArchiveAsync(
path: string
): Promise<string> {
const fileSize = (await fs.stat(path)).size;
const { url } = await uploadFileAtPathToS3Async(
const bucketKey = await uploadFileAtPathToGCSAsync(
graphqlClient,
UploadSessionType.EasSubmitAppArchive,
UploadSessionType.EasSubmitGcsAppArchive,
path,
createProgressTracker({
total: fileSize,
@@ -29,5 +29,5 @@ export async function uploadAppArchiveAsync(
completedMessage: 'Uploaded to EAS Submit',
})
);
return url;
return bucketKey;
}
+10 -9
View File
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
import { Archive, ArchiveSourceType } from '../ArchiveSource';
import { ArchiveSourceType, ResolvedArchiveSource } from '../ArchiveSource';
export interface ArchiveSourceSummaryFields {
archiveUrl?: string;
archivePath?: string;
@@ -43,19 +43,20 @@ function formatSubmissionBuildSummary(build: BuildFragment): string {
);
}
export function formatArchiveSourceSummary({ source, build }: Archive): ArchiveSourceSummaryFields {
export function formatArchiveSourceSummary(
archive: ResolvedArchiveSource
): ArchiveSourceSummaryFields {
const summarySlice: ArchiveSourceSummaryFields = {};
switch (source.sourceType) {
case ArchiveSourceType.path:
summarySlice.archivePath = source.path;
switch (archive.sourceType) {
case ArchiveSourceType.gcs:
summarySlice.archivePath = archive.localSource.path;
break;
case ArchiveSourceType.url:
summarySlice.archiveUrl = source.url;
summarySlice.archiveUrl = archive.url;
break;
case ArchiveSourceType.buildId:
case ArchiveSourceType.latest:
summarySlice.formattedBuild = formatSubmissionBuildSummary(build!);
case ArchiveSourceType.build:
summarySlice.formattedBuild = formatSubmissionBuildSummary(archive.build!);
break;
}
return summarySlice;
+3 -4
View File
@@ -1,19 +1,18 @@
import chalk from 'chalk';
import { URL } from 'url';
import { getExpoWebsiteBaseUrl } from '../../api';
import { SubmissionFragment } from '../../graphql/generated';
import Log from '../../log';
import Log, { link } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
export function printSubmissionDetailsUrls(submissions: SubmissionFragment[]): void {
if (submissions.length === 1) {
const [submission] = submissions;
Log.log(`Submission details: ${chalk.underline(getSubmissionDetailsUrl(submission))}`);
Log.log(`Submission details: ${link(getSubmissionDetailsUrl(submission))}`);
} else {
submissions.forEach(submission => {
Log.log(
`${appPlatformDisplayNames[submission.platform]} submission details: ${chalk.underline(
`${appPlatformDisplayNames[submission.platform]} submission details: ${link(
getSubmissionDetailsUrl(submission)
)}`
);
@@ -0,0 +1,155 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { UpdateBranch, UpdateChannel } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
jest.mock('../../graphql/queries/ChannelQuery');
jest.mock('../../graphql/queries/BranchQuery');
describe(getBranchNameFromChannelNameAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('returns branch name when channel exists and has one connected branch', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name'],
}) as any
);
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'channel-name'
);
expect(result).toBe('test-branch-name');
});
test('errors when no branch is connected to channel', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
}) as any
);
expect(
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
});
test('errors when more than one branch is connected to channel', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name', 'test-branch-name-2'],
}) as any
);
expect(
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has multiple branches associated with it. Instead, use '--branch' instead of '--channel'"
);
});
test('creates channel and branch when channel does not exist, and returns branch name', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name'],
}) as any
);
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'test-channel-name'
);
expect(result).toBe('test-branch-name');
});
});
function mockUpdateChannel({
channelName,
branchNames,
}: {
channelName?: string;
branchNames?: string[];
}): UpdateChannel {
return {
id: 'a2a1fa12-9d6a-433a-a432-49c64ef8439f',
name: channelName ?? 'default-channel-name',
createdAt: '2022-12-07T02:24:29.786Z',
appId: '123',
updatedAt: '2022-12-07T02:24:29.786Z',
branchMapping:
'{"data":[{"branchId":"f9dc4dbb-663f-4a19-8cf2-a783b484d2db","branchMappingLogic":"true"}],"version":0}',
updateBranches: branchNames ? mockUpdateBranches(branchNames) : [],
__typename: 'UpdateChannel',
};
}
function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
return branchNames.map(branchName => ({
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
name: branchName ?? 'default-branch-name',
appId: '123',
createdAt: '2022-12-07T02:24:29.786Z',
updatedAt: '2022-12-07T02:24:29.786Z',
updates: [],
updateGroups: [
[
{
id: '8503286d-4b18-4b38-a0f7-2b9ae09531a0',
group: 'd715bc20-76b5-477b-a060-3f8184aae87a',
message: 'my message',
createdAt: '2022-12-07T02:24:43.487Z',
runtimeVersion: '1.0.0',
platform: 'ios',
manifestFragment: '...',
activityTimestamp: '2022-12-07T02:24:43.487Z',
branchId: '',
awaitingCodeSigningInfo: false,
isGitWorkingTreeDirty: false,
manifestPermalink: '...',
updatedAt: '2022-12-07T02:24:43.487Z',
gitCommitHash: 'abc5baf77eee821b800af97879d19cc3132b46a7',
actor: {
__typename: 'User' as const,
id: 'f42b73a3-ce6c-4059-a825-e5ddafa022f7',
username: 'jonexpo',
} as any,
branch: {
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
name: 'production3',
appId: '123',
createdAt: '2022-12-07T02:24:29.786Z',
updatedAt: '2022-12-07T02:24:29.786Z',
updateGroups: [],
updates: [],
__typename: 'UpdateBranch' as const,
},
codeSigningInfo: null,
__typename: 'Update' as const,
},
],
],
__typename: 'UpdateBranch' as const,
}));
}
+67 -4
View File
@@ -1,7 +1,9 @@
import { modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { getEASUpdateURL } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -16,7 +18,7 @@ import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from './android/UpdatesModule';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from './ios/UpdatesModule';
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'appVersion' } as const;
export const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
function getDefaultRuntimeVersion(workflow: Workflow): NonNullable<ExpoConfig['runtimeVersion']> {
@@ -225,7 +227,7 @@ function warnEASUpdatesManualConfig({
/**
* Make sure that the current `app.json` configuration for EAS Updates is set natively.
*/
async function ensureEASUpdatesIsConfiguredNativelyAsync(
async function ensureEASUpdateIsConfiguredNativelyAsync(
graphqlClient: ExpoGraphqlClient,
{
exp,
@@ -252,6 +254,67 @@ async function ensureEASUpdatesIsConfiguredNativelyAsync(
}
}
/**
* Make sure EAS Build profiles are configured to work with EAS Update by adding channels to build profiles.
*/
export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
Log.warn(
`EAS Build is not configured. If you'd like to use EAS Build with EAS Update, run ${chalk.bold(
'eas build:configure'
)}, then re-run ${chalk.bold('eas update:configure')} to configure ${chalk.bold(
'eas.json'
)} with EAS Update.`
);
return;
}
try {
const easJsonAccessor = new EasJsonAccessor(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
const easBuildProfilesWithChannels = Object.keys(easJsonRawObject.build).reduce(
(acc, profileNameKey) => {
const buildProfile = easJsonRawObject.build[profileNameKey];
const isNotAlreadyConfigured = !buildProfile.channel && !buildProfile.releaseChannel;
if (isNotAlreadyConfigured) {
return {
...acc,
[profileNameKey]: {
...buildProfile,
channel: profileNameKey,
},
};
}
return {
...acc,
[profileNameKey]: {
...easJsonRawObject.build[profileNameKey],
},
};
},
{}
);
return {
...easJsonRawObject,
build: easBuildProfilesWithChannels,
};
});
await easJsonAccessor.writeAsync();
Log.withTick(`Configured ${chalk.bold('eas.json')}.`);
} catch (error) {
Log.error(`We were not able to configure ${chalk.bold('eas.json')}. Error: ${error}.`);
}
}
/**
* Make sure EAS Update is fully configured in the current project.
* This goes over a checklist and performs the following checks or changes:
@@ -261,7 +324,7 @@ async function ensureEASUpdatesIsConfiguredNativelyAsync(
* - Sets `updates.url` if not set
* - Ensure latest changes are reflected in the native config, if any
*/
export async function ensureEASUpdatesIsConfiguredAsync(
export async function ensureEASUpdateIsConfiguredAsync(
graphqlClient: ExpoGraphqlClient,
{
exp: expWithoutUpdates,
@@ -301,7 +364,7 @@ export async function ensureEASUpdatesIsConfiguredAsync(
});
if (projectChanged || !hasExpoUpdates) {
await ensureEASUpdatesIsConfiguredNativelyAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredNativelyAsync(graphqlClient, {
exp: expWithUpdates,
projectDir,
projectId,
@@ -0,0 +1,58 @@
import { ensureBranchExistsAsync } from '../branch/queries';
import { ChannelNotFoundError } from '../channel/errors';
import { createChannelOnAppAsync } from '../channel/queries';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { ChannelQuery } from '../graphql/queries/ChannelQuery';
export async function getBranchNameFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
channelName: string
): Promise<string> {
let branchName;
try {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName,
});
if (channel.updateBranches.length === 1) {
branchName = channel.updateBranches[0].name;
} else if (channel.updateBranches.length === 0) {
throw new Error(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
} else {
throw new Error(
`Channel has multiple branches associated with it. Instead, use '--branch' instead of '--channel'`
);
}
} catch (error) {
if (!(error instanceof ChannelNotFoundError)) {
throw error;
}
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName: channelName,
});
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${projectId}`
);
}
branchName = channelName;
}
return branchName;
}
+3 -1
View File
@@ -126,7 +126,9 @@ export async function selectUpdateGroupOnBranchAsync(
}),
promptOptions: {
title: 'Load more update groups?',
createDisplayTextForSelectionPromptListItem: updateGroup => formatUpdateTitle(updateGroup[0]),
makePartialChoiceObject: updateGroup => ({
title: formatUpdateTitle(updateGroup[0]),
}),
getIdentifierForQueryItem: updateGroup => updateGroup[0].group,
},
});
+24 -49
View File
@@ -1,37 +1,29 @@
import assert from 'assert';
import FormData from 'form-data';
import fs from 'fs-extra';
import { Response } from 'node-fetch';
import nullthrows from 'nullthrows';
import promiseRetry from 'promise-retry';
import { URL } from 'url';
import { ExpoGraphqlClient } from './commandUtils/context/contextUtils/createGraphqlClient';
import fetch from './fetch';
import { UploadSessionType } from './graphql/generated';
import { PresignedPost, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
import { SignedUrl, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
import { ProgressHandler } from './utils/progress';
export async function uploadFileAtPathToS3Async(
export interface PresignedPost {
url: string;
fields: Record<string, string>;
}
export async function uploadFileAtPathToGCSAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType,
path: string,
handleProgressEvent: ProgressHandler
): Promise<{ url: string; bucketKey: string }> {
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
): Promise<string> {
const signedUrl = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
const response = await uploadWithPresignedPostWithProgressAsync(
path,
presignedPost,
handleProgressEvent
);
const location = nullthrows(
response.headers.get('location'),
`location does not exist in response headers (make sure you're uploading to AWS S3)`
);
const url = fixS3Url(location);
return { url, bucketKey: presignedPost.fields.key };
await uploadWithSignedUrlWithProgressAsync(path, signedUrl, handleProgressEvent);
return signedUrl.bucketKey;
}
export async function uploadWithPresignedPostWithRetryAsync(
@@ -72,10 +64,10 @@ export async function uploadWithPresignedPostWithRetryAsync(
);
}
async function createPresignedPostFormDataAsync(
async function uploadWithPresignedPostAsync(
file: string,
presignedPost: PresignedPost
): Promise<{ form: FormData; fileSize: number }> {
): Promise<Response> {
const fileStat = await fs.stat(file);
const fileSize = fileStat.size;
const form = new FormData();
@@ -83,14 +75,6 @@ async function createPresignedPostFormDataAsync(
form.append(fieldKey, fieldValue);
}
form.append('file', fs.createReadStream(file), { knownLength: fileSize });
return { form, fileSize };
}
async function uploadWithPresignedPostAsync(
file: string,
presignedPost: PresignedPost
): Promise<Response> {
const { form } = await createPresignedPostFormDataAsync(file, presignedPost);
const formHeaders = form.getHeaders();
return await fetch(presignedPost.url, {
method: 'POST',
@@ -101,23 +85,25 @@ async function uploadWithPresignedPostAsync(
});
}
async function uploadWithPresignedPostWithProgressAsync(
async function uploadWithSignedUrlWithProgressAsync(
file: string,
presignedPost: PresignedPost,
signedUrl: SignedUrl,
handleProgressEvent: ProgressHandler
): Promise<Response> {
const { form, fileSize } = await createPresignedPostFormDataAsync(file, presignedPost);
const formHeaders = form.getHeaders();
const uploadPromise = fetch(presignedPost.url, {
method: 'POST',
body: form,
const fileStat = await fs.stat(file);
const fileSize = fileStat.size;
const readStream = fs.createReadStream(file);
const uploadPromise = fetch(signedUrl.url, {
method: 'PUT',
body: readStream,
headers: {
...formHeaders,
...signedUrl.headers,
},
});
let currentSize = 0;
form.addListener('data', (chunk: Buffer) => {
readStream.addListener('data', (chunk: Buffer) => {
currentSize += Buffer.byteLength(chunk);
handleProgressEvent({
progress: {
@@ -136,14 +122,3 @@ async function uploadWithPresignedPostWithProgressAsync(
throw error;
}
}
/**
* S3 returns broken URLs, sth like:
* https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c
* This function replaces %2F with /.
*/
export function fixS3Url(archiveUrl: string): string {
const parsed = new URL(archiveUrl);
parsed.pathname = decodeURIComponent(parsed.pathname);
return parsed.toString();
}
@@ -129,7 +129,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => 'item: ' + item.value,
makePartialChoiceObject: item => ({ title: 'item: ' + item.value }),
},
});
@@ -179,7 +179,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => item.value,
makePartialChoiceObject: item => ({ title: item.value }),
},
});
+82 -45
View File
@@ -1,23 +1,28 @@
import spawnAsync from '@expo/spawn-async';
import cliProgress from 'cli-progress';
import glob from 'fast-glob';
import fs from 'fs';
import fs from 'fs-extra';
import path from 'path';
import { Stream } from 'stream';
import { extract } from 'tar';
import { promisify } from 'util';
import { v4 as uuidv4 } from 'uuid';
import fetch, { RequestInit } from '../fetch';
import fetch, { RequestInit, Response } from '../fetch';
import { AppPlatform } from '../graphql/generated';
import Log from '../log';
import { formatBytes } from './files';
import { getTmpDirectory } from './paths';
import { ProgressHandler, createProgressTracker } from './progress';
const pipeline = promisify(Stream.pipeline);
type ProgressCallback = (progress: number, total: number, loaded: number) => void;
function wrapFetchWithProgress() {
return async (url: string, init: RequestInit, onProgressCallback: ProgressCallback) => {
function wrapFetchWithProgress(): (
url: string,
init: RequestInit,
progressHandler: ProgressHandler
) => Promise<Response> {
let didProgressBarFinish = false;
return async (url: string, init: RequestInit, progressHandler: ProgressHandler) => {
const response = await fetch(url, init);
if (response.ok) {
@@ -40,7 +45,15 @@ function wrapFetchWithProgress() {
const progress = length / total;
onProgressCallback(progress, total, length);
if (!didProgressBarFinish) {
progressHandler({
progress: { total, percent: progress, transferred: length },
isComplete: total === length,
});
if (total === length) {
didProgressBarFinish = true;
}
}
};
response.body.on('data', chunk => {
@@ -56,72 +69,96 @@ function wrapFetchWithProgress() {
};
}
async function downloadFileWithProgressBarAsync(
async function downloadFileWithProgressTrackerAsync(
url: string,
outputPath: string,
infoMessage?: string
progressTrackerMessage: string | ((ratio: number, total: number) => string),
progressTrackerCompletedMessage: string
): Promise<void> {
Log.newLine();
Log.log(infoMessage ? infoMessage : `Downloading file from ${url}...`);
const downloadProgressBar = new cliProgress.SingleBar(
{ format: '|{bar}|' },
cliProgress.Presets.rect
);
try {
const response = await wrapFetchWithProgress()(
url,
{
timeout: 1000 * 60 * 5, // 5 minutes
},
createProgressTracker({
message: progressTrackerMessage,
completedMessage: progressTrackerCompletedMessage,
})
);
let downloadProgressBarStarted = false;
const response = await wrapFetchWithProgress()(
url,
{
timeout: 1000 * 60 * 5, // 5 minutes
},
(_progress: number, total: number, loaded: number): void => {
if (!downloadProgressBarStarted) {
downloadProgressBar.start(total, loaded);
downloadProgressBarStarted = true;
} else if (loaded < total) {
downloadProgressBar.update(loaded);
} else {
downloadProgressBar.stop();
}
if (!response.ok) {
throw new Error(`Failed to download file from ${url}`);
}
);
if (!response.ok) {
throw new Error(`Failed to download file from ${url}`);
await pipeline(response.body, fs.createWriteStream(outputPath));
} catch (error: any) {
if (await fs.pathExists(outputPath)) {
await fs.remove(outputPath);
}
throw error;
}
await pipeline(response.body, fs.createWriteStream(outputPath));
}
export async function downloadAndExtractAppAsync(
async function maybeCacheAppAsync(appPath: string, cachedAppPath?: string): Promise<string> {
if (cachedAppPath) {
await fs.ensureDir(path.dirname(cachedAppPath));
await fs.move(appPath, cachedAppPath);
return cachedAppPath;
}
return appPath;
}
export async function downloadAndMaybeExtractAppAsync(
url: string,
applicationExtension: string
platform: AppPlatform,
cachedAppPath?: string
): Promise<string> {
const outputDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(outputDir, { recursive: true });
const tmpArchivePathDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(tmpArchivePathDir, { recursive: true });
if (url.endsWith('apk')) {
const apkFilePath = path.join(outputDir, `${uuidv4()}.apk`);
await downloadFileWithProgressTrackerAsync(
url,
apkFilePath,
(ratio, total) => `Downloading app (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
'Successfully downloaded app'
);
return maybeCacheAppAsync(apkFilePath, cachedAppPath);
} else {
const tmpArchivePathDir = path.join(getTmpDirectory(), uuidv4());
await fs.mkdir(tmpArchivePathDir, { recursive: true });
const tmpArchivePath = path.join(tmpArchivePathDir, `${uuidv4()}.tar.gz`);
const tmpArchivePath = path.join(tmpArchivePathDir, `${uuidv4()}.tar.gz`);
await downloadFileWithProgressBarAsync(url, tmpArchivePath, 'Downloading app archive...');
await tarExtractAsync(tmpArchivePath, outputDir);
await downloadFileWithProgressTrackerAsync(
url,
tmpArchivePath,
(ratio, total) =>
`Downloading app archive (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
'Successfully downloaded app archive'
);
await tarExtractAsync(tmpArchivePath, outputDir);
return await getAppPathAsync(outputDir, applicationExtension);
const appPath = await getAppPathAsync(outputDir, platform === AppPlatform.Ios ? 'app' : 'apk');
return maybeCacheAppAsync(appPath, cachedAppPath);
}
}
export async function extractAppFromLocalArchiveAsync(
appArchivePath: string,
applicationExtension: string
platform: AppPlatform
): Promise<string> {
const outputDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(outputDir, { recursive: true });
await tarExtractAsync(appArchivePath, outputDir);
return await getAppPathAsync(outputDir, applicationExtension);
return await getAppPathAsync(outputDir, platform === AppPlatform.Android ? 'apk' : 'app');
}
async function getAppPathAsync(outputDir: string, applicationExtension: string): Promise<string> {
+9 -15
View File
@@ -23,7 +23,10 @@ function dotExpoHomeDirectory(): string {
return dirPath;
}
const getStateJsonPath = (): string => path.join(dotExpoHomeDirectory(), 'state.json');
export const getStateJsonPath = (): string => path.join(dotExpoHomeDirectory(), 'state.json');
export const getEasBuildRunCacheDirectoryPath = (): string =>
path.join(getTmpDirectory(), 'eas-build-run-cache');
// Paths for storing things like data, config, cache, etc.
// Should use the correct OS-specific paths (e.g. XDG base directory on Linux)
@@ -35,17 +38,8 @@ const {
temp: TEMP_PATH,
} = envPaths('eas-cli');
const getDataDirectory = (): string => DATA_PATH;
const getConfigDirectory = (): string => CONFIG_PATH;
const getCacheDirectory = (): string => CACHE_PATH;
const getLogDirectory = (): string => LOG_PATH;
const getTmpDirectory = (): string => TEMP_PATH;
export {
getDataDirectory,
getConfigDirectory,
getCacheDirectory,
getLogDirectory,
getTmpDirectory,
getStateJsonPath,
};
export const getDataDirectory = (): string => DATA_PATH;
export const getConfigDirectory = (): string => CONFIG_PATH;
export const getCacheDirectory = (): string => CACHE_PATH;
export const getLogDirectory = (): string => LOG_PATH;
export const getTmpDirectory = (): string => TEMP_PATH;
+7 -5
View File
@@ -21,7 +21,7 @@ export function createProgressTracker({
completedMessage,
}: {
total?: number;
message: string | ((ratio: number) => string);
message: string | ((ratio: number, total: number) => string);
completedMessage: string | ((duration: string) => string);
}): ProgressHandler {
let bar: Ora | null = null;
@@ -31,17 +31,19 @@ export function createProgressTracker({
const timerLabel = String(Date.now());
const getMessage = (v: number): string => {
const getMessage = (v: number, total: number): string => {
const ratio = Math.min(Math.max(v, 0), 1);
const percent = Math.floor(ratio * 100);
return typeof message === 'string' ? `${message} ${percent.toFixed(0)}%` : message(ratio);
return typeof message === 'string'
? `${message} ${percent.toFixed(0)}%`
: message(ratio, total);
};
return ({ progress, isComplete, error }) => {
if (progress) {
if (!bar && (progress.total !== undefined || total !== undefined)) {
calcTotal = (total ?? progress.total) as number;
bar = ora(getMessage(0)).start();
bar = ora(getMessage(0, calcTotal)).start();
startTimer(timerLabel);
}
if (progress.total) {
@@ -56,7 +58,7 @@ export function createProgressTracker({
percentage = current / calcTotal;
}
bar.text = getMessage(percentage);
bar.text = getMessage(percentage, calcTotal);
}
transferredSoFar = progress.transferred;
}
+13 -3
View File
@@ -3,6 +3,12 @@ import uniqBy from './expodash/uniqBy';
const fetchMoreValue = '_fetchMore';
export interface SelectPromptEntry {
title: string;
description?: string;
disabled?: boolean;
}
type BasePaginatedQueryArgs<QueryReturnType extends Record<string, any>> = {
limit: number;
offset: number;
@@ -21,8 +27,9 @@ type PaginatedQueryWithSelectPromptArgs<QueryReturnType extends Record<string, a
BasePaginatedQueryArgs<QueryReturnType> & {
promptOptions: {
readonly title: string;
createDisplayTextForSelectionPromptListItem: (queryItem: QueryReturnType) => string;
makePartialChoiceObject: (queryItem: QueryReturnType) => SelectPromptEntry;
getIdentifierForQueryItem: (queryItem: QueryReturnType) => string;
readonly selectPromptWarningMessage?: string;
};
};
@@ -100,7 +107,7 @@ async function paginatedQueryWithSelectPromptInternalAsync<
const selectionPromptListItems = uniqBy(newAccumulator, queryItem =>
promptOptions.getIdentifierForQueryItem(queryItem)
).map(queryItem => ({
title: promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
...promptOptions.makePartialChoiceObject(queryItem),
value: promptOptions.getIdentifierForQueryItem(queryItem),
}));
if (areMorePagesAvailable) {
@@ -112,7 +119,10 @@ async function paginatedQueryWithSelectPromptInternalAsync<
const valueOfUserSelectedListItem = await selectAsync<string>(
promptOptions.title,
selectionPromptListItems
selectionPromptListItems,
{
warningMessageForDisabledEntries: promptOptions.selectPromptWarningMessage,
}
);
if (valueOfUserSelectedListItem === fetchMoreValue) {
+3 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "2.7.0",
"version": "3.0.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.18.6",
"@expo/eas-build-job": "0.2.96",
"@expo/eas-build-job": "0.2.97",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
@@ -14,6 +14,7 @@
"joi": "17.6.4",
"log-symbols": "4.1.0",
"semver": "7.3.8",
"terminal-link": "2.1.1",
"tslib": "2.4.0"
},
"devDependencies": {
+2 -4
View File
@@ -303,6 +303,7 @@
"enum": [
"default",
"latest",
"macos-monterey-12.6-xcode-14.1",
"macos-monterey-12.6-xcode-14.0",
"macos-monterey-12.4-xcode-13.4",
"macos-monterey-12.3-xcode-13.3",
@@ -310,10 +311,7 @@
"macos-big-sur-11.4-xcode-13.0",
"macos-big-sur-11.4-xcode-12.5"
],
"markdownEnumDescriptions": [
"`macos-monterey-12.4-xcode-13.4`",
"`macos-monterey-12.6-xcode-14.0`"
]
"markdownEnumDescriptions": ["`macos-monterey-12.6-xcode-14.0`", "`macos-monterey-12.6-xcode-14.1`"]
},
{
"type": "string"
+32
View File
@@ -3,12 +3,40 @@ import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import * as fleece from 'golden-fleece';
import { ValidationError } from 'joi';
import path from 'path';
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
import { link } from './log';
import { EasJsonSchema } from './schema';
import { EasJson } from './types';
const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
// Ask user to upgrade eas-cli version or check the docs when image is invalid.
(err: ValidationError) => {
for (const detail of err.details) {
// image should be only placed under 'build.profilename.platform.image' key
// if it's not the case show standard Joi error
if (
detail.path.length === 4 &&
detail.path[0] === 'build' &&
['ios', 'android'].includes(detail.path[2].toString()) &&
detail.path[3] === 'image'
) {
throw new InvalidEasJsonError(
chalk.red(
`Specified build image '${
detail?.context?.value
}' is not recognized. Please update your EAS CLI and see ${link(
'https://docs.expo.dev/build-reference/infrastructure/'
)} for supported build images.`
)
);
}
}
},
];
export class EasJsonAccessor {
private easJsonPath: string;
@@ -39,6 +67,10 @@ export class EasJsonAccessor {
noDefaults: true,
});
if (error) {
for (const handler of customErrorMessageHandlers) {
handler(error);
}
const errorMessages = error.message.split('. ');
throw new InvalidEasJsonError(
`${chalk.bold('eas.json')} is not valid.\n- ${errorMessages.join('\n- ')}`
+16
View File
@@ -0,0 +1,16 @@
import chalk from 'chalk';
import terminalLink from 'terminal-link';
// link function from packages/eas-cli/src/log.ts
export function link(
url: string,
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
): string {
let output: string;
if (terminalLink.isSupported) {
output = terminalLink(text, url);
} else {
output = `${text === url ? '' : text + ': '}${chalk.underline(url)}`;
}
return dim ? chalk.dim(output) : output;
}
+3
View File
@@ -5,5 +5,8 @@ set -eo pipefail
SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
pushd $SCRIPTS_DIR >/dev/null 2>&1
# Install modules
yarn --silent
# Run script
yarn --silent $@
popd >/dev/null 2>&1
Generated
+4 -4
View File
@@ -1378,10 +1378,10 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected]6":
version "0.2.96"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.96.tgz#711d0a03e078d7e03e9514a5c7acdea0d593d110"
integrity sha512-Y8jeEEwzU/GfYNSq+5Snbl5NFpXSgqtRCvPIpWUWka0XsPf4qhU9e6iWjOJboi5VbQIAQfZbXMjW/OZByjT/OA==
"@expo/[email protected]7":
version "0.2.97"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.97.tgz#8f0884c498c17bb3d3e19cad767ee7f3b6b81c05"
integrity sha512-1OtHIoJbMQSsp0jEqBLI7nsqqhy1Hv38H0Ogznyr3uFqk5zWLX57pmSx4wWYv9R8M6SXsgJJq/nNa5RAopsipA==
dependencies:
joi "^17.4.2"
semver "^7.3.5"