mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34563070f4 | ||
|
|
66b269f4bf | ||
|
|
168cb4d4a0 | ||
|
|
91b531da43 | ||
|
|
2e0d567361 | ||
|
|
81e6280cff | ||
|
|
e2fda7b9d3 | ||
|
|
ede30807f5 | ||
|
|
9f354013fa | ||
|
|
30552f134c | ||
|
|
e4beba9a21 | ||
|
|
b71ab7ec84 | ||
|
|
596cfc372b | ||
|
|
d5f6c5680c | ||
|
|
28c508ff62 | ||
|
|
97ff6f406d | ||
|
|
bd39b296fa | ||
|
|
bfbe1e930b | ||
|
|
3fe45176cc | ||
|
|
bffa6fbf03 | ||
|
|
161808bc37 | ||
|
|
ac42b15acd | ||
|
|
47d21de4eb | ||
|
|
e12ffa9ebc | ||
|
|
2944023986 | ||
|
|
49c1e7670d | ||
|
|
b157b901e3 | ||
|
|
1a1f88f202 | ||
|
|
d02d70da91 | ||
|
|
c37f2957bb | ||
|
|
a32f5fe57a | ||
|
|
b7f6d36a63 | ||
|
|
2975a025ac | ||
|
|
f4e396dde2 | ||
|
|
ef54911323 | ||
|
|
18b1074232 | ||
|
|
8468e0c5ea | ||
|
|
436d5c9ec9 | ||
|
|
1f88f7e1a3 | ||
|
|
09682754f8 | ||
|
|
8e3d84b1e5 | ||
|
|
8bb2631192 | ||
|
|
38a7fb6266 | ||
|
|
0ac2fb77a1 | ||
|
|
f12774b539 | ||
|
|
d50bdedcec | ||
|
|
8d443e5992 | ||
|
|
2b5fb6de53 | ||
|
|
dc86f27199 | ||
|
|
d97ed80867 | ||
|
|
3601b448fe | ||
|
|
68aab13d51 | ||
|
|
6cef13d756 | ||
|
|
1d45941f5d | ||
|
|
5e5badf11f | ||
|
|
840c65946b | ||
|
|
fdbd786126 | ||
|
|
d5365275e0 | ||
|
|
886e48d3ee | ||
|
|
342c78a4f6 | ||
|
|
a483d94e26 | ||
|
|
b40412a7a1 | ||
|
|
d652e04f7b | ||
|
|
85326d1104 | ||
|
|
28c087138b | ||
|
|
8badf14ec3 | ||
|
|
4f1db29ef8 | ||
|
|
2354dc1b71 | ||
|
|
f106bfa6d7 | ||
|
|
1a7edbea06 | ||
|
|
5bfaee9e4d | ||
|
|
0cb2309d7d | ||
|
|
0415a1d97f | ||
|
|
249c60f4d1 | ||
|
|
c357cc378c | ||
|
|
f7c0d731b6 | ||
|
|
e852e3edac | ||
|
|
2a283b74a3 | ||
|
|
c749bc2372 | ||
|
|
9c60f7190e | ||
|
|
8126b2276e | ||
|
|
e1de21015f | ||
|
|
61e505f1f8 | ||
|
|
2e3611161e | ||
|
|
95f5eaaacf | ||
|
|
87a3ea99e2 | ||
|
|
ddab65a55d | ||
|
|
0cf8f8ce0d | ||
|
|
2b531ec70c | ||
|
|
6cd7966904 | ||
|
|
ad72ab990f | ||
|
|
b53412fc00 | ||
|
|
cbb2de9e11 | ||
|
|
593c42f601 | ||
|
|
84b10b9dad | ||
|
|
0f286061e4 | ||
|
|
c8aad01480 | ||
|
|
d70a6bee69 | ||
|
|
67799b5c54 | ||
|
|
05b8eef622 | ||
|
|
50e56b5bc4 | ||
|
|
30ab4e62ec | ||
|
|
a1d901294b | ||
|
|
eed818cfe5 | ||
|
|
db0b6bfcf5 | ||
|
|
da81c1c6df | ||
|
|
42d3296f07 | ||
|
|
f9b4b1a6e8 | ||
|
|
d03603a75f | ||
|
|
ffa9e5ef66 | ||
|
|
e7f3cf9c74 | ||
|
|
e82a4a0a57 | ||
|
|
175f07fac9 | ||
|
|
2869744a5d | ||
|
|
99f898a483 | ||
|
|
1448db96e6 | ||
|
|
e02036a46a | ||
|
|
09f33263cb | ||
|
|
412b1ec74c | ||
|
|
9a1538fb4d | ||
|
|
0ad9b31ee3 | ||
|
|
4cbb7bb2f2 | ||
|
|
ae109cd8c6 | ||
|
|
fbaba774a6 | ||
|
|
38bc8062a4 | ||
|
|
d41d184b55 | ||
|
|
d6d32a7a7d | ||
|
|
15a9532fa7 | ||
|
|
3b9fb724fe | ||
|
|
33767b5635 | ||
|
|
175fc2b30e | ||
|
|
aec9347d69 | ||
|
|
91500d07c0 | ||
|
|
62ca14b504 |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
@@ -3,6 +3,7 @@
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
|
||||
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
|
||||
|
||||
# Why
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn lerna run build
|
||||
- run: yarn typecheck
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
|
||||
+166
-3
@@ -8,16 +8,179 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix bundle identifier resolution when native target is not provided. ([#434](https://github.com/expo/eas-cli/pull/434) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix git repo root path getter on Windows. ([#429](https://github.com/expo/eas-cli/pull/429) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix resolving Android application identifier. ([#431](https://github.com/expo/eas-cli/pull/431) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Opt out of capability syncing with `EXPO_NO_CAPABILITY_SYNC=1`. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add more verbose logging around capability syncing to help debug reported issues. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add managed credentials support for multitarget iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.1](https://github.com/expo/eas-cli/releases/tag/v0.15.1) - 2021-05-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support auto capabilities in managed workflow using Expo config plugin introspection. ([#419](https://github.com/expo/eas-cli/pull/419) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix sending `appBuildVersion` as part of the build metadata. ([#423](https://github.com/expo/eas-cli/pull/423) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.0](https://github.com/expo/eas-cli/releases/tag/v0.15.0) - 2021-05-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `[EAS BUILD API]` Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
|
||||
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support empty passwords in Android and iOS keystores. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add more build metadata - `appBuildVersion`. ([#413](https://github.com/expo/eas-cli/pull/413) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
|
||||
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
|
||||
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
|
||||
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make the default urql requestPolicy 'network-only'. ([#397](https://github.com/expo/eas-cli/pull/397) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
## [0.14.0](https://github.com/expo/eas-cli/releases/tag/v0.14.0) - 2021-05-10
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Make eas branch:publish work with expo-cli >= 4.4.3 ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
|
||||
- Create asset keys without an extension. ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Display platforms supported by an update group ([#391](https://github.com/expo/eas-cli/pull/391) by [@jkhales](https://github.com/jkhales))
|
||||
- Add a --platform flag to branch:publish command ([#389](https://github.com/expo/eas-cli/pull/389) by [@jkhales](https://github.com/jkhales))
|
||||
- Release new credentials manager. ([#363](https://github.com/expo/eas-cli/pull/363) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add QR code to install internal distribution build on device. ([#371](https://github.com/expo/eas-cli/pull/371) by [@axeldelafosse](https://github.com/axeldelafosse))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.13.0](https://github.com/expo/eas-cli/releases/tag/v0.13.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.12.0](https://github.com/expo/eas-cli/releases/tag/v0.12.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add the initiating user's username to the build metadata. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `--clear-cache` flag for `eas build`. ([#355](https://github.com/expo/eas-cli/pull/355) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where the account name was used as the username. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.11.1](https://github.com/expo/eas-cli/releases/tag/v0.11.1) - 2021-04-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Resolve ios distribution types. ([#348](https://github.com/expo/eas-cli/pull/348) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump version of `@expo/apple-utils` to fix capabilities sync immediately after initial identifier registration. ([593c4](https://github.com/expo/eas-cli/commit/593c42f601396deba1751489343ec07b0a8876da) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.11.0](https://github.com/expo/eas-cli/releases/tag/v0.11.0) - 2021-04-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `"credentialsSource": "auto"` is now deprecated. The option will use `"remote"` as the default value. ([#345](https://github.com/expo/eas-cli/pull/345) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add validation when setting up ad-hoc provisioning profile in non-interactive mode. ([#338](https://github.com/expo/eas-cli/pull/338) by [@dsokal](https://github.com/dsokal))
|
||||
- Allow for registering devices when running an ad-hoc build for the first time. ([#340](https://github.com/expo/eas-cli/pull/340) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where all Android managed builds produced AAB archives. The `buildType` property from `eas.json` is not ignored now. ([#349](https://github.com/expo/eas-cli/pull/349) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `secrets:<COMMAND>` is renamed to `secret:<COMMAND>` ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secret:delete` now accepts `id` through a flag rather than an argument ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the behavior where the provisioning profile was invalidated after syncing bundle id capabilities. ([#334](https://github.com/expo/eas-cli/pull/334) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Change build credentials summary format. ([#321](https://github.com/expo/eas-cli/pull/321) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.9.1](https://github.com/expo/eas-cli/releases/tag/v0.9.1) - 2021-04-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix provisioning profile validation to actually check status. ([#318](https://github.com/expo/eas-cli/pull/318) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace Credentials REST API calls with GraphQL counterparts, also improved DX. ([#293](https://github.com/expo/eas-cli/pull/293), [#299](https://github.com/expo/eas-cli/pull/299), [#301](https://github.com/expo/eas-cli/pull/301), [#317](https://github.com/expo/eas-cli/pull/317) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.9.0](https://github.com/expo/eas-cli/releases/tag/v0.9.0) - 2021-04-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
|
||||
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
|
||||
- Bump @expo/config-plugins to fix `eas build:configure` on Windows
|
||||
|
||||
### 🧹 Chores
|
||||
- Bump @expo/config-plugins to fix `eas build:configure` on Windows. ([d0f3e](https://github.com/expo/eas-cli/commit/d0f3e387e7f31dbce4c3b93b8d083c7d20d9e30d) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Replace REST API calls with GraphQL counterparts. ([#333](https://github.com/expo/eas-cli/pull/333) by [@barthap](https://github.com/barthap))
|
||||
|
||||
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
|
||||
|
||||
|
||||
+36
-6
@@ -1,9 +1,39 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Add release notes to `CHANGELOG.md`.
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag
|
||||
like `v1.0.0`.
|
||||
1. Wait until the `release` GitHub workflow has finished.
|
||||
1. Once the release ready to go, you can go to
|
||||
https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag like `v1.0.0`.
|
||||
1. **Wait until the `release` GitHub workflow has finished.**
|
||||

|
||||
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
|
||||
|
||||
- This command works only if `HEAD` is on the release commit, e.g. when `git log -1` prints:
|
||||
|
||||
```
|
||||
commit cbb2de9e118bfbcb5a597b261d2dbef0bb5d4b0b (tag: v0.11.1)
|
||||
Author: Brent Vatne <[email protected]>
|
||||
Date: Tue Apr 20 15:00:15 2021 -0700
|
||||
|
||||
v0.11.1
|
||||
```
|
||||
|
||||
- If you run `yarn lerna publish from-git` on any other commit, it'll print something like:
|
||||
|
||||
```
|
||||
lerna notice from-git No tagged release found
|
||||
lerna success No changed packages to publish
|
||||
```
|
||||
|
||||
- If you ever run into the above issue, use `git checkout RELEASE_COMMIT_HASH` to checkout to the release commit. Later, when you've published the packages to npm, run `git checkout main` to go back to the latest commit.
|
||||
|
||||
1. Update `CHANGELOG.md`.
|
||||
- Add the new section with the release version in the title.
|
||||
- Leave the `main` section empty (without any changelog entries).
|
||||
1. Once the release is ready to go, you can go to https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
- Remember to copy the changelog entries from `CHANGELOG.md`.
|
||||
- Before hitting the submit button, make sure there are 5 assets available under the release entry.
|
||||

|
||||
1. If any of the changelog entries have been tagged with `[EAS BUILD API]`
|
||||
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
|
||||
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commit/) for that version. In most cases, it will be the latest `Publish` commit.
|
||||
- Find the `eas-cli-local-build-plugin` version published in that commit.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.9.0"
|
||||
"version": "0.17.0"
|
||||
}
|
||||
@@ -6,6 +6,9 @@
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"release": "lerna version",
|
||||
|
||||
+47
-24
@@ -47,9 +47,9 @@ eas --help COMMAND
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas secrets:create`](#eas-secretscreate)
|
||||
* [`eas secrets:delete [ID]`](#eas-secretsdelete-id)
|
||||
* [`eas secrets:list`](#eas-secretslist)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
@@ -69,7 +69,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/login.ts)_
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -83,7 +83,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/logout.ts)_
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -97,7 +97,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/account/view.ts)_
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -108,6 +108,8 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [build/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/analytics.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
Start a build
|
||||
@@ -118,6 +120,8 @@ USAGE
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--clear-cache Clear cache before the build
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
--skip-credentials-check Skip validation of build credentials
|
||||
@@ -125,7 +129,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/index.ts)_
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -136,6 +140,8 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
Configure the project to support EAS Build.
|
||||
@@ -149,7 +155,7 @@ OPTIONS
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/configure.ts)_
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -165,7 +171,7 @@ OPTIONS
|
||||
--status=(in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/list.ts)_
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -176,7 +182,7 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/build/view.ts)_
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -187,7 +193,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/credentials.ts)_
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -198,7 +204,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/create.ts)_
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -212,7 +218,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/list.ts)_
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -223,7 +229,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/device/view.ts)_
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -242,13 +248,13 @@ OPTIONS
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
|
||||
## `eas secrets:create`
|
||||
## `eas secret:create`
|
||||
|
||||
Create an environment secret on the current project or owner account.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:create
|
||||
$ eas secret:create
|
||||
|
||||
OPTIONS
|
||||
--name=name Name of the secret
|
||||
@@ -256,30 +262,36 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
## `eas secrets:delete [ID]`
|
||||
_See code: [build/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
Delete an environment secret by ID.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:delete [ID]
|
||||
$ eas secret:delete
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the secret to delete
|
||||
OPTIONS
|
||||
--id=id ID of the secret to delete
|
||||
|
||||
DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
## `eas secrets:list`
|
||||
_See code: [build/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:list
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
Submits build artifact to app store
|
||||
@@ -304,7 +316,8 @@ OPTIONS
|
||||
|
||||
iOS specific options=asc-app-id App Store Connect unique application Apple ID
|
||||
number. Providing this param results in skipping
|
||||
app creation step. Learn more.
|
||||
app creation step. Learn more:
|
||||
https://expo.fyi/asc-app-id
|
||||
|
||||
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
|
||||
expo.ios.bundleIdentifier from app config)
|
||||
@@ -357,7 +370,7 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.9.0/build/commands/submit.ts)_
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/submit.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -376,6 +389,8 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
Delete a webhook on the current project.
|
||||
@@ -388,6 +403,8 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
List webhooks on the current project.
|
||||
@@ -400,6 +417,8 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
Create a webhook on the current project.
|
||||
@@ -418,6 +437,8 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
View a webhook on the current project.
|
||||
@@ -429,4 +450,6 @@ USAGE
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [build/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.17.0/build/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -3,6 +3,7 @@ schema: 'https://staging.exp.host/--/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,68 +1,72 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.9.0",
|
||||
"version": "0.17.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@amplitude/identify": "^1.5.0",
|
||||
"@amplitude/node": "^1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.17",
|
||||
"@expo/config": "~3.3.19",
|
||||
"@expo/config-plugins": "1.0.24",
|
||||
"@expo/eas-build-job": "0.2.22",
|
||||
"@expo/eas-json": "^0.9.0",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/errors": "^1.3.3",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"@urql/core": "^2.0.0",
|
||||
"@urql/exchange-retry": "^0.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-table3": "^0.6.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"env-paths": "^2.2.0",
|
||||
"fast-glob": "^3.2.4",
|
||||
"figures": "^3.2.0",
|
||||
"form-data": "^3.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"getenv": "^1.0.0",
|
||||
"got": "^11.5.2",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"keychain": "^1.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"mime": "^2.4.7",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"ora": "^5.1.0",
|
||||
"pkg-dir": "^4.2.0",
|
||||
"progress": "^2.0.3",
|
||||
"prompts": "^2.3.2",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"semver": "^7.3.4",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"tar": "^6.0.5",
|
||||
"tempy": "^0.7.0",
|
||||
"terminal-link": "^2.1.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"tslib": "^1",
|
||||
"turndown": "^7.0.0",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^8.3.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
"@amplitude/identify": "1.5.0",
|
||||
"@amplitude/node": "1.5.0",
|
||||
"@expo/apple-utils": "0.0.0-alpha.20",
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "2.0.2",
|
||||
"@expo/eas-build-job": "0.2.35",
|
||||
"@expo/eas-json": "^0.17.0",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@hapi/joi": "17.1.1",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/errors": "1.3.3",
|
||||
"@oclif/plugin-help": "3.2.0",
|
||||
"@urql/core": "2.0.0",
|
||||
"@urql/exchange-retry": "0.2.0",
|
||||
"chalk": "4.1.0",
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "3.0.3",
|
||||
"env-paths": "2.2.0",
|
||||
"fast-glob": "3.2.4",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "3.0.0",
|
||||
"fs-extra": "9.0.1",
|
||||
"getenv": "1.0.0",
|
||||
"got": "11.5.2",
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.0",
|
||||
"keychain": "1.3.0",
|
||||
"lodash": "4.17.20",
|
||||
"mime": "2.4.7",
|
||||
"minimatch": "3.0.4",
|
||||
"nanoid": "3.1.23",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-forge": "0.10.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"progress": "2.0.3",
|
||||
"prompts": "2.3.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"semver": "7.3.4",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.0.5",
|
||||
"tempy": "0.7.0",
|
||||
"terminal-link": "2.1.1",
|
||||
"timeago.js": "4.0.2",
|
||||
"tslib": "1.14.1",
|
||||
"turndown": "7.0.0",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
@@ -163,10 +167,13 @@
|
||||
"scripts": {
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme",
|
||||
"build": "tsc",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
},
|
||||
"types": "build/index.d.ts"
|
||||
"types": "build/index.d.ts",
|
||||
"gitHead": "161808bc37c255fdea6801d69d23dbc725c056c3"
|
||||
}
|
||||
@@ -21,7 +21,7 @@ describe('apiClient', () => {
|
||||
],
|
||||
});
|
||||
|
||||
let error: Error;
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
@@ -41,7 +41,7 @@ describe('apiClient', () => {
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error;
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
|
||||
jest.mock('prompts');
|
||||
|
||||
function createMockCredentialsProvider({
|
||||
hasRemote,
|
||||
hasLocal,
|
||||
isLocalSynced,
|
||||
}: any): CredentialsProvider {
|
||||
return {
|
||||
platform: 'android',
|
||||
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
|
||||
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
|
||||
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
|
||||
} as CredentialsProvider;
|
||||
}
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(ensureCredentialsAsync, () => {
|
||||
describe('for generic builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'local' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask when local and remote are not the same (select remote)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'remote' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on EAS servers'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('should ask when no credentials are present and return remote if confirm=true', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: true }; //confirm
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should ask when no credentials are present and abort if confimr=false', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: false }; //confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, false);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Aborting build process, credentials are not configured for Android'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should throw an error when no credentials are present in non interactive mode', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
return { value: true }; // confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Credentials for this app are not configured');
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for managed builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
|
||||
it('should use credentials.json even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to local', () => {
|
||||
it('should use credentials.json without asking even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.LOCAL,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to remote', () => {
|
||||
it('should use credentials from server even if credentials.json exists and have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.REMOTE,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { ensureValidVersions } from '../utils/updates';
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
@@ -70,7 +70,7 @@ export async function syncUpdatesConfigurationAsync(
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readVersionCode, readVersionName } from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(() => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.removeSync(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
fs.mkdirpSync(os.tmpdir());
|
||||
});
|
||||
|
||||
describe(readVersionCode, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version code from native code', () => {
|
||||
const exp = initGenericProject();
|
||||
const versionCode = readVersionCode('/repo', exp);
|
||||
expect(versionCode).toBe(123);
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version code from expo config', () => {
|
||||
const exp = initManagedProject();
|
||||
const versionCode = readVersionCode('/repo', exp);
|
||||
expect(versionCode).toBe(123);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readVersionName, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the version name from native code', () => {
|
||||
const exp = initGenericProject();
|
||||
const versionName = readVersionName('/repo', exp);
|
||||
expect(versionName).toBe('1.0');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the version from expo config', () => {
|
||||
const exp = initManagedProject();
|
||||
const versionName = readVersionName('/repo', exp);
|
||||
expect(versionName).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode 123
|
||||
versionName "1.0"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
return fakeExp;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureAppIdentifierIsDefinedAsync,
|
||||
getAndroidApplicationIdAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { Platform } from '../types';
|
||||
|
||||
enum ApplicationIdSource {
|
||||
AndroidProject,
|
||||
AppJson,
|
||||
}
|
||||
|
||||
function isApplicationIdValid(applicationId: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
|
||||
}
|
||||
|
||||
export async function ensureApplicationIdIsValidAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const applicationId = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform: Platform.ANDROID,
|
||||
exp,
|
||||
});
|
||||
if (!isApplicationIdValid(applicationId)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
Log.error(
|
||||
`Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
|
||||
);
|
||||
Log.error(`Update "android.package" in ${configDescription} and run this command again.`);
|
||||
throw new Error('Invalid applicationId');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureApplicationIdAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
allowExperimental: boolean
|
||||
): Promise<void> {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
const applicationIdFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
|
||||
|
||||
if (applicationIdFromAndroidProject && applicationIdFromConfig) {
|
||||
if (applicationIdFromConfig !== applicationIdFromAndroidProject) {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`We detected that your Android project is configured with a different application id than the one defined in ${configDescription}.`
|
||||
);
|
||||
const hasApplicationIdInStaticConfig = await hasApplicationIdInStaticConfigAsync(
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!hasApplicationIdInStaticConfig) {
|
||||
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
|
||||
However, if you choose the one defined in the Android project you'll have to update ${configDescription} on your own.`);
|
||||
}
|
||||
Log.newLine();
|
||||
const { applicationIdSource } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'applicationIdSource',
|
||||
message: 'Which application id should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `${chalk.bold(
|
||||
applicationIdFromAndroidProject
|
||||
)} - In build.gradle in Android project`,
|
||||
value: ApplicationIdSource.AndroidProject,
|
||||
},
|
||||
{
|
||||
title: `${chalk.bold(applicationIdFromConfig)} - In your ${configDescription}`,
|
||||
value: ApplicationIdSource.AppJson,
|
||||
},
|
||||
],
|
||||
});
|
||||
switch (applicationIdSource) {
|
||||
case ApplicationIdSource.AppJson: {
|
||||
await updateApplicationIdInBuildGradleAsync(projectDir, exp);
|
||||
break;
|
||||
}
|
||||
case ApplicationIdSource.AndroidProject: {
|
||||
if (hasApplicationIdInStaticConfig) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
|
||||
} else {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!applicationIdFromAndroidProject && !applicationIdFromConfig) {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
} else if (applicationIdFromAndroidProject && !applicationIdFromConfig) {
|
||||
if (getConfigFilePaths(projectDir).staticConfigPath) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
|
||||
} else {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
}
|
||||
} else if (!applicationIdFromAndroidProject && applicationIdFromConfig) {
|
||||
// This should never happen, adding warning just in case
|
||||
Log.warn(
|
||||
'applicationId is not specified in your ./android/app/build.gradle file. Make sure your project is configured correctly before building.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (allowExperimental) {
|
||||
// this step is optional, the Play store is using applicationId value from build.gradle
|
||||
// to identify the app, so package name does not need to be updated
|
||||
await updatePackageNameAsync(projectDir, exp);
|
||||
await gitAddAsync(path.join(projectDir, 'android'), { intentToAdd: true });
|
||||
}
|
||||
}
|
||||
|
||||
function missingPackageMessage(configDescription: string): string {
|
||||
return `Please define "android.package" in ${configDescription} and run "eas build:configure" again.`;
|
||||
}
|
||||
|
||||
async function updateAppJsonConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
newApplicationId: string
|
||||
): Promise<void> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic configs");
|
||||
|
||||
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
...rawStaticConfig.expo,
|
||||
android: { ...rawStaticConfig.expo?.android, package: newApplicationId },
|
||||
};
|
||||
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
|
||||
|
||||
exp.android = { ...exp.android, package: newApplicationId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static config exists and if expo.android.package is defined there.
|
||||
* It will return false if the value in static config is different than "android.package" in ExpoConfig
|
||||
*/
|
||||
async function hasApplicationIdInStaticConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<boolean> {
|
||||
if (!exp.android?.package) {
|
||||
return false;
|
||||
}
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
return false;
|
||||
}
|
||||
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
|
||||
return rawStaticConfig?.expo?.android?.package === exp.android.package;
|
||||
}
|
||||
|
||||
async function updateApplicationIdInBuildGradleAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const updatedBuildGradleContents = AndroidConfig.Package.setPackageInBuildGradle(
|
||||
exp,
|
||||
buildGradleContents
|
||||
);
|
||||
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
|
||||
}
|
||||
|
||||
async function updatePackageNameAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
await AndroidConfig.Package.renamePackageOnDisk(exp, projectDir);
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const updatedAndroidManifest = AndroidConfig.Package.setPackageInAndroidManifest(
|
||||
exp,
|
||||
androidManifest
|
||||
);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(
|
||||
androidManifestPath,
|
||||
updatedAndroidManifest
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Android, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
@@ -10,13 +11,17 @@ import AndroidCredentialsProvider, {
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationId,
|
||||
} from '../../project/android/applicationId';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
@@ -24,7 +29,7 @@ import { prepareJobAsync } from './prepareJob';
|
||||
export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.ANDROID>({
|
||||
commandCtx,
|
||||
platform: Platform.ANDROID,
|
||||
@@ -63,11 +68,12 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
}
|
||||
}
|
||||
|
||||
await ensureApplicationIdIsValidAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
@@ -113,24 +119,28 @@ async function ensureAndroidCredentialsAsync(
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = getApplicationId(
|
||||
ctx.commandCtx.projectDir,
|
||||
ctx.commandCtx.exp
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
|
||||
`You do not have access to account: ${ctx.commandCtx.accountName}`
|
||||
),
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
ctx.buildProfile.workflow,
|
||||
ctx.buildProfile.credentialsSource,
|
||||
ctx.commandCtx.nonInteractive
|
||||
);
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
|
||||
@@ -2,20 +2,24 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getAndroidApplicationIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/android/applicationId';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { configureApplicationIdAsync, ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfAndroidPackageDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
await configureApplicationIdAsync(ctx.projectDir, ctx.exp, ctx.allowExperimental);
|
||||
await ensureApplicationIdIsValidAsync(ctx.projectDir, ctx.exp);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await gitAddAsync(easGradlePath, { intentToAdd: true });
|
||||
@@ -30,20 +34,11 @@ export async function validateAndSyncProjectConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
|
||||
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
const isApplicationIdConfigured =
|
||||
applicationIdFromAndroidProject && applicationIdFromAndroidProject === packageNameFromConfig;
|
||||
|
||||
const isEasBuildGradleConfigured = await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(
|
||||
projectDir
|
||||
);
|
||||
if (!isEasBuildGradleConfigured || !isApplicationIdConfigured) {
|
||||
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
|
||||
throw new Error(
|
||||
'Project is not configured. Please run "eas build:configure" to configure the project.'
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidGenericJobInput, AndroidManagedJobInput } from '../../graphql/generated';
|
||||
import {
|
||||
AndroidGenericJobInput,
|
||||
AndroidManagedBuildType,
|
||||
AndroidManagedJobInput,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
|
||||
export function transformGenericJob(job: Android.GenericJob): AndroidGenericJobInput {
|
||||
@@ -25,5 +29,16 @@ export function transformManagedJob(job: Android.ManagedJob): AndroidManagedJobI
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
username: job.username,
|
||||
buildType: transformBuildType(job.buildType),
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.ManagedBuildType): AndroidManagedBuildType {
|
||||
if (buildType === Android.ManagedBuildType.APK) {
|
||||
return AndroidManagedBuildType.Apk;
|
||||
} else if (buildType === Android.ManagedBuildType.APP_BUNDLE) {
|
||||
return AndroidManagedBuildType.AppBundle;
|
||||
} else {
|
||||
return AndroidManagedBuildType.DevelopmentClient;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,17 @@
|
||||
import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { Android, ArchiveSource, Cache, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
@@ -71,18 +64,19 @@ async function prepareJobCommonAsync(
|
||||
|
||||
return {
|
||||
platform: Platform.ANDROID,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
},
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
ndk: ctx.buildProfile.ndk,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
...buildCredentials,
|
||||
@@ -114,11 +108,11 @@ async function prepareManagedJobAsync(
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.MANAGED,
|
||||
username: accountName,
|
||||
username,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { resolveWorkflow } from '../../project/workflow';
|
||||
|
||||
export function readVersionCode(projectDir: string, exp: ExpoConfig): number | undefined {
|
||||
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = readBuildGradle(projectDir);
|
||||
const matchResult = buildGradle?.match(/versionCode (.*)/);
|
||||
if (matchResult) {
|
||||
return Number(matchResult[1]);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return AndroidConfig.Version.getVersionCode(exp) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function readVersionName(projectDir: string, exp: ExpoConfig): string | undefined {
|
||||
const workflow = resolveWorkflow(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = readBuildGradle(projectDir);
|
||||
const matchResult = buildGradle?.match(/versionName ["'](.*)["']/);
|
||||
if (matchResult) {
|
||||
return matchResult[1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
function readBuildGradle(projectDir: string): string | undefined {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
if (!fs.pathExistsSync(buildGradlePath)) {
|
||||
return undefined;
|
||||
}
|
||||
return fs.readFileSync(buildGradlePath, 'utf8');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -11,6 +11,7 @@ import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadata } from './metadata';
|
||||
import { Platform, TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
@@ -28,33 +29,27 @@ export interface CredentialsResult<Credentials> {
|
||||
credentials: Credentials;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration, TJob extends Job> {
|
||||
export interface JobData<Credentials> {
|
||||
credentials?: Credentials;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>;
|
||||
projectConfiguration: ProjectConfiguration;
|
||||
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(
|
||||
ctx: BuildContext<TPlatform>,
|
||||
jobData: {
|
||||
archiveBucketKey: string;
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
}
|
||||
): Promise<Job>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>
|
||||
): Promise<() => Promise<string>> {
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
{
|
||||
@@ -83,25 +78,51 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
);
|
||||
}
|
||||
|
||||
const archiveBucketKey = await uploadProjectAsync(builder.ctx);
|
||||
const projectArchive = builder.ctx.commandCtx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(builder.ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
archiveBucketKey,
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
return sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
if (error?.expoApiV2ErrorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
|
||||
if (builder.ctx.commandCtx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error) {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
`You have already reached the maximum number of pending ${
|
||||
requestedPlatformDisplayNames[job.platform]
|
||||
} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -143,13 +164,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
}
|
||||
}
|
||||
|
||||
async function sendBuildRequestAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration,
|
||||
TJob extends Job
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, ProjectConfiguration, TJob>,
|
||||
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
): Promise<string> {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
[RequestedPlatform.iOS]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function configureAsync(options: {
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.iOS].includes(options.platform),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
|
||||
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
|
||||
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AppPlatform } from '../graphql/generated';
|
||||
import { RequestedPlatform } from './types';
|
||||
|
||||
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
|
||||
[RequestedPlatform.iOS]: 'iOS',
|
||||
[RequestedPlatform.Ios]: 'iOS',
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
@@ -19,6 +19,8 @@ export interface CommandContext {
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
@@ -31,6 +33,8 @@ export async function createCommandContextAsync({
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive = false,
|
||||
local,
|
||||
clearCache = false,
|
||||
skipCredentialsCheck = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
@@ -41,6 +45,8 @@ export async function createCommandContextAsync({
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
clearCache: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
@@ -59,6 +65,8 @@ export async function createCommandContextAsync({
|
||||
projectName,
|
||||
exp,
|
||||
nonInteractive,
|
||||
local,
|
||||
clearCache,
|
||||
skipCredentialsCheck,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
@@ -79,7 +87,7 @@ export interface ConfigureContext {
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
|
||||
@@ -19,6 +19,9 @@ export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
if (commandCtx.local) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
Log.newLine();
|
||||
@@ -36,7 +39,7 @@ async function startBuildsAsync(
|
||||
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildiOS = [RequestedPlatform.iOS, RequestedPlatform.All].includes(
|
||||
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
@@ -46,27 +49,29 @@ async function startBuildsAsync(
|
||||
|
||||
const builds: {
|
||||
platform: Platform;
|
||||
sendBuildRequestAsync: () => Promise<string>;
|
||||
sendBuildRequestAsync: () => Promise<string | undefined>;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildiOS) {
|
||||
if (shouldBuildIos) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
|
||||
}
|
||||
return Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
);
|
||||
return (
|
||||
await Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
)
|
||||
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
|
||||
}
|
||||
|
||||
async function waitForBuildEndAsync(
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 1800, intervalSec = 30 } = {}
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
|
||||
const spinner = ora().start();
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { CredentialsProvider } from '../credentials/CredentialsProvider';
|
||||
import Log from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
|
||||
function logCredentials(target: 'local' | 'remote', platform: Platform) {
|
||||
let message = `Using ${target} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
|
||||
Log.succeed(message);
|
||||
}
|
||||
|
||||
async function ensureCredentialsAutoAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
const platform = requestedPlatformDisplayNames[provider.platform];
|
||||
switch (workflow) {
|
||||
case Workflow.MANAGED:
|
||||
if (await provider.hasLocalAsync()) {
|
||||
return CredentialsSource.LOCAL;
|
||||
} else {
|
||||
return CredentialsSource.REMOTE;
|
||||
}
|
||||
case Workflow.GENERIC: {
|
||||
const hasLocal = await provider.hasLocalAsync();
|
||||
const hasRemote = await provider.hasRemoteAsync();
|
||||
if (hasRemote && hasLocal) {
|
||||
if (!(await provider.isLocalSyncedAsync())) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
|
||||
);
|
||||
}
|
||||
|
||||
const { select } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
message: 'Which credentials do you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on EAS servers.', value: CredentialsSource.REMOTE },
|
||||
],
|
||||
});
|
||||
return select;
|
||||
} else {
|
||||
return CredentialsSource.LOCAL;
|
||||
}
|
||||
} else if (hasLocal) {
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
} else if (hasRemote) {
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
|
||||
);
|
||||
} else if (Log.isDebug) {
|
||||
Log.warn(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${platform} credentials not found, generate now?`,
|
||||
});
|
||||
if (confirm) {
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCredentialsAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
src: CredentialsSource,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
case CredentialsSource.REMOTE:
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
case CredentialsSource.AUTO:
|
||||
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
|
||||
}
|
||||
}
|
||||
@@ -27,18 +27,10 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
appIdentifier: metadata.appIdentifier,
|
||||
appName: metadata.appName,
|
||||
appVersion: metadata.appVersion,
|
||||
buildProfile: metadata.buildProfile,
|
||||
cliVersion: metadata.cliVersion,
|
||||
...metadata,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
gitCommitHash: metadata.gitCommitHash,
|
||||
releaseChannel: metadata.releaseChannel,
|
||||
sdkVersion: metadata.sdkVersion,
|
||||
trackingContext: metadata.trackingContext,
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getBundleIdentifier(): string {
|
||||
return 'com.bundle.id';
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readPlistAsync } from '../plist';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from '../version';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
@@ -38,9 +45,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('2');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
@@ -57,9 +66,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
|
||||
@@ -76,9 +87,11 @@ describe(bumpVersionAsync, () => {
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
@@ -99,7 +112,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('2');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
});
|
||||
@@ -115,7 +128,7 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
@@ -131,12 +144,48 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios.buildNumber).toBe('1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe(readBuildNumberAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp);
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the build number from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp);
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readShortVersionAsync, () => {
|
||||
describe('generic project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp);
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initGenericProject();
|
||||
const shortVersion = await readShortVersionAsync('/repo', exp);
|
||||
expect(shortVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initGenericProject(): ExpoConfig {
|
||||
vol.fromJSON(
|
||||
{
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import path from 'path';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { XcodeBuildContext, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { sanitizedProjectName } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { Platform } from '../types';
|
||||
import { ensureBundleIdentifierIsValidAsync } from './bundleIdentifer';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformGenericJob, transformManagedJob } from './graphql';
|
||||
import { prepareJobAsync, sanitizedTargetName } from './prepareJob';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
): Promise<() => Promise<string | undefined>> {
|
||||
const buildCtx = createBuildContext<Platform.IOS>({
|
||||
commandCtx,
|
||||
platform: Platform.IOS,
|
||||
@@ -40,33 +43,27 @@ export async function prepareIosBuildAsync(
|
||||
);
|
||||
}
|
||||
|
||||
let iosBuildScheme: string | undefined;
|
||||
let iosApplicationTarget: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
iosBuildScheme
|
||||
if (buildCtx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.exp
|
||||
);
|
||||
} else {
|
||||
if (!commandCtx.exp.name) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
iosApplicationTarget = sanitizedTargetName(commandCtx.exp.name);
|
||||
if (!iosApplicationTarget) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
}
|
||||
|
||||
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(buildCtx);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: commandCtx.projectDir,
|
||||
exp: commandCtx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosBuildScheme,
|
||||
iosApplicationTarget,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
},
|
||||
ensureCredentialsAsync: ensureIosCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: commandCtx.projectDir,
|
||||
@@ -74,7 +71,12 @@ export async function prepareIosBuildAsync(
|
||||
buildProfile: buildCtx.buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync,
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
@@ -100,6 +102,39 @@ export async function prepareIosBuildAsync(
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveXcodeBuildContextAsync(
|
||||
buildCtx: BuildContext<Platform.IOS>
|
||||
): Promise<XcodeBuildContext> {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
const buildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
return {
|
||||
buildScheme,
|
||||
buildConfiguration:
|
||||
buildCtx.buildProfile.schemeBuildConfiguration ??
|
||||
(await IOSConfig.BuildScheme.getArchiveBuildConfigurationForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildScheme
|
||||
)),
|
||||
applicationTarget: await IOSConfig.BuildScheme.getApplicationTargetNameForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildScheme
|
||||
),
|
||||
};
|
||||
} else {
|
||||
const expoName = buildCtx.commandCtx.exp.name;
|
||||
if (!expoName) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
const sanitizedExpoName = sanitizedProjectName(expoName);
|
||||
if (!sanitizedExpoName) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
return {
|
||||
buildScheme: sanitizedExpoName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.IOS>): Promise<string> {
|
||||
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
|
||||
if (schemes.length === 1) {
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureAppIdentifierIsDefinedAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Platform } from '../types';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
|
||||
enum BundleIdentiferSource {
|
||||
XcodeProject,
|
||||
AppJson,
|
||||
}
|
||||
|
||||
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(bundleIdentifier);
|
||||
}
|
||||
|
||||
export async function ensureBundleIdentifierIsValidAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform: Platform.IOS,
|
||||
exp,
|
||||
});
|
||||
if (!isBundleIdentifierValid(bundleIdentifier)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
Log.error(
|
||||
`Invalid format of iOS bundleId. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`
|
||||
);
|
||||
Log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
|
||||
throw new Error('Invalid bundleIdentifier');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
const bundleIdentifierFromConfig = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
|
||||
if (bundleIdentifierFromPbxproj !== bundleIdentifierFromConfig) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
|
||||
);
|
||||
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!hasBundleIdentifierInStaticConfig) {
|
||||
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
|
||||
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`);
|
||||
}
|
||||
Log.newLine();
|
||||
const { bundleIdentifierSource } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'bundleIdentifierSource',
|
||||
message: 'Which bundle identifier should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `${chalk.bold(bundleIdentifierFromPbxproj)} - In Xcode project`,
|
||||
value: BundleIdentiferSource.XcodeProject,
|
||||
},
|
||||
{
|
||||
title: `${chalk.bold(bundleIdentifierFromConfig)} - In your ${configDescription}`,
|
||||
value: BundleIdentiferSource.AppJson,
|
||||
},
|
||||
],
|
||||
});
|
||||
switch (bundleIdentifierSource) {
|
||||
case BundleIdentiferSource.AppJson: {
|
||||
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
|
||||
projectDir,
|
||||
bundleIdentifierFromConfig
|
||||
);
|
||||
break;
|
||||
}
|
||||
case BundleIdentiferSource.XcodeProject: {
|
||||
if (hasBundleIdentifierInStaticConfig) {
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
|
||||
});
|
||||
} else {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
|
||||
if (getConfigFilePaths(projectDir).staticConfigPath) {
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
|
||||
});
|
||||
} else {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
}
|
||||
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
|
||||
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
|
||||
projectDir,
|
||||
bundleIdentifierFromConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function missingBundleIdentifierMessage(configDescription: string): string {
|
||||
return `Please define "ios.bundleIdentifier" in ${configDescription} and run "eas build:configure" again.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static config exists and if ios.bundleIdentifier is defined there.
|
||||
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
|
||||
*/
|
||||
async function hasBundleIdentifierInStaticConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<boolean> {
|
||||
if (!exp.ios?.bundleIdentifier) {
|
||||
return false;
|
||||
}
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
return false;
|
||||
}
|
||||
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
|
||||
return rawStaticConfig?.expo?.ios?.bundleIdentifier === exp.ios.bundleIdentifier;
|
||||
}
|
||||
@@ -1,35 +1,24 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
EasJsonReader,
|
||||
VersionAutoIncrement,
|
||||
iOSBuildProfile,
|
||||
iOSGenericBuildProfile,
|
||||
} from '@expo/eas-json';
|
||||
import { IosBuildProfile, VersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import {
|
||||
configureBundleIdentifierAsync,
|
||||
ensureBundleIdentifierIsValidAsync,
|
||||
} from './bundleIdentifer';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
const disableIosBundleIdentifierValidation = (
|
||||
await new EasJsonReader(ctx.projectDir, 'all').readRawAsync()
|
||||
)?.experimental?.disableIosBundleIdentifierValidation;
|
||||
if (!disableIosBundleIdentifierValidation) {
|
||||
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureBundleIdentifierIsValidAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
warnIfBundleIdentifierDefinedInAppConfigForGenericProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
@@ -44,26 +33,12 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: iOSBuildProfile;
|
||||
buildProfile: IosBuildProfile;
|
||||
}): Promise<void> {
|
||||
const { workflow, autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const genericBuildProfile = buildProfile as iOSGenericBuildProfile;
|
||||
if (!genericBuildProfile.disableIosBundleIdentifierValidation) {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
if (
|
||||
!bundleIdentifierFromPbxproj ||
|
||||
bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier
|
||||
) {
|
||||
throw new Error(
|
||||
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
|
||||
@@ -1,73 +1,57 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { AppLookupParams } from '../../credentials/ios/credentials';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
buildCtx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
if (!shouldProvideCredentials(buildCtx)) {
|
||||
return;
|
||||
}
|
||||
assert(ctx.commandCtx.exp?.ios?.bundleIdentifier, 'ios.bundleIdentifier is required');
|
||||
return await resolveIosCredentialsAsync(ctx.commandCtx.projectDir, {
|
||||
app: {
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
bundleIdentifier: ctx.commandCtx.exp.ios.bundleIdentifier,
|
||||
},
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
|
||||
const { projectDir } = buildCtx.commandCtx;
|
||||
|
||||
const ctx = await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: buildCtx.commandCtx.nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
distribution: iOSDistributionType;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
}
|
||||
const provider = new IosCredentialsProvider(ctx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, buildCtx.commandCtx.accountName),
|
||||
`You do not have access to account: ${buildCtx.commandCtx.accountName}`
|
||||
),
|
||||
projectName: buildCtx.commandCtx.projectName,
|
||||
},
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
buildCtx.buildProfile.workflow
|
||||
),
|
||||
},
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(
|
||||
await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: params.app,
|
||||
distribution: params.distribution,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
params.workflow,
|
||||
params.credentialsSource,
|
||||
params.nonInteractive
|
||||
);
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
|
||||
return ctx.buildProfile.distribution !== 'simulator';
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return buildCtx.buildProfile.distribution !== 'simulator';
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
IosJobSecretsInput,
|
||||
IosManagedBuildType,
|
||||
IosManagedJobInput,
|
||||
IosSchemeBuildConfiguration,
|
||||
} from '../../graphql/generated';
|
||||
import { transformProjectArchive } from '../graphql';
|
||||
|
||||
@@ -21,9 +20,7 @@ export function transformGenericJob(job: Ios.GenericJob): IosGenericJobInput {
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
scheme: job.scheme,
|
||||
schemeBuildConfiguration:
|
||||
job.schemeBuildConfiguration &&
|
||||
transformSchemeBuildConfiguration(job.schemeBuildConfiguration),
|
||||
buildConfiguration: job.buildConfiguration,
|
||||
artifactPath: job.artifactPath,
|
||||
};
|
||||
}
|
||||
@@ -74,16 +71,6 @@ function transformIosSecrets(secrets: {
|
||||
};
|
||||
}
|
||||
|
||||
function transformSchemeBuildConfiguration(
|
||||
schemeBuildConfiguration: Ios.SchemeBuildConfiguration
|
||||
): IosSchemeBuildConfiguration {
|
||||
if (schemeBuildConfiguration === Ios.SchemeBuildConfiguration.DEBUG) {
|
||||
return IosSchemeBuildConfiguration.Debug;
|
||||
} else {
|
||||
return IosSchemeBuildConfiguration.Release;
|
||||
}
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Ios.ManagedBuildType): IosManagedBuildType {
|
||||
if (buildType === Ios.ManagedBuildType.DEVELOPMENT_CLIENT) {
|
||||
return IosManagedBuildType.DevelopmentClient;
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Ios,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import { ArchiveSource, Cache, Ios, Job, Workflow, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { IosGenericBuildProfile, IosManagedBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
IosCredentials,
|
||||
IosTargetCredentials,
|
||||
isCredentialsMap,
|
||||
readEnvironmentSecretsAsync,
|
||||
} from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { readEnvironmentSecretsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosBuildScheme?: string;
|
||||
iosApplicationTarget?: string;
|
||||
};
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
export async function prepareJobAsync(
|
||||
@@ -61,36 +46,21 @@ interface CommonJobProperties {
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{
|
||||
archiveBucketKey,
|
||||
credentials,
|
||||
targetName,
|
||||
}: { archiveBucketKey: string; credentials?: IosCredentials; targetName?: string }
|
||||
{ credentials, projectArchive }: { credentials?: IosCredentials; projectArchive: ArchiveSource }
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const environmentSecrets = await readEnvironmentSecretsAsync(ctx.commandCtx.projectDir);
|
||||
|
||||
let buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials && isCredentialsMap(credentials)) {
|
||||
const targets = Object.keys(credentials);
|
||||
for (const target of targets) {
|
||||
buildCredentials[target] = prepareTargetCredentials(credentials[target]);
|
||||
const buildCredentials: CommonJobProperties['secrets']['buildCredentials'] = {};
|
||||
if (credentials) {
|
||||
const targetNames = Object.keys(credentials);
|
||||
for (const targetName of targetNames) {
|
||||
buildCredentials[targetName] = prepareTargetCredentials(credentials[targetName]);
|
||||
}
|
||||
} else if (credentials) {
|
||||
// targetName
|
||||
// - for managed projects: sanitized .name from the app config
|
||||
// - for generic projects: name of the application target
|
||||
assert(targetName, 'target name should be defined');
|
||||
buildCredentials = {
|
||||
[targetName]: prepareTargetCredentials(credentials),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: archiveBucketKey,
|
||||
},
|
||||
projectArchive,
|
||||
distribution: ctx.buildProfile.distribution,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
@@ -99,9 +69,13 @@ async function prepareJobCommonAsync(
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
cocoapods: ctx.buildProfile.cocoapods,
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
cache: {
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.commandCtx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(environmentSecrets ? { environmentSecrets } : {}),
|
||||
buildCredentials,
|
||||
@@ -109,12 +83,12 @@ async function prepareJobCommonAsync(
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.TargetCredentials {
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: targetCredentials.distributionCertificate.certP12,
|
||||
password: targetCredentials.distributionCertificate.certPassword,
|
||||
dataBase64: targetCredentials.distributionCertificate.certificateP12,
|
||||
password: targetCredentials.distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -122,22 +96,18 @@ function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSGenericBuildProfile
|
||||
buildProfile: IosGenericBuildProfile
|
||||
): Promise<Partial<Ios.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.GENERIC,
|
||||
scheme: jobData.projectConfiguration.iosBuildScheme,
|
||||
schemeBuildConfiguration:
|
||||
buildProfile.schemeBuildConfiguration === 'Auto'
|
||||
? undefined
|
||||
: buildProfile.schemeBuildConfiguration,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: buildProfile.schemeBuildConfiguration,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
@@ -147,30 +117,20 @@ async function prepareGenericJobAsync(
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSManagedBuildProfile
|
||||
buildProfile: IosManagedBuildProfile
|
||||
): Promise<Partial<Ios.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
projectArchive: jobData.projectArchive,
|
||||
})),
|
||||
type: Workflow.MANAGED,
|
||||
buildType: buildProfile.buildType,
|
||||
username: accountName,
|
||||
username,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
|
||||
// it's used in eject
|
||||
export function sanitizedTargetName(name: string) {
|
||||
return name
|
||||
.replace(/[\W_]+/g, '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflow } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
@@ -100,9 +102,30 @@ export async function bumpVersionInAppJsonAsync({
|
||||
}
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
return await readPlistAsync(infoPlistPath);
|
||||
export async function readShortVersionAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<string | undefined> {
|
||||
const workflow = resolveWorkflow(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleShortVersionString;
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readBuildNumberAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<string | undefined> {
|
||||
const workflow = resolveWorkflow(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir);
|
||||
return infoPlist.CFBundleVersion;
|
||||
} else {
|
||||
return IOSConfig.Version.getBuildNumber(exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToInfoPlistAsync({
|
||||
@@ -120,6 +143,11 @@ async function writeVersionsToInfoPlistAsync({
|
||||
return updatedInfoPlist;
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
async function writeInfoPlistAsync({
|
||||
projectDir,
|
||||
infoPlist,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
const execNameOrPath =
|
||||
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
|
||||
(await findWithNodeResolution()) ??
|
||||
PLUGIN_PACKAGE_NAME;
|
||||
try {
|
||||
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
await spawnAsync(execNameOrPath, [arg], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
|
||||
Log.warn(
|
||||
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
|
||||
`npm install -g ${PLUGIN_PACKAGE_NAME}`
|
||||
)} and make sure it's in PATH.`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findWithNodeResolution(): Promise<string | undefined> {
|
||||
try {
|
||||
const resolvedPath = path.resolve(
|
||||
path.dirname(require.resolve(PLUGIN_PACKAGE_NAME)),
|
||||
'../bin/run'
|
||||
);
|
||||
if (await fs.pathExists(resolvedPath)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import { getAppIdentifierAsync } from '../project/projectUtils';
|
||||
import { getApplicationId } from '../project/android/applicationId';
|
||||
import { getBundleIdentifier } from '../project/ios/bundleIdentifier';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitCommitHashAsync } from '../utils/git';
|
||||
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
|
||||
import { readVersionCode, readVersionName } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import { readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync } from './ios/UpdatesModule';
|
||||
import { readBuildNumberAsync, readShortVersionAsync } from './ios/version';
|
||||
import { Platform } from './types';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
@@ -26,7 +31,8 @@ export async function collectMetadata<T extends Platform>(
|
||||
): Promise<Metadata> {
|
||||
return {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: ctx.commandCtx.exp.version!,
|
||||
appVersion: await resolveAppVersionAsync(ctx),
|
||||
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource,
|
||||
@@ -34,17 +40,42 @@ export async function collectMetadata<T extends Platform>(
|
||||
releaseChannel: await resolveReleaseChannel(ctx),
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
appName: ctx.commandCtx.exp.name,
|
||||
appIdentifier:
|
||||
(await getAppIdentifierAsync({
|
||||
projectDir: ctx.commandCtx.projectDir,
|
||||
platform: ctx.platform,
|
||||
exp: ctx.commandCtx.exp,
|
||||
})) ?? undefined,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
gitCommitHash: await gitCommitHashAsync(),
|
||||
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveAppVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
} else {
|
||||
return readVersionName(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAppBuildVersionAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
} else {
|
||||
const versionCode = readVersionCode(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
return versionCode !== undefined ? String(versionCode) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return getBundleIdentifier(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
} else {
|
||||
return getApplicationId(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveReleaseChannel<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
|
||||
@@ -2,7 +2,7 @@ export { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
iOS = 'ios',
|
||||
Ios = 'ios',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import * as ExpoConfig from '@expo/config';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames } from '../constants';
|
||||
|
||||
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL)
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
else if (credentialsSource === CredentialsSource.REMOTE)
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
import {
|
||||
BuildError,
|
||||
@@ -15,7 +17,7 @@ import {
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../constants';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export function printLogsUrls(
|
||||
accountName: string,
|
||||
@@ -82,10 +84,12 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
|
||||
buildId: build.id,
|
||||
account: accountName,
|
||||
});
|
||||
const installUrl = getInternalDistributionInstallUrl(build);
|
||||
qrcodeTerminal.generate(installUrl, code => console.log(`${indentString(code, 2)}\n`));
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} Open this link on your ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} devices to install the app:`
|
||||
} devices (or scan the QR code) to install the app:`
|
||||
);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
} else {
|
||||
|
||||
@@ -123,7 +123,7 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
`file://${await gitRootDirectoryAsync()}`,
|
||||
await getGitRootFullPathAsync(),
|
||||
shallowClonePath,
|
||||
]);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
@@ -152,6 +152,18 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
async function getGitRootFullPathAsync() {
|
||||
if (process.platform === 'win32') {
|
||||
// getRootDirectoryAsync() will return C:/path/to/repo on Windows and path
|
||||
// prefix should be file:///
|
||||
return `file:///${await gitRootDirectoryAsync()}`;
|
||||
} else {
|
||||
// getRootDirectoryAsync() will /path/to/repo, and path prefix should be
|
||||
// file:/// so only file:// needs to be prepended
|
||||
return `file://${await gitRootDirectoryAsync()}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function showDiffAsync() {
|
||||
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
|
||||
await gitDiffAsync({ withPager: outputTooLarge });
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import assert from 'assert';
|
||||
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getBuildLogsUrl({
|
||||
buildId,
|
||||
@@ -13,3 +16,15 @@ export function getBuildLogsUrl({
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
|
||||
}
|
||||
|
||||
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
|
||||
if (build.platform === AppPlatform.Ios) {
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
|
||||
build.project.id
|
||||
}/builds/${build.id}/manifest.plist`;
|
||||
}
|
||||
|
||||
assert(build.artifacts?.buildUrl, 'buildUrl is missing');
|
||||
|
||||
return build.artifacts.buildUrl;
|
||||
}
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getBranchNameAsync } from '../../utils/git';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
@@ -78,11 +80,8 @@ export default class BranchCreate extends Command {
|
||||
}
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
@@ -107,9 +106,7 @@ export default class BranchCreate extends Command {
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${exp.slug}`
|
||||
)}.`
|
||||
`️Created a new branch: ${chalk.bold(newBranch.name)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
GetBranchInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
async function getBranchInfoAsync({
|
||||
appId,
|
||||
@@ -44,7 +46,7 @@ async function getBranchInfoAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const branchId = data.app?.byId.updateBranchByName.id;
|
||||
const branchId = data.app?.byId.updateBranchByName?.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${name} on ${appId}`);
|
||||
}
|
||||
@@ -104,11 +106,8 @@ export default class BranchDelete extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
@@ -148,9 +147,7 @@ export default class BranchDelete extends Command {
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(
|
||||
`@${accountName}/${exp.slug}`
|
||||
)}.`
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,30 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import CliTable from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
import { format } from 'timeago.js';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
BranchesByAppQuery,
|
||||
BranchesByAppQueryVariables,
|
||||
Maybe,
|
||||
Robot,
|
||||
Update,
|
||||
User,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchesByAppQuery, BranchesByAppQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
const BRANCHES_LIMIT = 10_000;
|
||||
export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
|
||||
export async function listBranchesAsync({ projectId }: { projectId: string }) {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
|
||||
gql`
|
||||
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
|
||||
query BranchesByAppQuery($appId: String!, $limit: Int!) {
|
||||
app {
|
||||
byFullName(fullName: $fullName) {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
fullName
|
||||
updateBranches(offset: 0, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
actor {
|
||||
__typename
|
||||
@@ -43,8 +36,11 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
updatedAt
|
||||
createdAt
|
||||
message
|
||||
runtimeVersion
|
||||
group
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,14 +48,14 @@ export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
}
|
||||
`,
|
||||
{
|
||||
fullName,
|
||||
appId: projectId,
|
||||
limit: BRANCHES_LIMIT,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data?.app?.byFullName.updateBranches ?? [];
|
||||
return data?.app?.byId.updateBranches ?? [];
|
||||
}
|
||||
|
||||
export default class BranchList extends Command {
|
||||
@@ -82,13 +78,29 @@ export default class BranchList extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const branches = await listBranchesAsync({ fullName });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
if (flags.json) {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
} else {
|
||||
const table = new CliTable({ head: ['Branch', 'Latest update'] });
|
||||
table.push(...branches.map(branch => [branch.name, formatUpdate(branch.updates[0])]));
|
||||
const table = new CliTable({
|
||||
head: ['branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
table.push(
|
||||
...branches.map(branch => [
|
||||
branch.name,
|
||||
formatUpdate(branch.updates[0]),
|
||||
branch.updates[0]?.runtimeVersion ?? 'N/A',
|
||||
branch.updates[0]?.group ?? 'N/A',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
}),
|
||||
])
|
||||
);
|
||||
Log.log(chalk.bold('Branches with their most recent update group:'));
|
||||
Log.log(table.toString());
|
||||
if (branches.length >= BRANCHES_LIMIT) {
|
||||
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
|
||||
@@ -96,17 +108,3 @@ export default class BranchList extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUpdate(
|
||||
update: Pick<Update, 'id' | 'updatedAt' | 'message'> & {
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
}
|
||||
): string {
|
||||
if (!update) {
|
||||
return 'N/A';
|
||||
}
|
||||
const message = update.message ? `"${update.message}" ` : '';
|
||||
return `${message}(${format(update.updatedAt, 'en_US')} by ${getActorDisplayName(
|
||||
update.actor as any
|
||||
)})`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { getConfig, getDefaultTarget } from '@expo/config';
|
||||
import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
@@ -18,20 +19,26 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import { getLastCommitMessageAsync } from '../../utils/git';
|
||||
import { formatUpdate, listBranchesAsync } from './list';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
|
||||
type PublishPlatforms = 'android' | 'ios';
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
type PlatformFlag = PublishPlatform | 'all';
|
||||
|
||||
async function getUpdateGroupAsync({
|
||||
group,
|
||||
@@ -64,10 +71,14 @@ export default class BranchPublish extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a branch.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Name of the branch to publish on',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
branch: flags.string({
|
||||
description: 'name of the branch to publish on.',
|
||||
}),
|
||||
message: flags.string({
|
||||
description: 'Short message describing the updates.',
|
||||
required: false,
|
||||
@@ -89,6 +100,13 @@ export default class BranchPublish extends Command {
|
||||
description: `skip running Expo CLI to bundle the app before publishing`,
|
||||
default: false,
|
||||
}),
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
description: `Only publish to a single platform`,
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: `return a json with the new update group.`,
|
||||
default: false,
|
||||
@@ -97,9 +115,9 @@ export default class BranchPublish extends Command {
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
branch: name,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
@@ -107,6 +125,7 @@ export default class BranchPublish extends Command {
|
||||
'skip-bundler': skipBundler,
|
||||
},
|
||||
} = this.parse(BranchPublish);
|
||||
const platformFlag = this.parse(BranchPublish).flags.platform as PlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
@@ -117,16 +136,23 @@ export default class BranchPublish extends Command {
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug, runtimeVersion } = exp;
|
||||
let { slug, runtimeVersion, sdkVersion } = exp;
|
||||
|
||||
// When a SDK version is supplied instead of a runtime version and we're in the managed workflow
|
||||
// construct the runtimeVersion with special meaning indicating that the runtime is an
|
||||
// Expo SDK preset runtime that can be launched in Expo Go.
|
||||
const isManagedProject = getDefaultTarget(projectDir) === 'managed';
|
||||
if (!runtimeVersion && sdkVersion && isManagedProject) {
|
||||
Log.withTick('Generating runtime version from sdk version');
|
||||
runtimeVersion = getRuntimeVersionForSDKVersion(sdkVersion);
|
||||
}
|
||||
|
||||
if (!runtimeVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
@@ -134,7 +160,7 @@ export default class BranchPublish extends Command {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const branches = await listBranchesAsync({ fullName: `@${accountName}/${slug}` });
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
name = await selectAsync<string>(
|
||||
'which branch would you like to publish on?',
|
||||
branches.map(branch => {
|
||||
@@ -166,30 +192,68 @@ export default class BranchPublish extends Command {
|
||||
if (group) {
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
const updateGroups = uniqBy(updates, u => u.group).map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
// Drop into interactive mode if the user has not specified an update group to republish.
|
||||
if (jsonFlag) {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
}
|
||||
|
||||
const updateGroups = uniqBy(updates, u => u.group)
|
||||
.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return platformFlag === 'all' || update.platform === platformFlag;
|
||||
})
|
||||
.map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}". Did you mean to do a regular publish?`
|
||||
`There are no updates on branch "${name}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
const updateGroup = await selectAsync<string>(
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === updateGroup);
|
||||
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
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 "${name}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
for (const update of updatesToRepublish) {
|
||||
const { platform, manifestFragment } = update;
|
||||
updateInfoGroup[platform as PublishPlatforms] = JSON.parse(manifestFragment);
|
||||
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 plaforms 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;
|
||||
|
||||
updateInfoGroup[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublish[0].group;
|
||||
oldMessage = updatesToRepublish[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublish[0].runtimeVersion;
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
} else {
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
@@ -198,7 +262,8 @@ export default class BranchPublish extends Command {
|
||||
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const assets = collectAssets(inputDir!);
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = collectAssets({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
async function renameUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
@@ -76,12 +78,8 @@ export default class BranchRename extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!currentName) {
|
||||
const validationMessage = 'current name may not be empty.';
|
||||
@@ -123,7 +121,7 @@ export default class BranchRename extends Command {
|
||||
Log.withTick(
|
||||
`️Renamed branch from ${currentName} to ${chalk.bold(
|
||||
editedBranch.name
|
||||
)} on project ${chalk.bold(`@${accountName}/${exp.slug}`)}.`
|
||||
)} on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,13 @@ import {
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
|
||||
@@ -59,7 +63,7 @@ export async function viewUpdateBranchAsync({
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -117,12 +121,8 @@ export default class BranchView extends Command {
|
||||
}
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
@@ -152,22 +152,25 @@ export default class BranchView extends Command {
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: ['createdAt', 'message', 'group', 'actor'],
|
||||
head: UPDATE_COLUMNS,
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
groupTable.push([
|
||||
new Date(update.createdAt).toLocaleString(),
|
||||
update.message,
|
||||
formatUpdate(update),
|
||||
update.runtimeVersion,
|
||||
update.group,
|
||||
update.actor?.firstName,
|
||||
getPlatformsForGroup({
|
||||
updates: UpdateBranch.updates,
|
||||
group: update.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Branch: ${chalk.bold(UpdateBranch.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
fullName
|
||||
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
|
||||
);
|
||||
Log.log(chalk.bold('Recent update groups published on this branch:'));
|
||||
|
||||
@@ -87,7 +87,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.iOS,
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
@@ -13,22 +12,14 @@ import { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import { isGitStatusCleanAsync } from '../../build/utils/repository';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class Build extends Command {
|
||||
@@ -52,18 +43,26 @@ export default class Build extends Command {
|
||||
default: false,
|
||||
description: 'Run command in --non-interactive mode',
|
||||
}),
|
||||
local: flags.boolean({
|
||||
default: false,
|
||||
description: 'Run build locally [experimental]',
|
||||
}),
|
||||
wait: flags.boolean({
|
||||
default: true,
|
||||
allowNo: true,
|
||||
description: 'Wait for build(s) to complete',
|
||||
}),
|
||||
'clear-cache': flags.boolean({
|
||||
default: false,
|
||||
description: 'Clear cache before the build',
|
||||
}),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = this.parse(Build);
|
||||
await initAnalyticsAsync(); // TODO: run in oclif hook for every command
|
||||
try {
|
||||
const user = await ensureLoggedInAsync();
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
@@ -76,20 +75,14 @@ export default class Build extends Command {
|
||||
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
platform,
|
||||
projectId,
|
||||
user,
|
||||
});
|
||||
if (accountHasPendingBuilds) {
|
||||
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +95,9 @@ export default class Build extends Command {
|
||||
projectDir,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
clearCache: flags['clear-cache'],
|
||||
skipCredentialsCheck: flags['skip-credentials-check'],
|
||||
local: flags.local,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
waitForBuildEnd: flags.wait,
|
||||
});
|
||||
@@ -113,56 +108,15 @@ export default class Build extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNoPendingBuildsExistAsync({
|
||||
user,
|
||||
platform,
|
||||
accountName,
|
||||
projectId,
|
||||
}: {
|
||||
user: Actor;
|
||||
platform: RequestedPlatform;
|
||||
accountName: string;
|
||||
projectId: string;
|
||||
}): Promise<boolean> {
|
||||
// allow expo admins to run as many builds as they wish
|
||||
if (user.isExpoAdmin) {
|
||||
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
|
||||
if (platform === RequestedPlatform.All) {
|
||||
Log.error('Builds for multiple platforms are not supported with flag --local');
|
||||
return false;
|
||||
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
|
||||
Log.error('Unsupported platform, macOS is required to build apps for iOS');
|
||||
return false;
|
||||
}
|
||||
|
||||
const appPlatforms = toAppPlatforms(platform);
|
||||
const maybePendingBuilds = await Promise.all(
|
||||
appPlatforms.map(appPlatform => BuildQuery.getPendingBuildIdAsync(accountName, appPlatform))
|
||||
);
|
||||
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
|
||||
if (pendingBuilds.length > 0) {
|
||||
Log.newLine();
|
||||
Log.error(
|
||||
'Your other builds are still pending. Wait for them to complete before running this command again.'
|
||||
);
|
||||
Log.newLine();
|
||||
const results = await Promise.all(
|
||||
pendingBuilds.map(pendingBuild => {
|
||||
return BuildQuery.byIdAsync(nullthrows(pendingBuild).id);
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
Log.log(formatGraphQLBuild(result));
|
||||
}
|
||||
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [AppPlatform.Android, AppPlatform.Ios];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [AppPlatform.Android];
|
||||
} else {
|
||||
return [AppPlatform.Ios];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +132,7 @@ async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.iOS,
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class BuildList extends Command {
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.iOS],
|
||||
options: [RequestedPlatform.All, RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
}),
|
||||
status: flags.enum({
|
||||
options: [
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
@@ -87,12 +87,8 @@ export default class ChannelCreate extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'Channel name may not be empty.';
|
||||
@@ -146,7 +142,7 @@ export default class ChannelCreate extends Command {
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new channel ${chalk.bold(newChannel.name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
fullName
|
||||
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
UpdateChannelBranchMappingMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
@@ -126,12 +125,7 @@ export default class ChannelEdit extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to edit a specific channel.';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -10,8 +9,8 @@ import {
|
||||
GetAllChannelsForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { logChannelDetails } from './view';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
|
||||
@@ -31,18 +30,21 @@ async function getAllUpdateChannelForAppAsync({
|
||||
updateChannels(offset: $offset, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
branchMapping
|
||||
updateBranches(offset: 0, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
platform
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -82,12 +84,7 @@ export default class ChannelList extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
|
||||
appId: projectId,
|
||||
@@ -102,27 +99,10 @@ export default class ChannelList extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: ['channel', 'branch', 'update', 'message', 'created-at', 'actor'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
Log.log(); // spacing
|
||||
for (const channel of channels) {
|
||||
// TODO (cedric): refactor when multiple branches per channel are available
|
||||
const branch = channel.updateBranches[0];
|
||||
const update = branch.updates[0];
|
||||
|
||||
table.push([
|
||||
channel.name,
|
||||
branch.name,
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
Log.log(`Details of channel ${chalk.bold(chalk.cyan(channel.name))}:`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Channels for this app:}`);
|
||||
Log.log(table.toString());
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import chalk from 'chalk';
|
||||
|
||||
import { GetChannelByNameForAppQuery, UpdateBranch } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { updateChannelBranchMappingAsync } from './edit';
|
||||
@@ -44,18 +44,20 @@ function getRolloutInfo(
|
||||
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
|
||||
currentPercent: number;
|
||||
} {
|
||||
const { branchMapping } = getBranchMapping(getUpdateChannelByNameForAppResult);
|
||||
const { branchMapping } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === newBranchId
|
||||
)[0];
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
|
||||
if (!newBranch || !oldBranch) {
|
||||
throw new Error(
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.name}".`
|
||||
`Branch mapping rollout is missing a branch for channel "${getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.name}".`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,11 +136,11 @@ async function startRolloutAsync({
|
||||
],
|
||||
};
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.updateBranches.filter(
|
||||
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
|
||||
branch => branch.id === oldBranchId
|
||||
)[0];
|
||||
if (!oldBranch) {
|
||||
@@ -200,7 +202,7 @@ async function editRolloutAsync({
|
||||
newBranchMapping.data[0].branchMappingLogic.operand = percent / 100;
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
@@ -290,7 +292,7 @@ async function endRolloutAsync({
|
||||
};
|
||||
|
||||
const newChannelInfo = await updateChannelBranchMappingAsync({
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName.id!,
|
||||
channelId: getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.id!,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
const logMessage = `️Rollout on channel ${chalk.bold(
|
||||
@@ -347,19 +349,15 @@ export default class ChannelRollout extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const getUpdateChannelByNameForAppResult = await getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
channelName: channelName!,
|
||||
});
|
||||
const { branchMapping: currentBranchMapping, isRollout } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppResult
|
||||
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
|
||||
);
|
||||
|
||||
if (currentBranchMapping.data.length === 0) {
|
||||
@@ -397,7 +395,7 @@ export default class ChannelRollout extends Command {
|
||||
percent,
|
||||
jsonFlag,
|
||||
projectId,
|
||||
fullName: `@${accountName}/${slug}`,
|
||||
fullName,
|
||||
currentBranchMapping,
|
||||
getUpdateChannelByNameForAppResult,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
@@ -10,9 +11,18 @@ import {
|
||||
GetChannelByNameForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import {
|
||||
FormatUpdateParameter,
|
||||
UPDATE_COLUMNS,
|
||||
formatUpdate,
|
||||
getPlatformsForGroup,
|
||||
} from '../../update/utils';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
@@ -31,15 +41,19 @@ export type BranchMapping = {
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(
|
||||
getChannelByNameForAppQuery: GetChannelByNameForAppQuery
|
||||
branchMappingString?: string
|
||||
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
|
||||
const branchMappingString =
|
||||
getChannelByNameForAppQuery.app?.byId.updateChannelByName.branchMapping;
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
|
||||
const branchMapping: BranchMapping = JSON.parse(branchMappingString);
|
||||
if (branchMapping.version !== 0) {
|
||||
throw new Error('Branch mapping must be version 0.');
|
||||
}
|
||||
@@ -96,15 +110,17 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
updateBranches(offset: 0, limit: 1000) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
updates(offset: 0, limit: 10) {
|
||||
id
|
||||
group
|
||||
message
|
||||
runtimeVersion
|
||||
createdAt
|
||||
platform
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
@@ -123,6 +139,57 @@ export async function getUpdateChannelByNameForAppAsync({
|
||||
);
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: {
|
||||
branchMapping: string;
|
||||
updateBranches: {
|
||||
updates: (FormatUpdateParameter & {
|
||||
runtimeVersion: string;
|
||||
group: string;
|
||||
platform: string;
|
||||
})[];
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
}): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
|
||||
const table = new Table({
|
||||
head: ['branch', ...(isRollout ? ['rollout percent'] : []), ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A,',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Log.log(table.toString());
|
||||
}
|
||||
|
||||
export default class ChannelView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'View a channel on the current project.';
|
||||
@@ -153,12 +220,8 @@ export default class ChannelView extends Command {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to view a specific channel.';
|
||||
@@ -187,53 +250,12 @@ export default class ChannelView extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(
|
||||
getUpdateChannelByNameForAppresult
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'branch',
|
||||
...(isRollout ? ['rollout percent'] : []),
|
||||
'active update group',
|
||||
'update message',
|
||||
'last publish',
|
||||
'actor',
|
||||
],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const index in branchMapping.data) {
|
||||
if (parseInt(index, 10) > 1) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const { branchId } = branchMapping.data[index];
|
||||
const branch = channel.updateBranches.filter(branch => branch.id === branchId)[0];
|
||||
if (!branch) {
|
||||
throw new Error('Branch mapping is pointing at a missing branch.');
|
||||
}
|
||||
const update = branch.updates[0];
|
||||
table.push([
|
||||
branch.name,
|
||||
...(isRollout
|
||||
? [
|
||||
parseInt(index, 10) === 0
|
||||
? `${rolloutPercent! * 100}%`
|
||||
: `${(1 - rolloutPercent!) * 100}%`,
|
||||
]
|
||||
: []),
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${fullName}}. Channel ID: {bold ${channel.id}}`
|
||||
);
|
||||
Log.log(chalk`{bold Recent update groups published on this branch:}`);
|
||||
Log.log(table.toString());
|
||||
Log.log(
|
||||
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
|
||||
);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync, setProjectIdAsync } from '../project/projectUtils';
|
||||
|
||||
export default class InitView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Create or link an EAS project';
|
||||
|
||||
async run() {
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
if (exp.extra?.eas?.projectId) {
|
||||
Log.error(
|
||||
`app.json is already linked to project with ID: ${chalk.bold(exp.extra?.eas?.projectId)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await setProjectIdAsync(projectDir);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -4,12 +4,15 @@ import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { getActorDisplayName } from '../../user/User';
|
||||
@@ -47,10 +50,7 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
+11
-9
@@ -1,5 +1,5 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
@@ -26,13 +26,11 @@ export default class EnvironmentSecretDelete extends Command {
|
||||
static description = `Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'id',
|
||||
required: false,
|
||||
description: `ID of the secret to delete`,
|
||||
},
|
||||
];
|
||||
static flags = {
|
||||
id: flags.string({
|
||||
description: 'ID of the secret to delete',
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
@@ -50,11 +48,13 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
}
|
||||
|
||||
let {
|
||||
args: { id },
|
||||
flags: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
let secret: EnvironmentSecretWithScope | undefined;
|
||||
|
||||
if (!id) {
|
||||
const validationMessage = 'You must select which secret to delete.';
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectFullName);
|
||||
|
||||
({ secret } = await promptAsync({
|
||||
@@ -68,6 +68,8 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
|
||||
}));
|
||||
|
||||
id = secret?.id;
|
||||
|
||||
if (!id) throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
+1
-2
@@ -18,9 +18,8 @@ import {
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class EnvironmentSecretsList extends Command {
|
||||
export default class EnvironmentSecretList extends Command {
|
||||
static description = 'Lists environment secrets available for your current app';
|
||||
static usage = 'secrets:list';
|
||||
|
||||
async run(): Promise<void> {
|
||||
await ensureLoggedInAsync();
|
||||
@@ -1,17 +1,14 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import { learnMore } from '../log';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import {
|
||||
AndroidSubmitCommandFlags,
|
||||
IosSubmitCommandFlags,
|
||||
SubmissionPlatform,
|
||||
} from '../submissions/types';
|
||||
import { AndroidSubmitCommandFlags, IosSubmitCommandFlags } from '../submissions/types';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
const COMMON_FLAGS = '';
|
||||
@@ -159,14 +156,15 @@ export default class BuildSubmit extends Command {
|
||||
'company-name': companyName,
|
||||
|
||||
// common
|
||||
platform: platformFromParams,
|
||||
...flags
|
||||
},
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const platform = platformFromParams ?? (await promptForPlatformAsync());
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -183,7 +181,7 @@ export default class BuildSubmit extends Command {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
if (platform === SubmissionPlatform.Android) {
|
||||
if (platform === AppPlatform.Android) {
|
||||
const options: AndroidSubmitCommandFlags = {
|
||||
androidPackage,
|
||||
releaseStatus,
|
||||
@@ -193,7 +191,7 @@ export default class BuildSubmit extends Command {
|
||||
const ctx = AndroidSubmitCommand.createContext(projectDir, projectId, options);
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
} else if (platform === SubmissionPlatform.iOS) {
|
||||
} else if (platform === AppPlatform.Ios) {
|
||||
const options: IosSubmitCommandFlags = {
|
||||
appleId,
|
||||
ascAppId,
|
||||
@@ -213,7 +211,7 @@ export default class BuildSubmit extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
async function promptForPlatformAsync(): Promise<AppPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Submit to platform',
|
||||
@@ -221,11 +219,11 @@ async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
choices: [
|
||||
{
|
||||
title: 'iOS',
|
||||
value: SubmissionPlatform.iOS,
|
||||
value: AppPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: SubmissionPlatform.Android,
|
||||
value: AppPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateGroupMutation,
|
||||
UpdateMutationDeleteUpdateGroupArgs,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
|
||||
async function deleteUpdateGroupAsync({
|
||||
group,
|
||||
}: {
|
||||
group: string;
|
||||
}): Promise<DeleteUpdateGroupMutation> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateGroupMutation, UpdateMutationDeleteUpdateGroupArgs>(
|
||||
gql`
|
||||
mutation DeleteUpdateGroup($group: ID!) {
|
||||
update {
|
||||
deleteUpdateGroup(group: $group) {
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ group }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class UpdateDelete extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Delete all the updates in an update Group.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'groupId',
|
||||
required: true,
|
||||
description: 'The ID of an update group to delete.',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `Return a json with the group ID of the deleted updates.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
args: { groupId: group },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(UpdateDelete);
|
||||
|
||||
if (!jsonFlag) {
|
||||
const shouldAbort = await confirmAsync({
|
||||
message:
|
||||
`🚨${chalk.red('CAUTION')}🚨\n\n` +
|
||||
`${chalk.yellow(`This will delete all of the updates in group "${group}".`)} ${chalk.red(
|
||||
'This is a permanent operation.'
|
||||
)}\n\n` +
|
||||
`If you want to revert to a previous publish, you should use 'branch:publish --republish' targeted at the last working update group instead.\n\n` +
|
||||
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'branch:publish --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent cacheing behavior by clients.\n\n` +
|
||||
`Would you like to abort?`,
|
||||
});
|
||||
|
||||
if (shouldAbort) {
|
||||
Log.log('Aborted.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUpdateGroupAsync({ group });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify({ group }));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.withTick(`Deleted update group ${group}`);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
export default class UpdateShow extends Command {
|
||||
static hidden = true;
|
||||
static description = 'details about a particular revision';
|
||||
|
||||
async run() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdatesByGroupQuery, UpdatesByGroupQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { UPDATE_COLUMNS, formatUpdate, getPlatformsForGroup } from '../../update/utils';
|
||||
|
||||
export async function viewUpdateAsync({
|
||||
groupId,
|
||||
}: {
|
||||
groupId: string;
|
||||
}): Promise<UpdatesByGroupQuery> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<UpdatesByGroupQuery, UpdatesByGroupQueryVariables>(
|
||||
gql`
|
||||
query UpdatesByGroup($groupId: ID!) {
|
||||
updatesByGroup(group: $groupId) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
platform
|
||||
message
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
username
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
if (data.updatesByGroup.length === 0) {
|
||||
throw new Error(`Could not find any updates with group ID: "${groupId}"`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
export default class UpdateView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Update group details.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'groupId',
|
||||
required: true,
|
||||
description: 'The ID of an update group.',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `Return a json with the updates belonging to the group.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
args: { groupId },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(UpdateView);
|
||||
|
||||
const { updatesByGroup } = await viewUpdateAsync({ groupId });
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(updatesByGroup));
|
||||
return;
|
||||
}
|
||||
|
||||
const groupTable = new Table({
|
||||
head: [...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
const representativeUpdate = updatesByGroup[0];
|
||||
groupTable.push([
|
||||
formatUpdate(representativeUpdate),
|
||||
representativeUpdate.runtimeVersion,
|
||||
representativeUpdate.group,
|
||||
getPlatformsForGroup({
|
||||
updates: updatesByGroup,
|
||||
group: updatesByGroup[0]?.group,
|
||||
}),
|
||||
]);
|
||||
|
||||
Log.log(groupTable.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export interface CredentialsProvider {
|
||||
platform: Platform;
|
||||
hasRemoteAsync(): Promise<boolean>;
|
||||
hasLocalAsync(): Promise<boolean>;
|
||||
isLocalSyncedAsync(): Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreFragment,
|
||||
AndroidKeystoreType,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { testKeystore } from './fixtures-android';
|
||||
import { testKeystoreBase64 } from './fixtures-base64-data';
|
||||
|
||||
export const testAppFragment: AppFragment = {
|
||||
id: 'test-app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
slug: 'testapp',
|
||||
};
|
||||
|
||||
export const testJksAndroidKeystoreFragment: AndroidKeystoreFragment = {
|
||||
id: 'test-id',
|
||||
type: AndroidKeystoreType.Jks,
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: testKeystore.keystorePassword,
|
||||
keyAlias: testKeystore.keyAlias,
|
||||
keyPassword: testKeystore.keyPassword,
|
||||
md5CertificateFingerprint: 'test-md5',
|
||||
sha1CertificateFingerprint: 'test-sha1',
|
||||
sha256CertificateFingerprint: 'test-sha256',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export const testLegacyAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: true,
|
||||
name: 'legacy',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testLegacyAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: true,
|
||||
app: testAppFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testAndroidBuildCredentialsFragment: AndroidAppBuildCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
isDefault: true,
|
||||
isLegacy: false,
|
||||
name: 'Google App Store Build Credentials',
|
||||
androidKeystore: testJksAndroidKeystoreFragment,
|
||||
};
|
||||
|
||||
export const testAndroidAppCredentialsFragment: CommonAndroidAppCredentialsFragment = {
|
||||
id: 'test-id',
|
||||
applicationIdentifier: null,
|
||||
isLegacy: false,
|
||||
app: testAppFragment,
|
||||
androidAppBuildCredentialsList: [testLegacyAndroidBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export function getNewAndroidApiMockWithoutCredentials() {
|
||||
return {
|
||||
getAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsListAsync: jest.fn(() => []),
|
||||
getLegacyAndroidAppCredentialsWithCommonFieldsAsync: jest.fn(),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
updateAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
createAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(),
|
||||
getAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createOrUpdateAndroidAppBuildCredentialsByNameAsync: jest.fn(),
|
||||
createKeystoreAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,44 @@
|
||||
import { AndroidCredentials } from '../android/credentials';
|
||||
import { testKeystore2Base64, testKeystoreBase64 } from './fixtures-base64-data';
|
||||
import { AndroidCredentials, Keystore } from '../android/credentials';
|
||||
import {
|
||||
testKeystore2Base64,
|
||||
testKeystoreBase64,
|
||||
testPKCS12KeystoreBase64,
|
||||
testPKCS12KeystoreEmptyPasswordBase64,
|
||||
} from './fixtures-base64-data';
|
||||
import { testExperienceName, testJester2ExperienceName } from './fixtures-constants';
|
||||
|
||||
export const testKeystore = {
|
||||
export const testKeystore: Keystore = {
|
||||
keystore: testKeystoreBase64,
|
||||
keystorePassword: 'ae6777e9444a436dbe533d2be46c83ba',
|
||||
keyAlias: 'QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==',
|
||||
keyPassword: '43f760fe7ecd4e6a925779eb45bc787b',
|
||||
};
|
||||
export const testKeystore2 = {
|
||||
export const testKeystore2: Keystore = {
|
||||
keystore: testKeystore2Base64,
|
||||
keystorePassword: '6faeed2326b94effadbeb731510c2378',
|
||||
keyAlias: 'QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA==',
|
||||
keyPassword: 'e4829b38057042d78f25053f390478f9',
|
||||
};
|
||||
|
||||
// Generated by: keytool -genkey -alias test-alias -keystore keystore.p12 -storetype PKCS12 -keyalg RSA -storepass password -validity 730 -keysize 2048
|
||||
// A Keystore in a PKCS#12 file, where all entries are under aliases
|
||||
// This particular PKCS#12 has one key and one certificate under 'test-alias'
|
||||
export const testPKCS12Keystore: Keystore = {
|
||||
keystore: testPKCS12KeystoreBase64,
|
||||
keystorePassword: 'password',
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
// openssl req -new -newkey rsa:4096 -nodes -keyout test.key -out test.csr
|
||||
// openssl x509 -req -sha256 -days 365 -in test.csr -signkey test.key -out test.pem
|
||||
// openssl pkcs12 -export -in test.pem -inkey test.key -name "test-alias" -passout pass: -out emptyPassword.p12
|
||||
// cat emptyPassword.p12 | base64
|
||||
export const testPKCS12EmptyPasswordKeystore: Keystore = {
|
||||
keystore: testPKCS12KeystoreEmptyPasswordBase64,
|
||||
keystorePassword: '',
|
||||
keyAlias: 'test-alias',
|
||||
};
|
||||
|
||||
export const testPushCredentials = {
|
||||
fcmApiKey: 'examplefcmapikey',
|
||||
};
|
||||
|
||||
@@ -4,4 +4,15 @@ export const testKeystoreBase64 =
|
||||
export const testKeystore2Base64 =
|
||||
'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';
|
||||
|
||||
export const testPKCS12KeystoreBase64 =
|
||||
'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';
|
||||
|
||||
export const testPKCS12KeystoreEmptyPasswordBase64 =
|
||||
'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
Line truncated
|
||||
|
||||
export const testProvisioningProfileBase64 = `MIIdDQYJKoZIhvcNAQcCoIIc/jCCHPoCAQExCzAJBgUrDgMCGgUAMIINoAYJKoZIhvcNAQcBoIINkQSCDY08P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+CjxkaWN0PgoJPGtleT5BcHBJRE5hbWU8L2tleT4KCTxzdHJpbmc+dGVzdGFwcCA8L3N0cmluZz4KCTxrZXk+QXBwbGljYXRpb25JZGVudGlmaWVyUHJlZml4PC9rZXk+Cgk8YXJyYXk+Cgk8c3RyaW5nPlFMNzZYWUg3M1A8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PgoJPGRhdGU+MjAyMC0wNy0xM1QxMjozNzowM1o8L2RhdGU+Cgk8a2V5PlBsYXRmb3JtPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5pT1M8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PklzWGNvZGVNYW5hZ2VkPC9rZXk+Cgk8ZmFsc2UvPgoJPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT4KCTxhcnJheT4KCQk8ZGF0YT5NSUlGb1RDQ0JJbWdBd0lCQWdJSU9UWW9sUmhEaTlrd0RRWUpLb1pJaHZjTkFRRUxCUUF3Z1pZeEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVN3d0tnWURWUVFMRENOQmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3pGRU1FSUdBMVVFQXd3N1FYQndiR1VnVjI5eWJHUjNhV1JsSUVSbGRtVnNiM0JsY2lCU1pXeGhkR2x2Ym5NZ1EyVnlkR2xtYVdOaGRHbHZiaUJCZFhSb2IzSnBkSGt3SGhjTk1qQXdOekV6TVRJeU5qRTJXaGNOTWpFd056RXpNVEl5TmpFMldqQ0JsREVhTUJnR0NnbVNKb21UOGl4a0FRRU1DbEZNTnpaWVdVZzNNMUF4T2pBNEJnTlZCQU1NTVdsUWFHOXVaU0JFYVhOMGNtbGlkWFJwYjI0NklFRnNhV05xWVNCWFlYSmphR0hGZ2lBb1VVdzNObGhaU0RjelVDa3hFekFSQmdOVkJBc01DbEZNTnpaWVdVZzNNMUF4R0RBV0JnTlZCQW9NRDBGc2FXTnFZU0JYWVhKamFHSEZnakVMTUFrR0ExVUVCaE1DVlZNd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNuTUNPT2VqWjEvUjBBTTJ3Sis0cjBDc2F3N0x3N2xFenBTREQ5OUd4TjA2ZkZ3cW1HRnBWdFhsRzhWYUN0cW1sZzl5MzJiMVltd1pJcXJRR3FteVY1cUk4RDJvTzh2QjVvRDJVT3IrUHgzOTQ0YnQyZFFZaUlHQTNlZi81SktZa1E3VDhXWXFQczg3Q1NORnZ5MHhJa1dHbFBpa0dhd0xXcmtzZmJUNFdkMU90dVhiYUtoTEZGNmp5alU0ZCtYN2NoNGtQOFFvdHlkWk5vWmltQTVTYWNtVmRidUM0YTlvQStPL3dscUliaXdiTUtGQlE2Q29tRTFKelpQaWtXMDFoZ1V2T2kvSEtKb0Nsdnp5SmZMLzhZbm43dDRSdUs5bmxHaHNIanJ5b3hFRDdiTGZpcUlKRTllcWlvNTBTK2cycFllWS9tNUh6SW04V1lOZ0RXcjFsTkFnTUJBQUdqZ2dIeE1JSUI3VEFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGSWduRndtcHRoaGdpK3pydXZaSFdjVlNWS08zTUQ4R0NDc0dBUVVGQndFQkJETXdNVEF2QmdnckJnRUZCUWN3QVlZamFIUjBjRG92TDI5amMzQXVZWEJ3YkdVdVkyOXRMMjlqYzNBd015MTNkMlJ5TVRFd2dnRWRCZ05WSFNBRWdnRVVNSUlCRURDQ0FRd0dDU3FHU0liM1kyUUZBVENCL2pDQnd3WUlLd1lCQlFVSEFnSXdnYllNZ2JOU1pXeHBZVzVqWlNCdmJpQjBhR2x6SUdObGNuUnBabWxqWVhSbElHSjVJR0Z1ZVNCd1lYSjBlU0JoYzNOMWJXVnpJR0ZqWTJWd2RHRnVZMlVnYjJZZ2RHaGxJSFJvWlc0Z1lYQndiR2xqWVdKc1pTQnpkR0Z1WkdGeVpDQjBaWEp0Y3lCaGJtUWdZMjl1WkdsMGFXOXVjeUJ2WmlCMWMyVXNJR05sY25ScFptbGpZWFJsSUhCdmJHbGplU0JoYm1RZ1kyVnlkR2xtYVdOaGRHbHZiaUJ3Y21GamRHbGpaU0J6ZEdGMFpXMWxiblJ6TGpBMkJnZ3JCZ0VGQlFjQ0FSWXFhSFIwY0RvdkwzZDNkeTVoY0hCc1pTNWpiMjB2WTJWeWRHbG1hV05oZEdWaGRYUm9iM0pwZEhrdk1CWUdBMVVkSlFFQi93UU1NQW9HQ0NzR0FRVUZCd01ETUIwR0ExVWREZ1FXQkJUTGhSdGRhRVRjRmx4Q0grZnRGQ3d4U3UvZmlUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0V3WUtLb1pJaHZkalpBWUJCQUVCL3dRQ0JRQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSEJxL2VSeitIYTRtemZ0cHZ0SGQ4TTFNb2d0c01UaVNNVDFZK0VGbUQ5UWZYdXdCM3F1ckhQbHFiMjFvV01CWU9TaWcwRDlFZG9Ta29STXAwdU5kaUltVnMycVc0bEgvQ25zL0w1QkhqU2NQc3kwbXUrSitlQjZXZzNXTDduNm1pM3BlOCtxZ2dxak41RVU2YW1Ca04wMmU3Z0pMZnlkcHRSRlBOaklOWFh6bDkxOVNDeUo2VXl3OGFUblBrZFc5eTI1UHgrUHJBSmNDVm1hbFRlYkJvNWxnbUNKejlKUkU3Q3pQd2xmaVBRbUZRMlJVTEpONTBrMmRGSmtESk1jUHd1OFdvUmNBMFE3M21NU256OVNiS21UcHhzeUNEUUo2WHJ5Sm1vKy9aQmEvM0k0dE15dkNBUVQ1alNLWjFrUXJ4c2xFdGJHZE5Bb0xUaUJ6dHY3Wjk0PTwvZGF0YT4KCTwvYXJyYXk+CgoJCQkJCQkJCQkJCgk8a2V5PkVudGl0bGVtZW50czwva2V5PgoJPGRpY3Q+CgkJPGtleT5iZXRhLXJlcG9ydHMtYWN0aXZlPC9rZXk+CgkJPHRydWUvPgoJCQkJCgkJCQk8a2V5PmFwcGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT4KCQk8c3RyaW5nPlFMNzZYWUg3M1Aub3JnLnJlYWN0anMubmF0aXZlLmV4YW1wbGUudGVzdGFwcC50dXJ0bGV2Mjwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT4KCQk8YXJyYXk+CgkJCQk8c3RyaW5nPlFMNzZYWUg3M1AuKjwvc3RyaW5nPgoJCQkJPHN0cmluZz5jb20uYXBwbGUudG9rZW48L3N0cmluZz4KCQk8L2FycmF5PgoJCQkJCgkJCQk8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+CgkJPGZhbHNlLz4KCQkJCQoJCQkJPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcjwva2V5PgoJCTxzdHJpbmc+UUw3NlhZSDczUDwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmFwcy1lbnZpcm9ubWVudDwva2V5PgoJCTxzdHJpbmc+cHJvZHVjdGlvbjwvc3RyaW5nPgoKCTwvZGljdD4KCTxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT4KCTxkYXRlPjIwMjEtMDctMTNUMTI6MjY6MTZaPC9kYXRlPgoJPGtleT5OYW1lPC9rZXk+Cgk8c3RyaW5nPm9yZy5yZWFjdGpzLm5hdGl2ZS5leGFtcGxlLnRlc3RhcHAudHVydGxldjIgcHJvZmlsPC9zdHJpbmc+Cgk8a2V5PlRlYW1JZGVudGlmaWVyPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5RTDc2WFlINzNQPC9zdHJpbmc+Cgk8L2FycmF5PgoJPGtleT5UZWFtTmFtZTwva2V5PgoJPHN0cmluZz5BbGljamEgV2FyY2hhxYI8L3N0cmluZz4KCTxrZXk+VGltZVRvTGl2ZTwva2V5PgoJPGludGVnZXI+MzY0PC9pbnRlZ2VyPgoJPGtleT5VVUlEPC9rZXk+Cgk8c3RyaW5nPjQ1NjBiNTU1LWFhODMtNDRiNy1iYjMxLTAwMGIwMDZjNDM2ZDwvc3RyaW5nPgoJPGtleT5WZXJzaW9uPC9rZXk+Cgk8aW50ZWdlcj4xPC9pbnRlZ2VyPgo8L2RpY3Q+CjwvcGxpc3Q+oIIMsjCCA/MwggLboAMCAQICARcwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTA3MDQxMjE3NDMyOFoXDTIyMDQxMjE3NDMyOFoweTELMAkGA1UEBhMCVVMxE
Line truncated
|
||||
|
||||
// Generated using instructions from https://support.magplus.com/hc/en-us/articles/203808748-iOS-Creating-a-Distribution-Certificate-and-p12-File
|
||||
// When exporting from kechain password was left blank
|
||||
export const testDistributionCertificateEmptyPasswordBase64 =
|
||||
'MIIMlwIBAzCCDF4GCSqGSIb3DQEHAaCCDE8EggxLMIIMRzCCBs8GCSqGSIb3DQEHBqCCBsAwgga8AgEAMIIGtQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIbxms3H70KbsCAggAgIIGiCAdgBcty+2paB8pfNSvARDGIMeFZq8dwUcEFCeTZgk9+u/Tt7t9S7u6sJgrxLmtBbugkAGHPpjaf9x7C39UOwSjNj9SbI3xZ2A0kzCAseVcg6ezqJaVfI262SFbJGonZC+qBPDSlQksCBpkYr9mA57F9Y5c9b3jLH8hmdvQgXKp1lADJyBGUjXSJewgFrszbglFtrPdfC2kjo84gjcGp2aeD97mIiC+VlIkRWjbbymR5D3eUf7iH4vcj7dzLX+KuXay6oP/Aw96jvqhHWM5UlUqywrnwVl8dRTxU30rl6IwbbzPbURMDbPdFZHkNk6Zdq4+qZemMqNINfnZ+8Fn3G1Q2lZBqmlwca8DsM6KiQe+kWNUuEHFBUAWtIEd2TBC7O5uEa0II2JxYTXqWs5IaoV6H4qrFmWrW/FeJMKoCJpRvyrK0ohvJKsweSTG1o0//t72SZcCZBBBrpq1zewMFDYmdrgPYDcCDTmn5m74n3bVbMGlKqe8e7juG+8EFz7wSulL9G9o27+d/zira3LW5k4CjFW5b4IOT6RREBA3CiURZRgPJ5TQn7p0a3yQv6XLKHtH9/s2lhcBKhgkADZq9vjptxfurYJ/TyXo2MmjTazM6GFz699PXhYd5vSMsUr5w2+ltLjh7N/ulV6U03D0tgvXy0/41HzwrIHsw59PQTxjiR9b8H3GeZw9LuvRw4hQC45vnn1Zhjld+czRk3idBpzAQz3C5C6HKvTgY0ATOF/VcBsY7iRooz0CYtpy5+EGB0Vecp7p4U0JycmoxJpDlwuy/6wnyOcMLCsRNAXY+zE+UTdq6G3AkAJEl//2hCAKh8PdqIFcyacBRKF+kqEGoLAeiyJD3aGVdiYXHU3J9ijtJxp0ynF/GuUKrvRhsVFNll1AhUlXgNE8eJMUo+ra60C0zpMGUCr/DW/5gzq3+2mmiXxPuL+pcUXH28Tih1k+LIJxcxEFyQk6wBxKjSV3M3VMnueXzO9QXPrZ4Tzo5j+SyqDXCMJRz2USfp6nnBZSPqPnmgSRG5oaXZ1WqAF+mPYFJQr0kJ1/llCeohVEa18/NaejXEvO5SabroPjq8KAAX+IlLZoxm9zf2P6cOOueGyUTocrO5moGsfv/jA7yI+fHlPYSABsNZcdOIs3A5TZMwK1OWokdab7seRpMHdV+t2CuVF0vCnTwRK6KTW2ws/73OxiZ+AXVQGb9Z5XsIhV5vuoXrHWBVLO/WkKbOSMz3URlfghe7xYw/6/xCy8eoqFot/pRj5k+ue9HHLfRhvlgk+zRNBS7RxIYa/i4/7VqTaP5GBfKecjUXrzp2Q0BzJhstHOYbB2ga//DOzUXWql61aW++0YyXNILNPrV+il0NSXUG8IinYx6nVIOwN7rWcZawxtULz8mGaKmKkOes0mSCCFZdXGXH0YcpzqjoCpJRTi9Wohi4crrKuOyaojavudQp5Khuik21Z+xKsM/SaQbXb+Hg7kcOnhKQmRURFt6/zsZXGMRTGoqICpsQkkIW6op0qkb0mJ5wjlk0XHzlywV4rtMH5apg7l5H0IXZRL/j2uvD8gJbQvN2M9aO/QlSsqFtSlB1A8KiGM4uw5bnWMNho+qFQsD3gn1WK6nQRhvw2UubfMOpOIhptYic6ZlfjDJIkVih9+paavWoALl4/yfhgI4rrQMoLdgu+lw/iOkRx7Zs/mibx6CudZvwBxYkarUD/4nZpmLfuEOlNETOSom3xwuvVJrT2nZSd6I+40Um7pzPYXtsgkgY1VGyVlUfD/a4U56RhJQK51X5TebQNnA9ylwN9husrQg4wtUyxcJPYfozbaDtc818V6aXdBgMOI664piSikpeGqI+2W3KaaNuJIO0YcvgNwfFMBr99ZDadmETDt3tKLrQZPqGhqislJtFG15C/LPWNtQgLu6is052OjbISXF6wksAIOwCttWaYk+tetF9H+4ZAx4YoLcocfX8vKeZc69KqmhOZi1prnstwgyBWQcdzb5HN7DLd1lhmqOHXRnFqT7lnsyM6Wok6m1Rq0D9AxuPXJzvcfNN3jpVUahCMOonaBmBUYLyNpcIm3fM93kb5Z5pnYhNnnigVuUf4gH4AgfVkFkgpOW+I5WbsukWHbS1tkNkOXo/A/8dW1ovLkv0FS+uf5h4OX6pmduellaCqX0NlEeP3M0fsG5NN5VrsKgKsasUuZsZ8shUhEkOINIl5ibPlsZiYHmQYK9Zkgm3d2cHUwggVwBgkqhkiG9w0BBwGgggVhBIIFXTCCBVkwggVVBgsqhkiG9w0BDAoBAqCCBO4wggTqMBwGCiqGSIb3DQEMAQMwDgQI1SsyhsysJ20CAggABIIEyI3hCegwo0DAV4GUbBs62Z0V4C2KmeFYG9DkAYnFvNRSAEnE8vfWz/iFCLGtpFo17otMspKMvX1V5AOfUlKcpsl2kDgxoqNs0ILoXLTq+RWO8gZ5CKXZxLpsoCa5g/Adleoliq315s4HrdHeXUPp5rRR0yEcI0gFoB3r6D/fETGJ4rW4J9dvWjYzIRySMjRok4dMRN4c2Olospfn9Ig8+E4M6anpuy5Q3oMhjMv2rf93siqqzKv+AdIeBKtTn++LuOj/imetCVM7B+wp+jc22W33gwxhsY5rPcm3yUXm5fFOdevEk2bXISwbKUZIgr/J7AmFexzjzDlsPWj5qA2sV62NX+wp0bRvxBQGv2he5h1sAdhEHG8PgyYftDUEdkjhipQ1V02h2dnd//kuRfQQvQXfkmEFwiSLpjmwNGe7+UPsUCp/23s2rgd5IlmJJhBZ1jEEQetntK6GE0FQceERtindwqmfvdxDPgx/A4Veir+nE/bVnXHuY9HT+MCVOoOtHd6mq59VjbEMDxpeYquhYfIVtjasNYmHogB0Vh8w4A8sqUUEWYU7XwOkdk3DVDPcH1zOdIUAZDqJtgxGHKIhAY7eUjrs2fRsJa5YjfKxw26E4OBQXcMBXIiXw1/fGyeksk0vQLw6gtM9W2PboaD9UOMySlAX8NgCCjekI67EBrmT7sienBo07YqTUnNyGn+cfwW+xWcCsLIQAtzkvj6D/Qp3X8Iix/tM76JEgj5XlqkfzrFgp6/DQnsovwI0rT4WNfdXORQ7OvDaP9pxpgjtMPtnU9FVKNFrUFnS5Gox24+ezRD0GMCDcHT0qjDaZt2qZ2Uhw+jorDCShK/BK0at7jjvpJNqBgdaHvuSzNEyW4GOWid/xwTswjMIjbC3Va77WXj0ECe5oiySoQOrO7yfp+l9a6WRkiWWINsGrjgKeTuP8JYGCKxKI6wn/EdjOEdoEMDQo7fSWvTfaSEeWvQhw1vHnc/gRk6e0EYHSAjEOnDGujAlfOoYJFS8ZblyQzWtxs0l4BXUUH8NuyzM484znBKxA35FVOeWEpDwpNlZTXFd90DSEk0nppmzO0yjzoQo2P48D3s7ohNXt9hpLAjLMm2Nus+eOw1Jw4eoCgdBQ1TkBdH5BCPywuhLYXGECe8rrOYmsBYxhSxE5cUxQjqHBD4ke0jND5ZeOQEu+gqK/wrNBI1Y2jCPXN+gOuURXqUznRPt7VRRNhDZzfdp/yvSU1iGHf0crtW3MuFsQSpg5Vh0Bp9DAUoFI+uzOinsgRWv/tmTW0MO5MRS+QluAFNXNmGPfP8FRcYYuud4W1JdcB1qaLzCrapHxYhPQ91HU51cAmd0xrQ+sm8I/mwL4FqGGzDkNaV85JSQ6hmSU6nOJiPAdMhfptmkzU3lImfN/KWyY5BOCPEnWYwGVCpUVguqA2tBRp36fE8LGw4UoSmGHGTO4/yqNZ3gadodyj2YPidRWUQyHBpMf3GGrBNkw9fNgrhEZOJly8FPIHShWg+8vJq4zzg44dbxldun7sGYGtWFLP0hB9sjQMf6Xb4QM77PIYqnzKWb37qu7MeLKisqnhQXgOdltfCVEsD8cLmsiBSIhbbTrVOZ1eXdp7GSQj0TG74hEIRLtWpcuzFUMC0GCSqGSIb3DQEJFDEgHh4AVwBvAGoAYwBpAGUAYwBoACAASwBvAHoAeQByAGEwIwYJKoZIhvcNAQkVMRYEFOZtLT3J9PWFMsJpTiXQeVHfjaZrMDAwITAJBgUrDgMCGgUABBRoxaaqcbwULmfILJR1w2Sikq8pUwQIE6Ey2sQzzokCAQE=';
|
||||
@@ -5,6 +5,7 @@ export const jester: Actor = {
|
||||
id: 'jester-id',
|
||||
username: 'jester',
|
||||
accounts: [{ id: 'jester-account-id', name: 'jester' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const jester2: Actor = {
|
||||
@@ -12,6 +13,7 @@ export const jester2: Actor = {
|
||||
id: 'jester2-id',
|
||||
username: 'jester2',
|
||||
accounts: [{ id: 'jester2-account-id', name: 'jester2' }],
|
||||
isExpoAdmin: false,
|
||||
};
|
||||
|
||||
export const testUsername = jester.username;
|
||||
@@ -32,6 +34,7 @@ export const testAppJson = {
|
||||
slug: testSlug,
|
||||
sdkVersion: '38.0.0',
|
||||
ios: { bundleIdentifier: testBundleIdentifier },
|
||||
android: { package: testPackageName },
|
||||
};
|
||||
|
||||
export const testAppJsonWithDifferentOwner = {
|
||||
|
||||
@@ -3,17 +3,18 @@ import merge from 'lodash/merge';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { Context } from '../context';
|
||||
import { getAndroidApiMockWithoutCredentials } from './fixtures-android';
|
||||
import { getNewAndroidApiMockWithoutCredentials } from './fixtures-android-new';
|
||||
import { getAppstoreMock } from './fixtures-appstore';
|
||||
import { testAppJson, testUsername } from './fixtures-constants';
|
||||
import { getIosApiMockWithoutCredentials } from './fixtures-ios';
|
||||
import { getNewIosApiMockWithoutCredentials } from './fixtures-new-ios';
|
||||
import { getNewIosApiMockWithoutCredentials } from './fixtures-ios';
|
||||
|
||||
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
|
||||
const defaultMock = {
|
||||
ios: getIosApiMockWithoutCredentials(),
|
||||
newIos: getNewIosApiMockWithoutCredentials(),
|
||||
ios: getNewIosApiMockWithoutCredentials(),
|
||||
android: getAndroidApiMockWithoutCredentials(),
|
||||
newAndroid: getNewAndroidApiMockWithoutCredentials(),
|
||||
appStore: getAppstoreMock(),
|
||||
bestEffortAppStoreAuthenticateAsync: jest.fn(),
|
||||
ensureAppleCtx: jest.fn(),
|
||||
ensureProjectContext: jest.fn(),
|
||||
user: {
|
||||
@@ -37,5 +38,5 @@ export function createManagerMock(mockOverride: Record<string, any> = {}): Crede
|
||||
}),
|
||||
},
|
||||
mockOverride
|
||||
);
|
||||
) as any;
|
||||
}
|
||||
@@ -4,45 +4,25 @@ import {
|
||||
AppleDistributionCertificateFragment,
|
||||
AppleProvisioningProfileFragment,
|
||||
AppleTeamFragment,
|
||||
CommonIosAppCredentialsFragment,
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../graphql/generated';
|
||||
import { IosAppCredentialsWithBuildCredentialsQueryResult } from '../ios/api/graphql/queries/IosAppCredentialsQuery';
|
||||
import {
|
||||
DistributionCertificate,
|
||||
DistributionCertificateStoreInfo,
|
||||
ProvisioningProfile,
|
||||
ProvisioningProfileStoreInfo,
|
||||
PushKey,
|
||||
PushKeyStoreInfo,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfiles = [testProvisioningProfile];
|
||||
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
expires: tomorrow.getTime(),
|
||||
distributionMethod: 'test',
|
||||
certificates: [],
|
||||
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
|
||||
provisioningProfile: testProvisioningProfile.provisioningProfile,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
|
||||
|
||||
export const testAppleTeamFragment: AppleTeamFragment = {
|
||||
id: 'test-apple-team-id-1',
|
||||
@@ -64,7 +44,7 @@ export const testProvisioningProfileFragment: AppleProvisioningProfileFragment =
|
||||
id: 'test-prov-prof-id-1',
|
||||
expiration: new Date(),
|
||||
developerPortalIdentifier: 'test-developer-identifier',
|
||||
provisioningProfile: 'test-provisioning-profile',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
updatedAt: new Date(),
|
||||
status: 'Active',
|
||||
appleTeam: { ...testAppleTeamFragment },
|
||||
@@ -117,11 +97,61 @@ export const testIosAppBuildCredentialsFragment: IosAppBuildCredentialsFragment
|
||||
provisioningProfile: testProvisioningProfileFragment,
|
||||
};
|
||||
|
||||
export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragment = {
|
||||
id: 'test-common-ios-app-credentials-id',
|
||||
app: testAppFragment,
|
||||
appleTeam: testAppleTeamFragment,
|
||||
appleAppIdentifier: testAppleAppIdentifierFragment,
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testIosAppCredentialsWithBuildCredentialsQueryResult: IosAppCredentialsWithBuildCredentialsQueryResult = {
|
||||
id: 'test-app-credential-id',
|
||||
iosAppBuildCredentialsArray: [testIosAppBuildCredentialsFragment],
|
||||
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
|
||||
};
|
||||
|
||||
export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
|
||||
export function getNewIosApiMockWithoutCredentials() {
|
||||
return {
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewIosApiMockWithCredentials() {
|
||||
return {
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: jest.fn(),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export const testDistCert: DistributionCertificate = {
|
||||
certP12: 'Y2VydHAxMg==',
|
||||
certPassword: 'test-password',
|
||||
@@ -133,18 +163,6 @@ export const testIosDistCredential: IosDistCredentials = {
|
||||
type: 'dist-cert',
|
||||
...testDistCert,
|
||||
};
|
||||
export const testIosDistCredentials = [testIosDistCredential];
|
||||
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
|
||||
id: 'test-id',
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
created: today.getTime(),
|
||||
expires: tomorrow.getTime(),
|
||||
ownerName: 'test-owner',
|
||||
ownerId: 'test-id',
|
||||
serialNumber: testIosDistCredential.distCertSerialNumber as string,
|
||||
};
|
||||
export const testDistCertsFromApple = [testDistCertFromApple];
|
||||
|
||||
export const testPushKey: PushKey = {
|
||||
apnsKeyP8: 'test-p8',
|
||||
@@ -152,22 +170,16 @@ export const testPushKey: PushKey = {
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy format fixtures
|
||||
*/
|
||||
|
||||
export const testIosPushCredential: IosPushCredentials = {
|
||||
id: 2,
|
||||
type: 'push-key',
|
||||
...testPushKey,
|
||||
};
|
||||
export const testIosPushCredentials = [testIosPushCredential];
|
||||
export const testPushKeyFromApple: PushKeyStoreInfo = {
|
||||
id: testIosPushCredential.apnsKeyId,
|
||||
name: 'test-name',
|
||||
};
|
||||
export const testPushKeysFromApple = [testPushKeyFromApple];
|
||||
export const testLegacyPushCert = {
|
||||
pushId: 'test-push-id',
|
||||
pushP12: 'test-push-p12',
|
||||
pushPassword: 'test-push-password',
|
||||
};
|
||||
|
||||
export const testAppCredential = {
|
||||
experienceName: testExperienceName,
|
||||
bundleIdentifier: testBundleIdentifier,
|
||||
@@ -182,62 +194,3 @@ export const testAllCredentialsForApp = {
|
||||
pushCredentials: testPushKey,
|
||||
distCredentials: testDistCert,
|
||||
};
|
||||
export const testAppCredentials = [testAppCredential];
|
||||
export const testAllCredentials = {
|
||||
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
|
||||
appCredentials: testAppCredentials,
|
||||
};
|
||||
|
||||
export function getIosApiMock() {
|
||||
return {
|
||||
getAllCredentials: jest.fn(() => testAllCredentials),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getDistributionCertificateByIdAsync: jest.fn(id =>
|
||||
id === testIosDistCredential.id ? testIosDistCredential : null
|
||||
),
|
||||
createDistributionCertificateAsync: jest.fn((owner, cert) => ({
|
||||
id: 123,
|
||||
type: 'dist-cert',
|
||||
...cert,
|
||||
})),
|
||||
updateDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
useDistributionCertificateAsync: jest.fn(),
|
||||
getPushKey: jest.fn(() => testIosPushCredential),
|
||||
createPushKeyAsync: jest.fn((owner, key) => ({ id: 124, type: 'push-key', ...key })),
|
||||
updatePushKeyAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
usePushKeyAsync: jest.fn(),
|
||||
|
||||
getAppCredentialsAsync: jest.fn(() => testAppCredentials),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfileAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getIosApiMockWithoutCredentials() {
|
||||
return {
|
||||
getAllCredentials: jest.fn(() => ({ appCredentials: [], userCredentials: [] })),
|
||||
getDistributionCertificateAsync: jest.fn(() => null),
|
||||
getDistributionCertificateByIdAsync: jest.fn(() => {
|
||||
throw new Error('not found');
|
||||
}),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
updateDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
useDistributionCertificateAsync: jest.fn(),
|
||||
getPushKeyAsync: jest.fn(() => null),
|
||||
createPushKeyAsync: jest.fn(),
|
||||
updatePushKeyAsync: jest.fn(),
|
||||
deletePushKeyAsync: jest.fn(),
|
||||
usePushKeyAsync: jest.fn(),
|
||||
getAppCredentialsAsync: jest.fn(app => ({
|
||||
experienceName: `${app.accountName}/${app.projectName}`,
|
||||
bundleIdentifier: app.bundleIdentifier,
|
||||
})),
|
||||
getProvisioningProfileAsync: jest.fn(() => null),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfileAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import {
|
||||
DistributionCertificate,
|
||||
DistributionCertificateStoreInfo,
|
||||
ProvisioningProfile,
|
||||
ProvisioningProfileStoreInfo,
|
||||
PushKey,
|
||||
PushKeyStoreInfo,
|
||||
} from '../ios/appstore/Credentials.types';
|
||||
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
|
||||
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
|
||||
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
export const testAppleTeam = {
|
||||
id: 'test-team-id',
|
||||
};
|
||||
export const testProvisioningProfile: ProvisioningProfile = {
|
||||
provisioningProfileId: 'test-id',
|
||||
provisioningProfile: testProvisioningProfileBase64,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfiles = [testProvisioningProfile];
|
||||
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
expires: tomorrow.getTime(),
|
||||
distributionMethod: 'test',
|
||||
certificates: [],
|
||||
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
|
||||
provisioningProfile: testProvisioningProfile.provisioningProfile,
|
||||
teamId: 'id',
|
||||
};
|
||||
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
|
||||
|
||||
export const testDistCert: DistributionCertificate = {
|
||||
certP12: 'Y2VydHAxMg==',
|
||||
certPassword: 'test-password',
|
||||
distCertSerialNumber: 'test-serial',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
export const testIosDistCredential: IosDistCredentials = {
|
||||
id: 1,
|
||||
type: 'dist-cert',
|
||||
...testDistCert,
|
||||
};
|
||||
export const testIosDistCredentials = [testIosDistCredential];
|
||||
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
|
||||
id: 'test-id',
|
||||
name: 'test-name',
|
||||
status: 'Active',
|
||||
created: today.getTime(),
|
||||
expires: tomorrow.getTime(),
|
||||
ownerName: 'test-owner',
|
||||
ownerId: 'test-id',
|
||||
serialNumber: testIosDistCredential.distCertSerialNumber as string,
|
||||
};
|
||||
export const testDistCertsFromApple = [testDistCertFromApple];
|
||||
|
||||
export const testPushKey: PushKey = {
|
||||
apnsKeyP8: 'test-p8',
|
||||
apnsKeyId: 'test-key-id',
|
||||
teamId: 'test-team-id',
|
||||
};
|
||||
|
||||
export const testIosPushCredential: IosPushCredentials = {
|
||||
id: 2,
|
||||
type: 'push-key',
|
||||
...testPushKey,
|
||||
};
|
||||
export const testIosPushCredentials = [testIosPushCredential];
|
||||
export const testPushKeyFromApple: PushKeyStoreInfo = {
|
||||
id: testIosPushCredential.apnsKeyId,
|
||||
name: 'test-name',
|
||||
};
|
||||
export const testPushKeysFromApple = [testPushKeyFromApple];
|
||||
export const testLegacyPushCert = {
|
||||
pushId: 'test-push-id',
|
||||
pushP12: 'test-push-p12',
|
||||
pushPassword: 'test-push-password',
|
||||
};
|
||||
export const testAppCredential = {
|
||||
experienceName: testExperienceName,
|
||||
bundleIdentifier: testBundleIdentifier,
|
||||
distCredentialsId: testIosDistCredential.id,
|
||||
pushCredentialsId: testIosPushCredential.id,
|
||||
credentials: {
|
||||
...testProvisioningProfile,
|
||||
},
|
||||
};
|
||||
export const testAllCredentialsForApp = {
|
||||
...testAppCredential,
|
||||
pushCredentials: testPushKey,
|
||||
distCredentials: testDistCert,
|
||||
};
|
||||
export const testAppCredentials = [testAppCredential];
|
||||
export const testAllCredentials = {
|
||||
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
|
||||
appCredentials: testAppCredentials,
|
||||
};
|
||||
|
||||
export function getNewIosApiMockWithoutCredentials() {
|
||||
return {
|
||||
getAppAsync: jest.fn(),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: () => testAppleTeam,
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewIosApiMockWithCredentials() {
|
||||
return {
|
||||
getAppAsync: jest.fn(),
|
||||
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
|
||||
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
|
||||
createOrGetExistingAppleTeamAsync: jest.fn(),
|
||||
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
|
||||
getDevicesForAppleTeamAsync: jest.fn(),
|
||||
createProvisioningProfileAsync: jest.fn(),
|
||||
getProvisioningProfileAsync: jest.fn(),
|
||||
updateProvisioningProfileAsync: jest.fn(),
|
||||
deleteProvisioningProfilesAsync: jest.fn(),
|
||||
getDistributionCertificateForAppAsync: jest.fn(),
|
||||
getDistributionCertificatesForAccountAsync: jest.fn(),
|
||||
createDistributionCertificateAsync: jest.fn(),
|
||||
deleteDistributionCertificateAsync: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -1,77 +1,28 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import Log from '../../log';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
import { CredentialsProvider } from '../CredentialsProvider';
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
|
||||
import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { SetupBuildCredentials } from './actions/new/SetupBuildCredentials';
|
||||
import { AppLookupParams } from './api/GraphqlClient';
|
||||
import { Keystore } from './credentials';
|
||||
|
||||
export interface AndroidCredentials {
|
||||
keystore: Keystore;
|
||||
}
|
||||
|
||||
interface AppLookupParams {
|
||||
projectName: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
skipCredentialsCheck?: boolean;
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
export default class AndroidCredentialsProvider {
|
||||
public readonly platform = Platform.ANDROID;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
private get projectFullName(): string {
|
||||
const { projectName, accountName } = this.options.app;
|
||||
return `@${accountName}/${projectName}`;
|
||||
}
|
||||
|
||||
public async hasRemoteAsync(): Promise<boolean> {
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
return !!keystore;
|
||||
}
|
||||
|
||||
public async hasLocalAsync(): Promise<boolean> {
|
||||
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
|
||||
return !!rawCredentialsJson?.android;
|
||||
} catch (err) {
|
||||
Log.error(err); // malformed json
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async isLocalSyncedAsync(): Promise<boolean> {
|
||||
try {
|
||||
const [remote, local] = await Promise.all([
|
||||
this.ctx.android.fetchKeystoreAsync(this.projectFullName),
|
||||
credentialsJsonReader.readAndroidCredentialsAsync(this.ctx.projectDir),
|
||||
]);
|
||||
const r = remote;
|
||||
const l = local?.keystore;
|
||||
return !!(
|
||||
r &&
|
||||
l &&
|
||||
r.keystore === l.keystore &&
|
||||
r.keystorePassword === l.keystorePassword &&
|
||||
r.keyAlias === l.keyAlias &&
|
||||
r.keyPassword === l.keyPassword
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCredentialsAsync(
|
||||
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
|
||||
): Promise<AndroidCredentials> {
|
||||
@@ -81,18 +32,25 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
case CredentialsSource.REMOTE:
|
||||
return await this.getRemoteAsync();
|
||||
}
|
||||
throw new Error('Unknown');
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
await new CredentialsManager(this.ctx).runActionAsync(
|
||||
new SetupBuildCredentials(this.projectFullName)
|
||||
);
|
||||
const keystore = await this.ctx.android.fetchKeystoreAsync(this.projectFullName);
|
||||
if (!keystore) {
|
||||
throw new Error('Unable to set up credentials, failed to fetch keystore from EAS servers');
|
||||
}
|
||||
return { keystore };
|
||||
const setupBuildCredentialsAction = await new SetupBuildCredentials({ app: this.options.app });
|
||||
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
|
||||
return this.toAndroidCredentials(buildCredentials);
|
||||
}
|
||||
|
||||
private toAndroidCredentials(
|
||||
androidBuildCredentials: AndroidAppBuildCredentialsFragment
|
||||
): AndroidCredentials {
|
||||
return {
|
||||
keystore: {
|
||||
keystore: nullthrows(androidBuildCredentials.androidKeystore?.keystore),
|
||||
keystorePassword: nullthrows(androidBuildCredentials.androidKeystore?.keystorePassword),
|
||||
keyAlias: nullthrows(androidBuildCredentials.androidKeystore?.keyAlias),
|
||||
keyPassword: nullthrows(androidBuildCredentials.androidKeystore?.keyPassword),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<AndroidCredentials> {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from 'assert';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { AndroidAppBuildCredentialsFragment } from '../../../graphql/generated';
|
||||
import { getApplicationId } from '../../../project/android/applicationId';
|
||||
import { getProjectAccountName, getProjectConfigDescription } from '../../../project/projectUtils';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { findAccountByName } from '../../../user/Account';
|
||||
import { Context } from '../../context';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {
|
||||
ctx.ensureProjectContext();
|
||||
const projectName = ctx.exp.slug;
|
||||
const accountName = getProjectAccountName(ctx.exp, ctx.user);
|
||||
const account = findAccountByName(ctx.user.accounts, accountName);
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
|
||||
const androidApplicationIdentifier = getApplicationId(ctx.projectDir, ctx.exp);
|
||||
if (!androidApplicationIdentifier) {
|
||||
throw new Error(
|
||||
`android.package needs to be defined in your ${getProjectConfigDescription(
|
||||
ctx.projectDir
|
||||
)} file`
|
||||
);
|
||||
}
|
||||
|
||||
return { account, projectName, androidApplicationIdentifier };
|
||||
}
|
||||
|
||||
export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
|
||||
ctx: Context,
|
||||
appLookupParams: AppLookupParams,
|
||||
{
|
||||
androidKeystoreId,
|
||||
}: {
|
||||
androidKeystoreId: string;
|
||||
}
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
assert(
|
||||
!ctx.nonInteractive,
|
||||
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
|
||||
);
|
||||
const existingDefaultBuildCredentials = await ctx.newAndroid.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (existingDefaultBuildCredentials) {
|
||||
return await ctx.newAndroid.updateAndroidAppBuildCredentialsAsync(
|
||||
existingDefaultBuildCredentials,
|
||||
{ androidKeystoreId }
|
||||
);
|
||||
}
|
||||
const { providedName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'providedName',
|
||||
message: 'Assign a name to your build credentials:',
|
||||
initial: generateRandomName(),
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
return await ctx.newAndroid.createAndroidAppBuildCredentialsAsync(appLookupParams, {
|
||||
name: providedName,
|
||||
isDefault: true,
|
||||
androidKeystoreId,
|
||||
});
|
||||
}
|
||||
|
||||
function generateRandomName(): string {
|
||||
return `Build Credentials ${nanoid(10)}`;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AndroidKeystoreFragment } from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { Account } from '../../../../user/Account';
|
||||
import { Context } from '../../../context';
|
||||
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
|
||||
import { KeystoreWithType, keystoreSchema } from '../../credentials';
|
||||
import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
import { getKeystoreWithType, validateKeystore } from '../../utils/keystoreNew';
|
||||
|
||||
export class CreateKeystore {
|
||||
constructor(private account: Account) {}
|
||||
|
||||
public async runAsync(ctx: Context): Promise<AndroidKeystoreFragment> {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(`New keystore cannot be created in non-interactive mode.`);
|
||||
}
|
||||
const keystore = await this.provideOrGenerateAsync();
|
||||
const keystoreFragment = await ctx.newAndroid.createKeystoreAsync(this.account, keystore);
|
||||
Log.succeed('Created keystore');
|
||||
return keystoreFragment;
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(): Promise<KeystoreWithType> {
|
||||
const providedKeystore = await askForUserProvidedAsync(keystoreSchema);
|
||||
if (providedKeystore) {
|
||||
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
|
||||
validateKeystore(providedKeystoreWithType);
|
||||
}
|
||||
return await generateRandomKeystoreAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreFragment,
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { Context } from '../../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { createOrUpdateDefaultAndroidAppBuildCredentialsAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from './CreateKeystore';
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Build Credentials for Android
|
||||
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
|
||||
*/
|
||||
export class SetupBuildCredentials {
|
||||
constructor(private options: Options) {}
|
||||
|
||||
async runAsync(ctx: Context): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const { app, name: maybeName } = this.options;
|
||||
const alreadySetupBuildCredentials = await this.getFullySetupBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name: maybeName,
|
||||
});
|
||||
if (alreadySetupBuildCredentials) {
|
||||
return alreadySetupBuildCredentials;
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Generating a new Keystore is not supported in --non-interactive mode'
|
||||
);
|
||||
}
|
||||
|
||||
const keystore = await new CreateKeystore(app.account).runAsync(ctx);
|
||||
return await this.assignBuildCredentialsAsync({ ctx, app, name: maybeName, keystore });
|
||||
}
|
||||
|
||||
async assignBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
keystore,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
keystore: AndroidKeystoreFragment;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
if (name) {
|
||||
return await ctx.newAndroid.createOrUpdateAndroidAppBuildCredentialsByNameAsync(app, name, {
|
||||
androidKeystoreId: keystore.id,
|
||||
});
|
||||
}
|
||||
return await createOrUpdateDefaultAndroidAppBuildCredentialsAsync(ctx, app, {
|
||||
androidKeystoreId: keystore.id,
|
||||
});
|
||||
}
|
||||
|
||||
async getFullySetupBuildCredentialsAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
if (name) {
|
||||
return await this.getFullySetupBuildCredentialsByNameAsync({ ctx, app, name });
|
||||
}
|
||||
|
||||
const defaultBuildCredentials = await ctx.newAndroid.getDefaultAndroidAppBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const defaultKeystore = defaultBuildCredentials?.androidKeystore ?? null;
|
||||
if (defaultKeystore) {
|
||||
Log.log(
|
||||
`Using Keystore from configuration: ${nullthrows(defaultBuildCredentials).name} (default)`
|
||||
);
|
||||
return defaultBuildCredentials;
|
||||
}
|
||||
|
||||
// fall back to legacy credentials if we cant find a default keystore
|
||||
const legacyBuildCredentials = await ctx.newAndroid.getLegacyAndroidAppBuildCredentialsAsync(
|
||||
app
|
||||
);
|
||||
const legacyKeystore = legacyBuildCredentials?.androidKeystore ?? null;
|
||||
if (legacyKeystore) {
|
||||
Log.log('Using Keystore ported from Expo Classic (expo-cli)');
|
||||
return legacyBuildCredentials;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFullySetupBuildCredentialsByNameAsync({
|
||||
ctx,
|
||||
app,
|
||||
name,
|
||||
}: {
|
||||
ctx: Context;
|
||||
app: AppLookupParams;
|
||||
name: string;
|
||||
}): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
const maybeBuildCredentials = await ctx.newAndroid.getAndroidAppBuildCredentialsByNameAsync(
|
||||
app,
|
||||
name
|
||||
);
|
||||
const keystore = maybeBuildCredentials?.androidKeystore ?? null;
|
||||
if (keystore) {
|
||||
const buildCredentials = nullthrows(maybeBuildCredentials);
|
||||
Log.log(
|
||||
`Using Keystore from configuration: ${buildCredentials.name}${
|
||||
buildCredentials.isDefault ? ' (default)' : ''
|
||||
}`
|
||||
);
|
||||
return buildCredentials;
|
||||
}
|
||||
Log.log(
|
||||
`No Keystore found for configuration: ${name}${
|
||||
maybeBuildCredentials?.isDefault ? ' (default)' : ''
|
||||
}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { confirmAsync } from '../../../../../prompts';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from '../CreateKeystore';
|
||||
|
||||
jest.mock('../../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
it('creates a keystore in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
|
||||
await createKeystoreAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created on expo servers
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
|
||||
|
||||
// fail if users are running in non-interactive mode
|
||||
await expect(createKeystoreAction.runAsync(ctx)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { confirmAsync, promptAsync } from '../../../../../prompts';
|
||||
import {
|
||||
getNewAndroidApiMockWithoutCredentials,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
testJksAndroidKeystoreFragment,
|
||||
testLegacyAndroidBuildCredentialsFragment,
|
||||
} from '../../../../__tests__/fixtures-android-new';
|
||||
import { createCtxMock } from '../../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../../errors';
|
||||
import { getAppLookupParamsFromContext } from '../../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from '../SetupBuildCredentials';
|
||||
|
||||
jest.mock('../../../../../prompts');
|
||||
(confirmAsync as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
describe('SetupBuildCredentials', () => {
|
||||
it('skips setup when there are prior legacy credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
getLegacyAndroidAppBuildCredentialsAsync: jest.fn(
|
||||
() => testLegacyAndroidBuildCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore not to be created
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
getDefaultAndroidAppBuildCredentialsAsync: jest.fn(
|
||||
() => testAndroidBuildCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore not to be created
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('sets up credentials when there are no prior credentials', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
createKeystoreAsync: jest.fn(() => testJksAndroidKeystoreFragment),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created
|
||||
expect(ctx.newAndroid.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
(promptAsync as jest.Mock).mockImplementation(() => ({ providedName: 'test-provided-name' }));
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: true,
|
||||
newAndroid: {
|
||||
...getNewAndroidApiMockWithoutCredentials(),
|
||||
},
|
||||
});
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
|
||||
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
|
||||
MissingCredentialsNonInteractiveError
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
AndroidKeystoreFragment,
|
||||
AppFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { AppQuery } from '../../ios/api/graphql/queries/AppQuery';
|
||||
import { KeystoreWithType } from '../credentials';
|
||||
import { AndroidAppBuildCredentialsMutation } from './graphql/mutations/AndroidAppBuildCredentialsMutation';
|
||||
import { AndroidAppCredentialsMutation } from './graphql/mutations/AndroidAppCredentialsMutation';
|
||||
import { AndroidKeystoreMutation } from './graphql/mutations/AndroidKeystoreMutation';
|
||||
import { AndroidAppCredentialsQuery } from './graphql/queries/AndroidAppCredentialsQuery';
|
||||
|
||||
export interface AppLookupParams {
|
||||
account: Account;
|
||||
projectName: string;
|
||||
androidApplicationIdentifier: string; // 'android.package' field in app.json
|
||||
}
|
||||
|
||||
export async function getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<CommonAndroidAppCredentialsFragment | null> {
|
||||
const { androidApplicationIdentifier } = appLookupParams;
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await AndroidAppCredentialsQuery.withCommonFieldsByApplicationIdentifierAsync(
|
||||
projectFullName,
|
||||
{
|
||||
androidApplicationIdentifier,
|
||||
legacyOnly: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAndroidAppBuildCredentialsListAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<AndroidAppBuildCredentialsFragment[]> {
|
||||
const appCredentials = await getAndroidAppCredentialsWithCommonFieldsAsync(appLookupParams);
|
||||
return appCredentials?.androidAppBuildCredentialsList ?? [];
|
||||
}
|
||||
|
||||
/* There is at most one set of legacy android app credentials associated with an Expo App */
|
||||
export async function getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<CommonAndroidAppCredentialsFragment | null> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await AndroidAppCredentialsQuery.withCommonFieldsByApplicationIdentifierAsync(
|
||||
projectFullName,
|
||||
{
|
||||
legacyOnly: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* There is at most one set of legacy android app build credentials associated with an Expo App */
|
||||
export async function getLegacyAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
const legacyAppCredentials = await getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
return legacyAppCredentials?.androidAppBuildCredentialsList[0] ?? null;
|
||||
}
|
||||
|
||||
export async function createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const maybeAndroidAppCredentials = await getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (maybeAndroidAppCredentials) {
|
||||
return maybeAndroidAppCredentials;
|
||||
} else {
|
||||
const app = await getAppAsync(appLookupParams);
|
||||
return await AndroidAppCredentialsMutation.createAndroidAppCredentialsAsync(
|
||||
{
|
||||
/* fcmKey not supported yet */
|
||||
},
|
||||
app.id,
|
||||
appLookupParams.androidApplicationIdentifier
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAndroidAppBuildCredentialsAsync(
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment,
|
||||
{
|
||||
androidKeystoreId,
|
||||
}: {
|
||||
androidKeystoreId: string;
|
||||
}
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
return await AndroidAppBuildCredentialsMutation.setKeystoreAsync(
|
||||
buildCredentials.id,
|
||||
androidKeystoreId
|
||||
);
|
||||
}
|
||||
|
||||
export async function createAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
{
|
||||
name,
|
||||
isDefault,
|
||||
androidKeystoreId,
|
||||
}: {
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
androidKeystoreId: string;
|
||||
}
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const androidAppCredentials = await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
const buildCredentialsList = androidAppCredentials.androidAppBuildCredentialsList;
|
||||
const existingDefaultBuildCredentials =
|
||||
buildCredentialsList.find(buildCredentials => buildCredentials.isDefault) ?? null;
|
||||
if (existingDefaultBuildCredentials && isDefault) {
|
||||
throw new Error(
|
||||
'Cannot create new default Android Build Credentials. A set of default credentials exists already.'
|
||||
);
|
||||
}
|
||||
|
||||
return await AndroidAppBuildCredentialsMutation.createAndroidAppBuildCredentialsAsync(
|
||||
{
|
||||
name,
|
||||
isDefault,
|
||||
keystoreId: androidKeystoreId,
|
||||
},
|
||||
androidAppCredentials.id
|
||||
);
|
||||
}
|
||||
|
||||
export async function getDefaultAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
const buildCredentialsList = await getAndroidAppBuildCredentialsListAsync(appLookupParams);
|
||||
return buildCredentialsList.find(buildCredentials => buildCredentials.isDefault) ?? null;
|
||||
}
|
||||
|
||||
export async function getAndroidAppBuildCredentialsByNameAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
name: string
|
||||
): Promise<AndroidAppBuildCredentialsFragment | null> {
|
||||
const buildCredentialsList = await getAndroidAppBuildCredentialsListAsync(appLookupParams);
|
||||
return buildCredentialsList.find(buildCredentials => buildCredentials.name === name) ?? null;
|
||||
}
|
||||
|
||||
export async function createOrUpdateAndroidAppBuildCredentialsByNameAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
name: string,
|
||||
{
|
||||
androidKeystoreId,
|
||||
}: {
|
||||
androidKeystoreId: string;
|
||||
}
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const existingBuildCredentialsWithName = await getAndroidAppBuildCredentialsByNameAsync(
|
||||
appLookupParams,
|
||||
name
|
||||
);
|
||||
if (existingBuildCredentialsWithName) {
|
||||
return await updateAndroidAppBuildCredentialsAsync(existingBuildCredentialsWithName, {
|
||||
androidKeystoreId,
|
||||
});
|
||||
}
|
||||
const defaultBuildCredentialsExist = !!(await getDefaultAndroidAppBuildCredentialsAsync(
|
||||
appLookupParams
|
||||
));
|
||||
return await createAndroidAppBuildCredentialsAsync(appLookupParams, {
|
||||
name,
|
||||
isDefault: !defaultBuildCredentialsExist, // make default if none exist
|
||||
androidKeystoreId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createOrUpdateDefaultIosAppBuildCredentialsAsync() {
|
||||
throw new Error('This requires user prompting. Look for me in BuildCredentialsUtils');
|
||||
}
|
||||
|
||||
export async function createKeystoreAsync(
|
||||
account: Account,
|
||||
keystore: KeystoreWithType
|
||||
): Promise<AndroidKeystoreFragment> {
|
||||
return await AndroidKeystoreMutation.createAndroidKeystore(
|
||||
{
|
||||
base64EncodedKeystore: keystore.keystore,
|
||||
keystorePassword: keystore.keystorePassword,
|
||||
keyAlias: keystore.keyAlias,
|
||||
keyPassword: keystore.keyPassword,
|
||||
type: keystore.type,
|
||||
},
|
||||
account.id
|
||||
);
|
||||
}
|
||||
|
||||
async function getAppAsync(appLookupParams: AppLookupParams): Promise<AppFragment> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await AppQuery.byFullNameAsync(projectFullName);
|
||||
}
|
||||
|
||||
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
|
||||
`@${account.name}/${projectName}`;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
CreateAndroidAppBuildCredentialsMutation,
|
||||
SetKeystoreMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppBuildCredentials';
|
||||
|
||||
const AndroidAppBuildCredentialsMutation = {
|
||||
async createAndroidAppBuildCredentialsAsync(
|
||||
androidAppBuildCredentialsInput: {
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
keystoreId: string;
|
||||
},
|
||||
androidAppCredentialsId: string
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateAndroidAppBuildCredentialsMutation>(
|
||||
gql`
|
||||
mutation CreateAndroidAppBuildCredentialsMutation(
|
||||
$androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput!
|
||||
$androidAppCredentialsId: ID!
|
||||
) {
|
||||
androidAppBuildCredentials {
|
||||
createAndroidAppBuildCredentials(
|
||||
androidAppBuildCredentialsInput: $androidAppBuildCredentialsInput
|
||||
androidAppCredentialsId: $androidAppCredentialsId
|
||||
) {
|
||||
id
|
||||
...AndroidAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AndroidAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidAppBuildCredentialsInput,
|
||||
androidAppCredentialsId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.androidAppBuildCredentials.createAndroidAppBuildCredentials,
|
||||
'GraphQL: `createAndroidAppBuildCredentials` not defined in server response'
|
||||
);
|
||||
return data.androidAppBuildCredentials.createAndroidAppBuildCredentials;
|
||||
},
|
||||
async setKeystoreAsync(
|
||||
androidAppBuildCredentialsId: string,
|
||||
keystoreId: string
|
||||
): Promise<AndroidAppBuildCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<SetKeystoreMutation>(
|
||||
gql`
|
||||
mutation SetKeystoreMutation($androidAppBuildCredentialsId: ID!, $keystoreId: ID!) {
|
||||
androidAppBuildCredentials {
|
||||
setKeystore(id: $androidAppBuildCredentialsId, keystoreId: $keystoreId) {
|
||||
id
|
||||
...AndroidAppBuildCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AndroidAppBuildCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidAppBuildCredentialsId,
|
||||
keystoreId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.androidAppBuildCredentials.setKeystore,
|
||||
'GraphQL: `setKeystore` not defined in server response'
|
||||
);
|
||||
return data.androidAppBuildCredentials.setKeystore;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppBuildCredentialsMutation };
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
CreateAndroidAppCredentialsMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
|
||||
|
||||
const AndroidAppCredentialsMutation = {
|
||||
async createAndroidAppCredentialsAsync(
|
||||
androidAppCredentialsInput: {
|
||||
fcmId?: string;
|
||||
},
|
||||
appId: string,
|
||||
applicationIdentifier: string
|
||||
): Promise<CommonAndroidAppCredentialsFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateAndroidAppCredentialsMutation>(
|
||||
gql`
|
||||
mutation CreateAndroidAppCredentialsMutation(
|
||||
$androidAppCredentialsInput: AndroidAppCredentialsInput!
|
||||
$appId: ID!
|
||||
$applicationIdentifier: String!
|
||||
) {
|
||||
androidAppCredentials {
|
||||
createAndroidAppCredentials(
|
||||
androidAppCredentialsInput: $androidAppCredentialsInput
|
||||
appId: $appId
|
||||
applicationIdentifier: $applicationIdentifier
|
||||
) {
|
||||
id
|
||||
...CommonAndroidAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(CommonAndroidAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidAppCredentialsInput,
|
||||
appId,
|
||||
applicationIdentifier,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.androidAppCredentials.createAndroidAppCredentials,
|
||||
'GraphQL: `createAndroidAppCredentials` not defined in server response'
|
||||
);
|
||||
return data.androidAppCredentials.createAndroidAppCredentials;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppCredentialsMutation };
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
AndroidKeystoreFragment,
|
||||
AndroidKeystoreType,
|
||||
CreateAndroidKeystoreMutation,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { AndroidKeystoreFragmentNode } from '../../../../../graphql/types/credentials/AndroidKeystore';
|
||||
|
||||
const AndroidKeystoreMutation = {
|
||||
async createAndroidKeystore(
|
||||
androidKeystoreInput: {
|
||||
base64EncodedKeystore: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword?: string;
|
||||
type: AndroidKeystoreType;
|
||||
},
|
||||
accountId: string
|
||||
): Promise<AndroidKeystoreFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateAndroidKeystoreMutation>(
|
||||
gql`
|
||||
mutation CreateAndroidKeystoreMutation(
|
||||
$androidKeystoreInput: AndroidKeystoreInput!
|
||||
$accountId: ID!
|
||||
) {
|
||||
androidKeystore {
|
||||
createAndroidKeystore(
|
||||
androidKeystoreInput: $androidKeystoreInput
|
||||
accountId: $accountId
|
||||
) {
|
||||
id
|
||||
...AndroidKeystoreFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AndroidKeystoreFragmentNode)}
|
||||
`,
|
||||
{
|
||||
androidKeystoreInput,
|
||||
accountId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(
|
||||
data.androidKeystore.createAndroidKeystore,
|
||||
'GraphQL: `createAndroidKeystore` not defined in server response'
|
||||
);
|
||||
return data.androidKeystore.createAndroidKeystore;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidKeystoreMutation };
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import {
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifierQuery,
|
||||
} from '../../../../../graphql/generated';
|
||||
import { CommonAndroidAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppCredentials';
|
||||
|
||||
const AndroidAppCredentialsQuery = {
|
||||
async withCommonFieldsByApplicationIdentifierAsync(
|
||||
projectFullName: string,
|
||||
{
|
||||
androidApplicationIdentifier,
|
||||
legacyOnly,
|
||||
}: {
|
||||
androidApplicationIdentifier?: string;
|
||||
legacyOnly?: boolean;
|
||||
}
|
||||
): Promise<CommonAndroidAppCredentialsFragment | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifierQuery>(
|
||||
gql`
|
||||
query CommonAndroidAppCredentialsWithBuildCredentialsByApplicationIdentifierQuery(
|
||||
$projectFullName: String!
|
||||
$applicationIdentifier: String
|
||||
$legacyOnly: Boolean
|
||||
) {
|
||||
app {
|
||||
byFullName(fullName: $projectFullName) {
|
||||
id
|
||||
androidAppCredentials(
|
||||
filter: {
|
||||
applicationIdentifier: $applicationIdentifier
|
||||
legacyOnly: $legacyOnly
|
||||
}
|
||||
) {
|
||||
id
|
||||
...CommonAndroidAppCredentialsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(CommonAndroidAppCredentialsFragmentNode)}
|
||||
`,
|
||||
{
|
||||
projectFullName,
|
||||
applicationIdentifier: androidApplicationIdentifier,
|
||||
legacyOnly,
|
||||
},
|
||||
{
|
||||
additionalTypenames: ['AndroidAppCredentials', 'AndroidAppBuildCredentials', 'App'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
assert(data.app, 'GraphQL: `app` not defined in server response');
|
||||
return data.app.byFullName.androidAppCredentials[0] ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
export { AndroidAppCredentialsQuery };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AndroidKeystoreType } from '../../graphql/generated';
|
||||
import { CredentialSchema } from '../utils/promptForCredentials';
|
||||
|
||||
export interface FcmCredentials {
|
||||
@@ -8,9 +9,13 @@ export interface Keystore {
|
||||
keystore: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
keyPassword?: string;
|
||||
}
|
||||
|
||||
export type KeystoreWithType = Keystore & {
|
||||
type: AndroidKeystoreType;
|
||||
};
|
||||
|
||||
export type AndroidCredentials = {
|
||||
experienceName: string;
|
||||
keystore: Keystore | null;
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`print credentials prints the AndroidAppCredentials fragment 1`] = `"Android Credentials Project: testApp Application Identifier: test.com.app EAS Credentials: These will take precedence over Expo Classic credentials Configuration: legacy (Default) Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago Credentials ported from Expo Classic (expo-cli): Keystore: Type: JKS Key Alias: QHdrb3p5cmEvY3JlZGVudGlhbHMtdGVzdA== MD5 Fingerprint: TE:ST:-M:D5 SHA1 Fingerprint: TE:ST:-S:HA:1 SHA256 Fingerprint: TE:ST:-S:HA:25:6 Updated 0 second ago"`;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { AndroidKeystoreType } from '../../../../graphql/generated';
|
||||
import {
|
||||
testKeystore,
|
||||
testPKCS12EmptyPasswordKeystore,
|
||||
testPKCS12Keystore,
|
||||
} from '../../../__tests__/fixtures-android';
|
||||
import { getKeystoreType, getKeystoreWithType, validateKeystore } from '../keystoreNew';
|
||||
|
||||
describe('getKeystoreType', () => {
|
||||
it('identifies a valid jks keystore', async () => {
|
||||
const type = getKeystoreType(testKeystore);
|
||||
expect(type).toBe(AndroidKeystoreType.Jks);
|
||||
});
|
||||
it('identifies a valid pkcs12 keystore', async () => {
|
||||
const type = getKeystoreType(testPKCS12Keystore);
|
||||
expect(type).toBe(AndroidKeystoreType.Pkcs12);
|
||||
});
|
||||
it('identifies a jks keystore with bad password as unknown', async () => {
|
||||
const keystoreWithType = getKeystoreType({
|
||||
...testKeystore,
|
||||
keystorePassword: 'not-the-password',
|
||||
});
|
||||
expect(keystoreWithType).toBe(AndroidKeystoreType.Unknown);
|
||||
});
|
||||
it('identifies a pkcs keystore with bad password as unknown', async () => {
|
||||
const keystoreWithType = getKeystoreType({
|
||||
...testPKCS12Keystore,
|
||||
keystorePassword: 'not-the-password',
|
||||
});
|
||||
expect(keystoreWithType).toBe(AndroidKeystoreType.Unknown);
|
||||
});
|
||||
it('identifies a bogus keystore as unknown', async () => {
|
||||
const type = getKeystoreType({ ...testKeystore, keystore: 'sdfsdfsdfdsfsfdsf' });
|
||||
expect(type).toBe(AndroidKeystoreType.Unknown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateKeystore', () => {
|
||||
it('validates a correctly formatted jks keystore', async () => {
|
||||
const keystoreWithType = getKeystoreWithType(testKeystore);
|
||||
expect(keystoreWithType.type).toBe(AndroidKeystoreType.Jks);
|
||||
expect(() => validateKeystore(keystoreWithType)).not.toThrow();
|
||||
});
|
||||
it('doesnt validate a jks keystore with wrong alias', async () => {
|
||||
const keystoreWithType = getKeystoreWithType({
|
||||
...testKeystore,
|
||||
keyAlias: 'non-existent-alias',
|
||||
});
|
||||
expect(() => validateKeystore(keystoreWithType)).toThrow();
|
||||
});
|
||||
it('doesnt validate a jks keystore with wrong key password', async () => {
|
||||
const keystoreWithType = getKeystoreWithType({
|
||||
...testKeystore,
|
||||
keyPassword: 'not-the-password',
|
||||
});
|
||||
expect(() => validateKeystore(keystoreWithType)).toThrow();
|
||||
});
|
||||
it('validates a correctly formatted pkcs 12 keystore', async () => {
|
||||
const keystoreWithType = getKeystoreWithType(testPKCS12Keystore);
|
||||
expect(keystoreWithType.type).toBe(AndroidKeystoreType.Pkcs12);
|
||||
expect(() => validateKeystore(keystoreWithType)).not.toThrow();
|
||||
});
|
||||
it('doesnt validate a PKCS 12 keystore with wrong alias', async () => {
|
||||
const keystoreWithType = getKeystoreWithType({
|
||||
...testPKCS12Keystore,
|
||||
keyAlias: 'non-existent-alias',
|
||||
});
|
||||
expect(() => validateKeystore(keystoreWithType)).toThrow();
|
||||
});
|
||||
it('validates an PKCS 12 Keystore with an empty password', async () => {
|
||||
const keystoreWithType = getKeystoreWithType(testPKCS12EmptyPasswordKeystore);
|
||||
expect(keystoreWithType.type).toBe(AndroidKeystoreType.Pkcs12);
|
||||
expect(() => validateKeystore(keystoreWithType)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import mockdate from 'mockdate';
|
||||
|
||||
import Log from '../../../../log';
|
||||
import {
|
||||
testAndroidAppCredentialsFragment,
|
||||
testLegacyAndroidAppCredentialsFragment,
|
||||
} from '../../../__tests__/fixtures-android-new';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { getAppLookupParamsFromContext } from '../../actions/BuildCredentialsUtils';
|
||||
import { displayAndroidAppCredentials } from '../printCredentialsBeta';
|
||||
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
|
||||
|
||||
mockdate.set(new Date('4/20/2021'));
|
||||
|
||||
describe('print credentials', () => {
|
||||
it('prints the AndroidAppCredentials fragment', async () => {
|
||||
const ctx = createCtxMock();
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
displayAndroidAppCredentials({
|
||||
appLookupParams,
|
||||
legacyAppCredentials: testLegacyAndroidAppCredentialsFragment,
|
||||
appCredentials: testAndroidAppCredentialsFragment,
|
||||
});
|
||||
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
|
||||
(acc, mockValue) => acc + mockValue
|
||||
);
|
||||
expect(loggedSoFar).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -5,9 +5,10 @@ import path from 'path';
|
||||
import tempy from 'tempy';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AndroidKeystoreType } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { getTmpDirectory } from '../../../utils/paths';
|
||||
import { Keystore } from '../credentials';
|
||||
import { Keystore, KeystoreWithType } from '../credentials';
|
||||
|
||||
export async function keytoolCommandExistsAsync(): Promise<boolean> {
|
||||
try {
|
||||
@@ -82,7 +83,7 @@ async function createKeystoreAsync(credentials: {
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
}): Promise<Keystore> {
|
||||
}): Promise<KeystoreWithType> {
|
||||
await ensureKeytoolCommandExistsAsync();
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const keystorePath = path.join(getTmpDirectory(), `${uuidv4()}-keystore.jks`);
|
||||
@@ -112,13 +113,14 @@ async function createKeystoreAsync(credentials: {
|
||||
return {
|
||||
...credentials,
|
||||
keystore: await fs.readFile(keystorePath, 'base64'),
|
||||
type: AndroidKeystoreType.Jks,
|
||||
};
|
||||
} finally {
|
||||
await fs.remove(keystorePath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateRandomKeystoreAsync(): Promise<Keystore> {
|
||||
export async function generateRandomKeystoreAsync(): Promise<KeystoreWithType> {
|
||||
const keystoreData = {
|
||||
keystorePassword: crypto.randomBytes(16).toString('hex'),
|
||||
keyPassword: crypto.randomBytes(16).toString('hex'),
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { getX509Asn1ByFriendlyName, parsePKCS12 } from '@expo/pkcs12';
|
||||
import jks from 'jks-js';
|
||||
import { asn1 } from 'node-forge';
|
||||
|
||||
import { AndroidKeystoreType } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Keystore, KeystoreWithType } from '../credentials';
|
||||
|
||||
export function getKeystoreWithType(keystore: Keystore): KeystoreWithType {
|
||||
const type = getKeystoreType(keystore);
|
||||
return { ...keystore, type };
|
||||
}
|
||||
|
||||
export function getKeystoreType(keystore: Keystore): AndroidKeystoreType {
|
||||
if (isPKCSKeystore(keystore)) {
|
||||
return AndroidKeystoreType.Pkcs12;
|
||||
} else if (isJKSKeystore(keystore)) {
|
||||
return AndroidKeystoreType.Jks;
|
||||
}
|
||||
return AndroidKeystoreType.Unknown;
|
||||
}
|
||||
|
||||
function isPKCSKeystore(keystore: Keystore): boolean {
|
||||
try {
|
||||
parsePKCS12(keystore.keystore, keystore.keystorePassword);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isJKSKeystore(keystore: Keystore): boolean {
|
||||
try {
|
||||
jks.parseJks(Buffer.from(keystore.keystore, 'base64'), keystore.keystorePassword);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateKeystore(keystore: KeystoreWithType): void {
|
||||
if (keystore.type === AndroidKeystoreType.Jks) {
|
||||
getPemFromJksKeystore(keystore);
|
||||
} else if (keystore.type === AndroidKeystoreType.Pkcs12) {
|
||||
getX509Asn1FromPKCS12Keystore(keystore.keystore, keystore.keystorePassword, keystore.keyAlias);
|
||||
} else if (keystore.type === AndroidKeystoreType.Unknown) {
|
||||
Log.warn('Unknown keystore type, skipping validation.');
|
||||
} else {
|
||||
Log.warn(`Unsupported keystore type: ${keystore.type}, skipping validation.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getPemFromJksKeystore(keystore: Keystore): string {
|
||||
const keystoreEntries = jks.parseJks(
|
||||
Buffer.from(keystore.keystore, 'base64'),
|
||||
keystore.keystorePassword
|
||||
);
|
||||
// keystore entries are case insensitive in both keytool and jks-js implementations
|
||||
const keystoreEntry = keystoreEntries.find(
|
||||
(entry: any) => entry.alias.toLowerCase() === keystore.keyAlias.toLowerCase()
|
||||
);
|
||||
if (!keystoreEntry) {
|
||||
throw new Error(`JKS Keystore does not contain alias: ${keystore.keyAlias}`);
|
||||
}
|
||||
const { protectedPrivateKey } = keystoreEntry;
|
||||
try {
|
||||
return jks.decrypt(protectedPrivateKey, keystore.keyPassword);
|
||||
} catch (e) {
|
||||
throw new Error(`${e.message}: Check that your key password is correct`);
|
||||
}
|
||||
}
|
||||
|
||||
function getX509Asn1FromPKCS12Keystore(
|
||||
p12BufferOrBase64String: Buffer | string,
|
||||
maybePassword: string | null,
|
||||
keyAlias: string
|
||||
): asn1.Asn1 {
|
||||
let x509Asn1;
|
||||
try {
|
||||
const p12 = parsePKCS12(p12BufferOrBase64String, maybePassword);
|
||||
x509Asn1 = getX509Asn1ByFriendlyName(p12, keyAlias);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid PKCS#12 (.p12) keystore: ${e.message}`);
|
||||
}
|
||||
if (!x509Asn1) {
|
||||
throw new Error(
|
||||
`PKCS#12 keystore: Unable to get certificate under alias: ${keyAlias}. Run this to find the available aliases: keytool -list -v -keystore [your-keystore-path]`
|
||||
);
|
||||
}
|
||||
return x509Asn1;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
AndroidAppBuildCredentialsFragment,
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { AppLookupParams } from '../api/GraphqlClient';
|
||||
|
||||
export function displayEmptyAndroidCredentials(appLookupParams: AppLookupParams): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.log(` No credentials set up yet!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* sort a build credentials array in descending order of preference
|
||||
* prefer default credentials, then prefer names that come first lexicographically
|
||||
*/
|
||||
function sortBuildCredentials(
|
||||
androidAppBuildCredentialsList: AndroidAppBuildCredentialsFragment[]
|
||||
): AndroidAppBuildCredentialsFragment[] {
|
||||
return androidAppBuildCredentialsList.sort((buildCredentialsA, buildCredentialsB) => {
|
||||
if (buildCredentialsA.isDefault) {
|
||||
return -1;
|
||||
} else if (buildCredentialsB.isDefault) {
|
||||
return 1;
|
||||
}
|
||||
return buildCredentialsA.name.localeCompare(buildCredentialsB.name);
|
||||
});
|
||||
}
|
||||
|
||||
function displayLegacyAndroidAppCredentials(
|
||||
legacyAppCredentials: CommonAndroidAppCredentialsFragment,
|
||||
appCredentials: CommonAndroidAppCredentialsFragment | null
|
||||
): void {
|
||||
if (appCredentials) {
|
||||
// differentiate between old and new if there's both
|
||||
Log.log(chalk.bold(` Credentials ported from Expo Classic (expo-cli):`));
|
||||
}
|
||||
// There can only be one set of build credentials in legacy app credentials
|
||||
const [legacyBuildCredentials] = legacyAppCredentials.androidAppBuildCredentialsList;
|
||||
if (legacyBuildCredentials) {
|
||||
displayAndroidBuildCredentials(
|
||||
legacyBuildCredentials,
|
||||
/* only one set to show, skip config display */ true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function displayEASAndroidAppCredentials(
|
||||
legacyAppCredentials: CommonAndroidAppCredentialsFragment | null,
|
||||
appCredentials: CommonAndroidAppCredentialsFragment
|
||||
): void {
|
||||
if (legacyAppCredentials) {
|
||||
// differentiate between old and new if there's both
|
||||
Log.log(chalk.bold(` EAS Credentials:`));
|
||||
Log.log(` These will take precedence over Expo Classic credentials`);
|
||||
}
|
||||
const sortedBuildCredentialsList = sortBuildCredentials(
|
||||
appCredentials.androidAppBuildCredentialsList
|
||||
);
|
||||
for (const buildCredentials of sortedBuildCredentialsList) {
|
||||
displayAndroidBuildCredentials(buildCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
function formatFingerprint(fingerprint: string | null): string {
|
||||
if (!fingerprint) {
|
||||
return 'unavailable';
|
||||
}
|
||||
const uppercaseFingerprint = fingerprint.toUpperCase();
|
||||
const bytes = [];
|
||||
for (let i = 0; i < uppercaseFingerprint.length; i++) {
|
||||
const halfByte = uppercaseFingerprint.charAt(i);
|
||||
if (i % 2 === 0) {
|
||||
bytes.push(halfByte); // first half of the byte
|
||||
} else {
|
||||
bytes[bytes.length - 1] += halfByte; // second half of the byte
|
||||
}
|
||||
}
|
||||
return bytes.join(':');
|
||||
}
|
||||
function displayAndroidBuildCredentials(
|
||||
buildCredentials: AndroidAppBuildCredentialsFragment,
|
||||
skipConfigurationDisplay?: boolean
|
||||
): void {
|
||||
if (!skipConfigurationDisplay) {
|
||||
const { isDefault, name } = buildCredentials;
|
||||
Log.log(chalk.bold(` Configuration: ${name}${isDefault ? ' (Default)' : ''}`));
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const maybeKeystore = buildCredentials.androidKeystore;
|
||||
Log.log(` Keystore:`);
|
||||
if (maybeKeystore) {
|
||||
const {
|
||||
keyAlias,
|
||||
type,
|
||||
md5CertificateFingerprint,
|
||||
sha1CertificateFingerprint,
|
||||
sha256CertificateFingerprint,
|
||||
updatedAt,
|
||||
} = maybeKeystore;
|
||||
Log.log(` Type: ${type}`);
|
||||
Log.log(` Key Alias: ${keyAlias}`);
|
||||
|
||||
Log.log(` MD5 Fingerprint: ${formatFingerprint(md5CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA1 Fingerprint: ${formatFingerprint(sha1CertificateFingerprint ?? null)}`);
|
||||
Log.log(` SHA256 Fingerprint: ${formatFingerprint(sha256CertificateFingerprint ?? null)}`);
|
||||
Log.log(` Updated ${fromNow(new Date(updatedAt))} ago`);
|
||||
} else {
|
||||
Log.log(` None assigned yet`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
export function displayAndroidAppCredentials({
|
||||
appLookupParams,
|
||||
legacyAppCredentials,
|
||||
appCredentials,
|
||||
}: {
|
||||
appLookupParams: AppLookupParams;
|
||||
legacyAppCredentials: CommonAndroidAppCredentialsFragment | null;
|
||||
appCredentials: CommonAndroidAppCredentialsFragment | null;
|
||||
}): void {
|
||||
const { projectName, androidApplicationIdentifier } = appLookupParams;
|
||||
Log.log(chalk.bold(`Android Credentials`));
|
||||
Log.log(` Project: ${projectName}`);
|
||||
Log.log(` Application Identifier: ${androidApplicationIdentifier}`);
|
||||
Log.newLine();
|
||||
|
||||
if (appCredentials) {
|
||||
displayEASAndroidAppCredentials(legacyAppCredentials, appCredentials);
|
||||
}
|
||||
|
||||
if (legacyAppCredentials) {
|
||||
displayLegacyAndroidAppCredentials(legacyAppCredentials, appCredentials);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { confirmAsync } from '../prompts';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import AndroidApi from './android/api/Client';
|
||||
import iOSApi from './ios/api/Client';
|
||||
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
|
||||
import * as IosGraphqlClient from './ios/api/GraphqlClient';
|
||||
import AppStoreApi from './ios/appstore/AppStoreApi';
|
||||
|
||||
@@ -27,8 +27,8 @@ export interface Context {
|
||||
readonly user: Actor;
|
||||
readonly nonInteractive: boolean;
|
||||
readonly android: AndroidApi;
|
||||
readonly ios: iOSApi;
|
||||
readonly newIos: typeof IosGraphqlClient;
|
||||
readonly newAndroid: typeof AndroidGraphqlClient;
|
||||
readonly ios: typeof IosGraphqlClient;
|
||||
readonly appStore: AppStoreApi;
|
||||
readonly hasProjectContext: boolean;
|
||||
readonly exp: ExpoConfig;
|
||||
@@ -56,8 +56,8 @@ export async function createCredentialsContextAsync(
|
||||
|
||||
class CredentialsContext implements Context {
|
||||
public readonly android = new AndroidApi();
|
||||
public readonly ios = new iOSApi();
|
||||
public readonly newIos = IosGraphqlClient;
|
||||
public readonly newAndroid = AndroidGraphqlClient;
|
||||
public readonly ios = IosGraphqlClient;
|
||||
public readonly appStore: AppStoreApi;
|
||||
public readonly nonInteractive: boolean;
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
|
||||
@@ -109,8 +109,8 @@ describe('credentialsJson', () => {
|
||||
expect(result).toEqual({
|
||||
provisioningProfile: 'c29tZWJpbmFyeWNvbnRlbnQ=',
|
||||
distributionCertificate: {
|
||||
certP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certPassword: 'certPass',
|
||||
certificateP12: 'c29tZWJpbmFyeWNvbnRlbnQy',
|
||||
certificatePassword: 'certPass',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -211,15 +211,15 @@ describe('credentialsJson', () => {
|
||||
target1: {
|
||||
provisioningProfile: 'cHByb2ZpbGUtMS1zb21lYmluYXJ5Y29udGVudA==',
|
||||
distributionCertificate: {
|
||||
certP12: 'Y2VydC0xLXNvbWViaW5hcnljb250ZW50',
|
||||
certPassword: 'cert-pass-1',
|
||||
certificateP12: 'Y2VydC0xLXNvbWViaW5hcnljb250ZW50',
|
||||
certificatePassword: 'cert-pass-1',
|
||||
},
|
||||
},
|
||||
target2: {
|
||||
provisioningProfile: 'cHByb2ZpbGUtMi1zb21lYmluYXJ5Y29udGVudA==',
|
||||
distributionCertificate: {
|
||||
certP12: 'Y2VydC0yLXNvbWViaW5hcnljb250ZW50',
|
||||
certPassword: 'cert-pass-2',
|
||||
certificateP12: 'Y2VydC0yLXNvbWViaW5hcnljb250ZW50',
|
||||
certificatePassword: 'cert-pass-2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,17 +3,20 @@ import { vol } from 'memfs';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../../__tests__/utils';
|
||||
import { IosDistributionType } from '../../../graphql/generated';
|
||||
import { testKeystore } from '../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../__tests__/fixtures-context';
|
||||
import {
|
||||
getNewIosApiMockWithoutCredentials,
|
||||
testAllCredentialsForApp,
|
||||
testIosDistCredential,
|
||||
testProvisioningProfile,
|
||||
testCommonIosAppCredentialsFragment,
|
||||
} from '../../__tests__/fixtures-ios';
|
||||
import { getAppLookupParamsFromContext } from '../../ios/actions/BuildCredentialsUtils';
|
||||
import { updateAndroidCredentialsAsync, updateIosCredentialsAsync } from '../update';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('prompts');
|
||||
jest.mock('../../../project/ios/bundleIdentifier');
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
@@ -195,9 +198,10 @@ describe('update credentials.json', () => {
|
||||
it('should update ios credentials in credentials.json if www returns valid credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => testAllCredentialsForApp),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
@@ -213,12 +217,19 @@ describe('update credentials.json', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
await updateIosCredentialsAsync(ctx, 'bundleIdentifier');
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
const certP12 = await fs.readFile('./cert.p12', 'base64');
|
||||
const pprofile = await fs.readFile('./pprofile', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
expect(certP12).toEqual(testAllCredentialsForApp.distCredentials.certP12);
|
||||
expect(pprofile).toEqual(testAllCredentialsForApp.credentials.provisioningProfile);
|
||||
expect(certP12).toEqual(
|
||||
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0].distributionCertificate
|
||||
?.certificateP12
|
||||
);
|
||||
expect(pprofile).toEqual(
|
||||
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0].provisioningProfile
|
||||
?.provisioningProfile
|
||||
);
|
||||
expect(credJson).toEqual({
|
||||
ios: {
|
||||
provisioningProfilePath: 'pprofile',
|
||||
@@ -232,12 +243,14 @@ describe('update credentials.json', () => {
|
||||
it('should create credentials.json provisioning profile and distribution certificate if credentials.json does not exist', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => testAllCredentialsForApp),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
await updateIosCredentialsAsync(ctx, 'bundleIdentifier');
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
const certP12 = await fs.readFile('./ios/certs/dist-cert.p12', 'base64');
|
||||
const pprofile = await fs.readFile('./ios/certs/profile.mobileprovision', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
@@ -256,9 +269,8 @@ describe('update credentials.json', () => {
|
||||
it('should not do anything if no credentials are returned from www', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => null),
|
||||
getDistributionCertificateAsync: jest.fn(() => null),
|
||||
getProvisioningProfileAsync: jest.fn(() => null),
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(() => null),
|
||||
},
|
||||
});
|
||||
const credJson = {
|
||||
@@ -276,11 +288,12 @@ describe('update credentials.json', () => {
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
try {
|
||||
await updateIosCredentialsAsync(ctx, 'bundleIdentifier');
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
throw new Error('updateIosCredentialsAsync should throw na error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'There are no credentials configured for this project on EAS servers'
|
||||
'There are no credentials configured for the APP_STORE distribution of this project on EAS servers'
|
||||
);
|
||||
}
|
||||
const certP12 = await fs.readFile('./cert.p12', 'base64');
|
||||
@@ -291,15 +304,15 @@ describe('update credentials.json', () => {
|
||||
expect(newCredJson).toEqual(credJson);
|
||||
});
|
||||
it('should display confirm prompt if some credentials are missing in www (confirm: true)', async () => {
|
||||
// create deep clone the quick and dirty way
|
||||
const testIosAppCredentialsNoDistCert = JSON.parse(
|
||||
JSON.stringify(testCommonIosAppCredentialsFragment)
|
||||
);
|
||||
testIosAppCredentialsNoDistCert.iosAppBuildCredentialsList[0].distributionCertificate = null;
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => ({
|
||||
...testAllCredentialsForApp,
|
||||
distCredentialsId: undefined,
|
||||
distCredentials: undefined,
|
||||
})),
|
||||
getDistributionCertificateAsync: jest.fn(() => null),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(() => testIosAppCredentialsNoDistCert),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -322,7 +335,8 @@ describe('update credentials.json', () => {
|
||||
'./pprofile': 'somebinarycontent',
|
||||
'./cert.p12': 'somebinarycontent2',
|
||||
});
|
||||
await updateIosCredentialsAsync(ctx, 'bundleIdentifier');
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore);
|
||||
const certP12Exists = await fs.pathExists('./cert.p12');
|
||||
const pprofile = await fs.readFile('./pprofile', 'base64');
|
||||
const credJson = await fs.readJson('./credentials.json');
|
||||
@@ -337,9 +351,10 @@ describe('update credentials.json', () => {
|
||||
it('should throw an error if credentials.json contains credentials for multi-target projects', async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => testAllCredentialsForApp),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
@@ -366,16 +381,18 @@ describe('update credentials.json', () => {
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
'./dist-cert-2.p12': 'cert-2-somebinarycontent',
|
||||
});
|
||||
await expect(() => updateIosCredentialsAsync(ctx, 'bundleIdentifier')).rejects.toThrow(
|
||||
/Updating credentials.json failed/
|
||||
);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await expect(() =>
|
||||
updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore)
|
||||
).rejects.toThrow(/Updating credentials.json failed/);
|
||||
});
|
||||
it(`should throw an error if there is a typo in credentials.json (because we can't tell if this is a target name or not)`, async () => {
|
||||
const ctx = createCtxMock({
|
||||
ios: {
|
||||
getAppCredentialsAsync: jest.fn(() => testAllCredentialsForApp),
|
||||
getDistributionCertificateAsync: jest.fn(() => testIosDistCredential),
|
||||
getProvisioningProfileAsync: jest.fn(() => testProvisioningProfile),
|
||||
...getNewIosApiMockWithoutCredentials(),
|
||||
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
|
||||
() => testCommonIosAppCredentialsFragment
|
||||
),
|
||||
},
|
||||
});
|
||||
vol.fromJSON({
|
||||
@@ -392,9 +409,10 @@ describe('update credentials.json', () => {
|
||||
'./pprofile-1.mobileprovision': 'pprofile-1-somebinarycontent',
|
||||
'./dist-cert-1.p12': 'cert-1-somebinarycontent',
|
||||
});
|
||||
await expect(() => updateIosCredentialsAsync(ctx, 'bundleIdentifier')).rejects.toThrow(
|
||||
/Updating credentials.json failed/
|
||||
);
|
||||
const appLookupParams = getAppLookupParamsFromContext(ctx);
|
||||
await expect(() =>
|
||||
updateIosCredentialsAsync(ctx, appLookupParams, IosDistributionType.AppStore)
|
||||
).rejects.toThrow(/Updating credentials.json failed/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ export interface CredentialsJson {
|
||||
keystorePath: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
keyPassword?: string;
|
||||
};
|
||||
};
|
||||
ios?: CredentialsJsonIosCredentials | Record<string, CredentialsJsonIosCredentials>;
|
||||
@@ -30,7 +30,7 @@ const IosTargetCredentials = Joi.object({
|
||||
provisioningProfilePath: Joi.string().required(),
|
||||
distributionCertificate: Joi.object({
|
||||
path: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
password: Joi.string().allow('').required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
@@ -38,9 +38,9 @@ const CredentialsJsonSchema = Joi.object({
|
||||
android: Joi.object({
|
||||
keystore: Joi.object({
|
||||
keystorePath: Joi.string().required(),
|
||||
keystorePassword: Joi.string().required(),
|
||||
keystorePassword: Joi.string().allow('').required(),
|
||||
keyAlias: Joi.string().required(),
|
||||
keyPassword: Joi.string().required(),
|
||||
keyPassword: Joi.string().allow(''),
|
||||
}).required(),
|
||||
}),
|
||||
ios: [
|
||||
@@ -59,8 +59,8 @@ interface AndroidCredentials {
|
||||
export interface IosTargetCredentials {
|
||||
provisioningProfile: string;
|
||||
distributionCertificate: {
|
||||
certP12: string;
|
||||
certPassword: string;
|
||||
certificateP12: string;
|
||||
certificatePassword: string;
|
||||
};
|
||||
}
|
||||
export type IosTargetCredentialsMap = Record<string, IosTargetCredentials>;
|
||||
@@ -127,11 +127,11 @@ async function readCredentialsForTargetAsync(
|
||||
'base64'
|
||||
),
|
||||
distributionCertificate: {
|
||||
certP12: await fs.readFile(
|
||||
certificateP12: await fs.readFile(
|
||||
getAbsolutePath(projectDir, targetCredentials.distributionCertificate.path),
|
||||
'base64'
|
||||
),
|
||||
certPassword: targetCredentials.distributionCertificate.password,
|
||||
certificatePassword: targetCredentials.distributionCertificate.password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user