mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98308bb3bd | ||
|
|
8b9de84855 | ||
|
|
496d6f5a6e | ||
|
|
89d1572f9d | ||
|
|
76f591583d | ||
|
|
5d40cc1bc0 | ||
|
|
60a349f2f5 | ||
|
|
439d45d6e1 | ||
|
|
cc48482595 | ||
|
|
a4fb2e20d3 | ||
|
|
ec16d92724 | ||
|
|
34e7720e6f | ||
|
|
938511d5fd | ||
|
|
b473e72c73 | ||
|
|
2816b432d8 | ||
|
|
56f0f7792d | ||
|
|
5236de797b | ||
|
|
fb33298024 | ||
|
|
66c6c8ab0b | ||
|
|
552ef1afe9 | ||
|
|
b35a4efdaa | ||
|
|
7244fc83dc | ||
|
|
1da9573040 | ||
|
|
4e8fc059a4 | ||
|
|
6bb3dd94f7 | ||
|
|
079328cf5a | ||
|
|
4ff8415f71 | ||
|
|
e8d14557d6 | ||
|
|
924f51ecb6 | ||
|
|
f297df7f67 | ||
|
|
ef8d6d08bc | ||
|
|
6ba59a321b | ||
|
|
6e87e94ab3 | ||
|
|
0dbb6890b2 | ||
|
|
2b228272c8 | ||
|
|
b5077bc4fd | ||
|
|
b07315e32d | ||
|
|
3c5bb3e815 | ||
|
|
5755dfdd3a | ||
|
|
d9e66437f4 | ||
|
|
faa75393a2 | ||
|
|
759d749a76 | ||
|
|
34c307c829 | ||
|
|
7b101601f2 | ||
|
|
668aeb2569 | ||
|
|
1f68e225d3 | ||
|
|
c530d06e22 | ||
|
|
9bfc44bdbc | ||
|
|
d11e43f8e0 | ||
|
|
c19a737287 | ||
|
|
2790fd4cd5 | ||
|
|
1b713af42f | ||
|
|
3ac6472682 | ||
|
|
4b8a9d195f | ||
|
|
30c5e9f4c1 | ||
|
|
d0c8f6bab7 | ||
|
|
faf6bffc62 | ||
|
|
ad55fdae37 | ||
|
|
b5f77404cb | ||
|
|
a1d4f26236 | ||
|
|
6c7bae2349 | ||
|
|
d7e48c2894 | ||
|
|
918a7f9501 | ||
|
|
852dac030f | ||
|
|
911cc90b93 | ||
|
|
b9234abff1 | ||
|
|
574bde9831 | ||
|
|
78837a413a | ||
|
|
6f63315f8e | ||
|
|
e8ac97c8cc | ||
|
|
a6fc31787d | ||
|
|
3056d8e23a | ||
|
|
1bf6757a72 | ||
|
|
b948dd5674 | ||
|
|
0e7a8bfe5e | ||
|
|
b86696fcdf | ||
|
|
f334bdbcc7 | ||
|
|
f16433048a | ||
|
|
e83061de36 | ||
|
|
73e0170836 | ||
|
|
078c7287d3 | ||
|
|
a65e691325 | ||
|
|
b306109676 | ||
|
|
035ca03b96 | ||
|
|
b3d092423b | ||
|
|
b768966ec4 | ||
|
|
1b40c1c20a | ||
|
|
98344b8fc2 | ||
|
|
f5480b4770 | ||
|
|
a2929ad364 | ||
|
|
6deee68a00 | ||
|
|
16d190ab84 | ||
|
|
6f2fc7c37b | ||
|
|
1b78407f15 | ||
|
|
05c8fde824 | ||
|
|
d9af6cb301 | ||
|
|
365e71f042 | ||
|
|
e74c606efa | ||
|
|
28f3a92c38 | ||
|
|
be9fb59e6d | ||
|
|
365a7465f4 | ||
|
|
85a66629c8 | ||
|
|
8a2d68fcb5 | ||
|
|
62819e0956 | ||
|
|
5c6880f4ce | ||
|
|
d5f562edf8 | ||
|
|
077d8b3d86 | ||
|
|
21c8131bcf | ||
|
|
18bf41782f | ||
|
|
0513fc51a6 | ||
|
|
528988dc0f | ||
|
|
7f46baeb21 | ||
|
|
608d61ed69 | ||
|
|
2b2b20a0f9 | ||
|
|
77423d1179 | ||
|
|
ebc277dfe4 | ||
|
|
427c8f152d | ||
|
|
1c245d23b4 | ||
|
|
29c4695351 | ||
|
|
7b968f6e0d | ||
|
|
8a38cc24e1 | ||
|
|
a85b9e2d80 | ||
|
|
e0f69327da | ||
|
|
76d4e65d92 | ||
|
|
58917ecbb4 | ||
|
|
e75a102361 | ||
|
|
061f626b2c | ||
|
|
3159550468 | ||
|
|
a20bd0051e | ||
|
|
0b64d1c8df | ||
|
|
fe37600323 | ||
|
|
ab507d0bdd | ||
|
|
7505b8d40a | ||
|
|
b31ddee6e7 | ||
|
|
01ce802b73 | ||
|
|
f75ef22154 | ||
|
|
7d3f4e8fd4 | ||
|
|
004caf4bb7 | ||
|
|
5a16f3d6d8 | ||
|
|
d2e4cb5fc6 | ||
|
|
2c1c9b91cb | ||
|
|
b447194c85 | ||
|
|
9ab095597a | ||
|
|
b1edff7242 | ||
|
|
4d9e37269c | ||
|
|
e7b48949db | ||
|
|
1c3edbe492 | ||
|
|
deaa3a9105 | ||
|
|
cbfcdaec5d | ||
|
|
37a32c93ee | ||
|
|
6461a78fa2 | ||
|
|
86c8ac093f | ||
|
|
608cdff981 | ||
|
|
4dc4ef85fa | ||
|
|
eaf15acb76 | ||
|
|
ae4ca91658 | ||
|
|
23ebb77952 | ||
|
|
ed2352f289 | ||
|
|
38ae8b68f1 | ||
|
|
5e02807ac9 | ||
|
|
f05d6ca3a2 | ||
|
|
b57082647d | ||
|
|
9d4fdc3c75 | ||
|
|
d1af9e385f | ||
|
|
59bbe19dde | ||
|
|
5e5b55f9e4 | ||
|
|
a1d086ffc6 | ||
|
|
2a96d04be3 | ||
|
|
d126ee988a | ||
|
|
4bb550e3bb | ||
|
|
6a55c65908 | ||
|
|
ab3495da5e | ||
|
|
63b67635e0 | ||
|
|
4b5cda3bbc | ||
|
|
63e3c1f2ab | ||
|
|
3f1543989f | ||
|
|
1c964d2ab1 | ||
|
|
bc282b6262 | ||
|
|
17661d1cf0 | ||
|
|
8d35745124 | ||
|
|
3740ee4ef9 | ||
|
|
751d3d1287 | ||
|
|
1fd14c07e1 | ||
|
|
39ea1705d8 | ||
|
|
2c96b9929d | ||
|
|
0dae24ae47 | ||
|
|
a63efe01a1 | ||
|
|
803457b37a | ||
|
|
dcb1305501 | ||
|
|
97fe2d62d2 | ||
|
|
ca0850b82b | ||
|
|
039a7b408b | ||
|
|
68660ae845 | ||
|
|
34bb2c8a75 | ||
|
|
ac5cf3d7dd | ||
|
|
8d686b855b | ||
|
|
19973bb86f | ||
|
|
d132ac09de | ||
|
|
4c5e5d4ded | ||
|
|
44af16efe1 | ||
|
|
f34487cc8e | ||
|
|
7ea1c5de17 | ||
|
|
261f611770 | ||
|
|
f9337f419a | ||
|
|
4280e10836 | ||
|
|
a2fe99c97e | ||
|
|
868fd399a4 | ||
|
|
7d60ba2cea | ||
|
|
db7461a569 | ||
|
|
facffbdcd7 | ||
|
|
dbdefa0e3d | ||
|
|
66daeade84 | ||
|
|
48ebd9bb78 | ||
|
|
3d3574d351 | ||
|
|
8da6adf4da | ||
|
|
bd2d39f26b | ||
|
|
e16e1f3871 | ||
|
|
d8bd6a1548 | ||
|
|
0f2fad3a65 | ||
|
|
78e05c48c8 | ||
|
|
fa10404843 | ||
|
|
5bf2c3a65b | ||
|
|
ec71e1168f | ||
|
|
8bf28029dc | ||
|
|
fc3c1e73a8 | ||
|
|
bb3915dc7e | ||
|
|
753df2c428 | ||
|
|
3111492f83 | ||
|
|
9c95755a46 | ||
|
|
da727ba170 | ||
|
|
4571910c4d | ||
|
|
c7cfcafff8 | ||
|
|
6f913f7320 | ||
|
|
0a49296231 | ||
|
|
7fc09b2376 | ||
|
|
fed28a87be |
No files matched your search
+7
-13
@@ -3,24 +3,18 @@ commentConfiguration:
|
||||
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
|
||||
rules:
|
||||
- patterns: ['**/*']
|
||||
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/build/**']
|
||||
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/commands/build/**']
|
||||
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/submit/**']
|
||||
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/commands/submit.ts']
|
||||
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
|
||||
mentions: ['khamilowicz', 'sjchmiela', 'radoslawkrzemien']
|
||||
- patterns: ['packages/eas-cli/src/update/**']
|
||||
mentions: ['EvanBacon', 'byCedric', 'wschurman']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/commands/update/**']
|
||||
mentions: ['EvanBacon', 'byCedric', 'wschurman']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/channel/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/commands/channel/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/schema/**']
|
||||
mentions: ['bycedric']
|
||||
- patterns: ['packages/eas-cli/src/metadata/**']
|
||||
mentions: ['bycedric']
|
||||
- patterns: ['packages/eas-cli/src/commands/metadata/**']
|
||||
mentions: ['bycedric']
|
||||
- patterns: ['packages/eas-json/**']
|
||||
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check if CHANGELOG.md was updated
|
||||
id: changelog-updated
|
||||
uses: tj-actions/changed-files@v41
|
||||
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076
|
||||
with:
|
||||
files: CHANGELOG.md
|
||||
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
|
||||
|
||||
@@ -226,16 +226,3 @@ jobs:
|
||||
SLACK_USERNAME: 'EAS CLI CI'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_FOOTER: ''
|
||||
open-pr-in-universe:
|
||||
name: Open a PR with `@expo/eas-json` bump in universe
|
||||
needs: publish-to-npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Open a PR with `@expo/eas-json` version bump in universe
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
repo: expo/universe
|
||||
ref: main
|
||||
workflow: update-eas-json.yml
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
@@ -28,6 +28,9 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn typecheck
|
||||
- run: |
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Test"
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
env:
|
||||
@@ -49,7 +52,7 @@ jobs:
|
||||
|
||||
notify-slack:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
needs: [test]
|
||||
name: Notify Slack
|
||||
if: ${{ github.ref == 'refs/heads/main' && always() }}
|
||||
steps:
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
destination_branch: 'main'
|
||||
pr_title: 'Upgrade eas-build dependencies'
|
||||
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
|
||||
pr_reviewer: 'szdziedzic,radoslawkrzemien,sjchmiela,khamilowicz'
|
||||
pr_reviewer: 'radoslawkrzemien,sjchmiela,khamilowicz'
|
||||
github_token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
|
||||
- uses: actions/github-script@v3
|
||||
|
||||
+363
@@ -12,6 +12,369 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [16.2.1](https://github.com/expo/eas-cli/releases/tag/v16.2.1) - 2025-04-04
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Bump `@expo/apple-utils` to use async JWT API. ([#2973](https://github.com/expo/eas-cli/pull/2973) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [16.2.0](https://github.com/expo/eas-cli/releases/tag/v16.2.0) - 2025-04-03
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add environment flag to `eas fingerprint:compare`. ([#2954](https://github.com/expo/eas-cli/pull/2954) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add build-profile flag to `eas fingerprint:generate`. ([#2966](https://github.com/expo/eas-cli/pull/2966) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove hidden flag from `eas fingerprint:generate`. ([#2965](https://github.com/expo/eas-cli/pull/2965) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Refactor `eas update` command to improve code readability. ([#2976](https://github.com/expo/eas-cli/pull/2976) by [@quinlanj](https://github.com/quinlanj))
|
||||
- `eas update`: add warning if no build exists with fingerprint. ([#2977](https://github.com/expo/eas-cli/pull/2977) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.1.0](https://github.com/expo/eas-cli/releases/tag/v16.1.0) - 2025-03-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add environment flag to `eas fingerprint:generate`. ([#2951](https://github.com/expo/eas-cli/pull/2951) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add `corepack` field to `eas.json`. ([#2964](https://github.com/expo/eas-cli/pull/2964) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [16.0.1](https://github.com/expo/eas-cli/releases/tag/v16.0.1) - 2025-03-20
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make `eas update:configure` re-apply configuration from app.json /app.config.js when run multiple times. ([#2957](https://github.com/expo/eas-cli/pull/2957) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [16.0.0](https://github.com/expo/eas-cli/releases/tag/v16.0.0) - 2025-03-19
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Add support for `.easignore` when `requireCommit` is set to `true`. ([#2942](https://github.com/expo/eas-cli/pull/2942) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- Up to 15.0.0, if `requireCommit` was `true`, `.easignore` was silently ignored.
|
||||
- Versions 15.0.0-15.0.13 started using `.easignore` to skip files from being bundled into a tarball when `requireCommit` was `true`. This was an unexpected change in behavior.
|
||||
- To clear this up, versions 15.0.13-15.0.15 were erroring if `.easignore` was present when `requireCommit` was `true`.
|
||||
- `[email protected]` formalizes the 15.0.0-15.0.13 behavior by adhering to `.easignore` even when `requireCommit` is set to `true`.
|
||||
- If you know what you're doing and you want to suppress a warning printed, you can do so by setting `EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING` environment variable to `true`.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add requestId to ApiV2Error. ([#2941](https://github.com/expo/eas-cli/pull/2941) by [@wschurman](https://github.com/wschurman))
|
||||
- Release `eas fingerprint:generate` in hidden mode ([#2937](https://github.com/expo/eas-cli/pull/2937) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use correct logic to determine whether artifacts have expired in `eas build:run` command. ([#2931](https://github.com/expo/eas-cli/pull/2931) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [15.0.15](https://github.com/expo/eas-cli/releases/tag/v15.0.15) - 2025-03-12
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Pass through the updates version to `setUpdatesConfigAsync`, which expects it in order to determine which field values to use. ([#2934](https://github.com/expo/eas-cli/pull/2934) by [@brentvatne](https://github.com/brentvatne)).
|
||||
|
||||
## [15.0.14](https://github.com/expo/eas-cli/releases/tag/v15.0.14) - 2025-03-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- In `EAS_NO_VCS=1`, use Git for repository root when `EAS_PROJECT_ROOT` is not set. ([#2901](https://github.com/expo/eas-cli/pull/2901) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Suggest using `eas build:dev` for matching configurations. ([#2929](https://github.com/expo/eas-cli/pull/2929) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [15.0.13](https://github.com/expo/eas-cli/releases/tag/v15.0.13) - 2025-03-04
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed `.git` being always unexpectedly removed if you had `requireCommit: true` and `.easignore` present. ([#2925](https://github.com/expo/eas-cli/pull/2925) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Fix `eas fingerprint:compare` URL generation and pretty prints. ([#2909](https://github.com/expo/eas-cli/pull/2909) by [@quinlanj](https://github.com/quinlanj))
|
||||
- fix formatFields to handle empty array. ([#2914](https://github.com/expo/eas-cli/pull/2914) by [@quinlanj](https://github.com/quinlanj))
|
||||
- fix git diff header in `fingerprint:compare`. ([#2915](https://github.com/expo/eas-cli/pull/2915) by [@quinlanj](https://github.com/quinlanj))
|
||||
- amend fingerprint compare url. ([#2923](https://github.com/expo/eas-cli/pull/2923) by [@quinlanj](https://github.com/quinlanj))
|
||||
- improve error message for `applicationIdSuffix`. ([#2924](https://github.com/expo/eas-cli/pull/2924) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [15.0.12](https://github.com/expo/eas-cli/releases/tag/v15.0.12) - 2025-02-22
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix APNS key creation. ([#2916](https://github.com/expo/eas-cli/pull/2916) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [15.0.11](https://github.com/expo/eas-cli/releases/tag/v15.0.11) - 2025-02-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix internal TestFlight group creation. ([#2906](https://github.com/expo/eas-cli/pull/2906) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Narrow amount of data queried for basic update channel operations. ([#2901](https://github.com/expo/eas-cli/pull/2901) by [@wschurman](https://github.com/wschurman))
|
||||
- Fix `eas fingerprint:compare` description. ([#2908](https://github.com/expo/eas-cli/pull/2908) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Skip auto-creation of TestFlight group when there are already exisitng TestFlight groups and allow to opt out of the behavior by setting `EAS_NO_AUTO_TESTFLIGHT_SETUP` env var. ([#2856](https://github.com/expo/eas-cli/pull/2856) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [15.0.10](https://github.com/expo/eas-cli/releases/tag/v15.0.10) - 2025-02-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix files deleted in working directory not being removed from the project archive when `requireCommit` is false. ([#2900](https://github.com/expo/eas-cli/pull/2900) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [15.0.9](https://github.com/expo/eas-cli/releases/tag/v15.0.9) - 2025-02-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix files not being ignored when creating a tarball on Windows in Git repository in no `requireCommit` mode. ([#2894](https://github.com/expo/eas-cli/pull/2894) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [15.0.8](https://github.com/expo/eas-cli/releases/tag/v15.0.8) - 2025-02-09
|
||||
|
||||
## [15.0.7](https://github.com/expo/eas-cli/releases/tag/v15.0.7) - 2025-02-09
|
||||
|
||||
## [15.0.6](https://github.com/expo/eas-cli/releases/tag/v15.0.6) - 2025-02-07
|
||||
|
||||
## [15.0.5](https://github.com/expo/eas-cli/releases/tag/v15.0.5) - 2025-02-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Do not copy files over onto a cloned Git repository when packing the project archive if `requireCommit` is true. ([#2885](https://github.com/expo/eas-cli/pull/2885) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- Fix `EISDIR` error when archiving project with submodules ignored. ([#2884](https://github.com/expo/eas-cli/pull/2884) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [15.0.4](https://github.com/expo/eas-cli/releases/tag/v15.0.4) - 2025-02-05
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed `GitClient` not respecting `.easignore` file. ([#2873](https://github.com/expo/eas-cli/pull/2873) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- Fix symlink support in `makeShallowCopyAsync`. ([#2874](https://github.com/expo/eas-cli/pull/2874) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- Allow excluding `.git` directory from project archive by adding it to `.easignore`. ([#2879](https://github.com/expo/eas-cli/pull/2879) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Popup website in fingerprint:compare. ([#2859](https://github.com/expo/eas-cli/pull/2859) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Fix fingerprint:compare URL. ([#2861](https://github.com/expo/eas-cli/pull/2861) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Make less gql calls in fingerprint:compare. ([#2860](https://github.com/expo/eas-cli/pull/2860) by [@quinlanj](https://github.com/quinlanj))
|
||||
- No longer require owner field for SDK >= 53 or canary. ([#2835](https://github.com/expo/eas-cli/pull/2835) by [@wschurman](https://github.com/wschurman))
|
||||
- Add --open flag to fingerprint:compare. ([#2872](https://github.com/expo/eas-cli/pull/2872) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [15.0.3](https://github.com/expo/eas-cli/releases/tag/v15.0.3) - 2025-02-04
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed EAS server environment variables does not pass to `npx expo-updates runtimeversion:resolve` call. ([#2867](https://github.com/expo/eas-cli/pull/2867) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## [15.0.2](https://github.com/expo/eas-cli/releases/tag/v15.0.2) - 2025-02-04
|
||||
|
||||
## [15.0.1](https://github.com/expo/eas-cli/releases/tag/v15.0.1) - 2025-02-04
|
||||
|
||||
## [15.0.0](https://github.com/expo/eas-cli/releases/tag/v15.0.0) - 2025-02-04
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Use Git to archive projects containing a Git repository. (Previously, Git would only be used if `requireCommit` flag in `eas.json` was set to `true`.) ([#2841](https://github.com/expo/eas-cli/pull/2841) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Print warning for `NoVcsClient` only once. ([#2863](https://github.com/expo/eas-cli/pull/2863) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add update support for fingerprint:compare. ([#2850](https://github.com/expo/eas-cli/pull/2850) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add update group id support for fingerprint:compare. ([#2851](https://github.com/expo/eas-cli/pull/2851) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add better interactive support for fingerprint:compare. ([#2854](https://github.com/expo/eas-cli/pull/2854) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [14.7.1](https://github.com/expo/eas-cli/releases/tag/v14.7.1) - 2025-01-31
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Account for `ios.config.usesNonExemptEncryption` in non-exempt encryption status prompt. ([#2852](https://github.com/expo/eas-cli/pull/2852) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [14.7.0](https://github.com/expo/eas-cli/releases/tag/v14.7.0) - 2025-01-30
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--submit` and `-s` as aliases for `--auto-submit` flag. ([#2846](https://github.com/expo/eas-cli/pull/2846) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Skip non-exempt check in non-interactive mode. ([#2849](https://github.com/expo/eas-cli/pull/2849) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [14.6.0](https://github.com/expo/eas-cli/releases/tag/v14.6.0) - 2025-01-30
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Prompt to set non-exempt encryption status for the iOS app to support faster store submissions. ([#2843](https://github.com/expo/eas-cli/pull/2843) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Automatically create internal TestFlight group in EAS Submit command. ([#2839](https://github.com/expo/eas-cli/pull/2839) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Sanitize and generate names for EAS Submit to prevent failures due to invalid characters or taken names. ([#2842](https://github.com/expo/eas-cli/pull/2842) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Release `eas fingerprint:compare`. ([#2821](https://github.com/expo/eas-cli/pull/2821) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Make new autoIncremented builds start at nr 1 by default ([#2828](https://github.com/expo/eas-cli/pull/2828) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
|
||||
## [14.5.0](https://github.com/expo/eas-cli/releases/tag/v14.5.0) - 2025-01-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Allow filtering by `--fingerprint_hash` in `eas build:list` command. ([#2818](https://github.com/expo/eas-cli/pull/2818) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Ensure that the AASA file is served with content type application/json ([#2829](https://github.com/expo/eas-cli/pull/2829) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Ensure that the AppleID provided in prompt or saved to cache does not contain invalid unprintable characters ([#2830](https://github.com/expo/eas-cli/pull/2830) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Fix logs typos in the `eas deploy` command. ([#2822](https://github.com/expo/eas-cli/pull/2822) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Make `deploy` the top level command for hosting. ([#2824](https://github.com/expo/eas-cli/pull/2824) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Allow longer submit profile extension chain (up to 5, same as build profile) ([#2831](https://github.com/expo/eas-cli/pull/2831) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
- Make variable naming more explicit, remove deprecated runtimeFingerprintSource uses. ([#2816](https://github.com/expo/eas-cli/pull/2816) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [14.4.1](https://github.com/expo/eas-cli/releases/tag/v14.4.1) - 2025-01-15
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Enable shell execution for env:exec commands. ([#2788](https://github.com/expo/eas-cli/pull/2788) by [@tharakadesilva](https://github.com/tharakadesilva))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Make automatic env resolution message shorter. ([#2806](https://github.com/expo/eas-cli/pull/2806) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Make "No remote versions are configured" message green instead of yellow. ([#2805](https://github.com/expo/eas-cli/pull/2805) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Upload local fingerprint on `eas fingerprint:compare`. ([#2808](https://github.com/expo/eas-cli/pull/2808) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Upgrade `eas-cli-local-build-plugin` to `1.0.163` to support Bun's new text-based lock file in local builds. ([#2817](https://github.com/expo/eas-cli/pull/2817) by [@shiroyasha9](https://github.com/shiroyasha9))
|
||||
|
||||
## [14.4.0](https://github.com/expo/eas-cli/releases/tag/v14.4.0) - 2025-01-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Load `.env` variables even when `--environment` is specified for `deploy` command. Conflicts will be highlighted by a warning message. ([#2783](https://github.com/expo/eas-cli/pull/2783) by [@kitten](https://github.com/kitten))
|
||||
- Silence all non-command output in non-interactive mode of eas env:exec. ([#2800](https://github.com/expo/eas-cli/pull/2800) by [@wschurman](https://github.com/wschurman))
|
||||
- Unhide `deploy` and `deploy:alias` commands ([#2807](https://github.com/expo/eas-cli/pull/2807) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## [14.3.1](https://github.com/expo/eas-cli/releases/tag/v14.3.1) - 2025-01-08
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Bump `@expo/[email protected]` to support Bun text-based lock files. ([#2801](https://github.com/expo/eas-cli/pull/2801) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## [14.3.0](https://github.com/expo/eas-cli/releases/tag/v14.3.0) - 2025-01-07
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Upload `package.json` when uploading workflow sources. ([#2786](https://github.com/expo/eas-cli/pull/2786) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Show `eas deploy` upload error messages. ([#2771](https://github.com/expo/eas-cli/pull/2771) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Prevent EAS CLI dependencies check from running repeatedly. ([#2781](https://github.com/expo/eas-cli/pull/2781) by [@kitten](https://github.com/kitten))
|
||||
- Prevent optimistic request body parsing for `eas deploy`. ([#2784](https://github.com/expo/eas-cli/pull/2784) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Update log output for `worker` deploy and alias commands. ([#2780](https://github.com/expo/eas-cli/pull/2780) by [@kitten](https://github.com/kitten))
|
||||
- Update various messages wording. ([#2790](https://github.com/expo/eas-cli/pull/2790) by [@simek](https://github.com/simek))
|
||||
- Use node18 as tsconfig base. ([#2739](https://github.com/expo/eas-cli/pull/2739) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [14.2.0](https://github.com/expo/eas-cli/releases/tag/v14.2.0) - 2024-12-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas deploy --dry-run` flag to output tarball. ([#2761](https://github.com/expo/eas-cli/pull/2761) by [@kitten](https://github.com/kitten))
|
||||
- Allow specifying credentials for android builds. ([#2775](https://github.com/expo/eas-cli/pull/2775) by [@khamilowicz](https://github.com/khamilowicz))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Remove random branch name generation for --auto branch name non-vcs fallback. ([#2747](https://github.com/expo/eas-cli/pull/2747) by [@wschurman](https://github.com/wschurman))
|
||||
- Upgrade @expo/multipart-body-parser. ([#2751](https://github.com/expo/eas-cli/pull/2751) by [@wschurman](https://github.com/wschurman))
|
||||
- Pass env var flag to worker deployments. ([#2763](https://github.com/expo/eas-cli/pull/2763) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Fix request for switching providers when doing Apple auth. ([#2769](https://github.com/expo/eas-cli/pull/2769) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Improve logging and validation in `eas env:exec` command. ([#2762](https://github.com/expo/eas-cli/pull/2762) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [14.1.0](https://github.com/expo/eas-cli/releases/tag/v14.1.0) - 2024-12-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for enabeling broadcast Push Notifications capability option, by setting `ios.usesBroadcastPushNotifications` to `true` in app config. ([#2748](https://github.com/expo/eas-cli/pull/2748) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add link to FYI page mentioning the workaround for Apple SMS 2FA issue. ([#2752](https://github.com/expo/eas-cli/pull/2752) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [14.0.3](https://github.com/expo/eas-cli/releases/tag/v14.0.3) - 2024-12-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump `@expo/apple-utils` to fix sending two-factor authentication codes via SMS. ([#2750](https://github.com/expo/eas-cli/pull/2750) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Change update message to allow faster copy and paste. ([#2661](https://github.com/expo/eas-cli/pull/2661) by [@jonluca](https://github.com/jonluca))
|
||||
- Allow using `$schema` field in `eas.json`. ([#2624](https://github.com/expo/eas-cli/pull/2624) by [@saiichihashimoto](https://github.com/saiichihashimoto))
|
||||
|
||||
## [14.0.2](https://github.com/expo/eas-cli/releases/tag/v14.0.2) - 2024-12-06
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Print warning instead of throwing an error when validating `CFBundleShortVersionString` against App Store requirements. ([#2741](https://github.com/expo/eas-cli/pull/2741) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [14.0.1](https://github.com/expo/eas-cli/releases/tag/v14.0.1) - 2024-12-06
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Bump `@expo/apple-utils` to improve error handling and input validation and change auth headers. ([#2745](https://github.com/expo/eas-cli/pull/2745) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [14.0.0](https://github.com/expo/eas-cli/releases/tag/v14.0.0) - 2024-12-06
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Change behavior of roll-back-to-embedded to not use current project state. ([#2722](https://github.com/expo/eas-cli/pull/2722) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add hidden fingerprint:compare feature. ([#2736](https://github.com/expo/eas-cli/pull/2736) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update `@expo/config` and `@expo/config-plugins` to fix `eas build` command for bare iOS projects on Windows. ([#2744](https://github.com/expo/eas-cli/pull/2744) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Fix outdated gitignored Google services file warning. ([#2730](https://github.com/expo/eas-cli/pull/2730) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [13.4.2](https://github.com/expo/eas-cli/releases/tag/v13.4.2) - 2024-11-25
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `@expo` packages to SDK 52 versions. ([#2706](https://github.com/expo/eas-cli/pull/2706) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [13.4.1](https://github.com/expo/eas-cli/releases/tag/v13.4.1) - 2024-11-22
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Improve logging in `eas env:pull`. ([#2720](https://github.com/expo/eas-cli/pull/2720) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [13.4.0](https://github.com/expo/eas-cli/releases/tag/v13.4.0) - 2024-11-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Calculate fingerprint on each update. ([#2687](https://github.com/expo/eas-cli/pull/2687) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Calculate fingerprint on each update republish. ([#2708](https://github.com/expo/eas-cli/pull/2708) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add `eas env` commands. ([#2711](https://github.com/expo/eas-cli/pull/2711) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add `--environment` flag to `eas update` command. ([#2711](https://github.com/expo/eas-cli/pull/2711) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Load readable environment variables from EAS servers on every build. ([#2644](https://github.com/expo/eas-cli/pull/2644) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add environment field to `eas.schema.json`. ([#2719](https://github.com/expo/eas-cli/pull/2719) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Deprecate `eas secret` commands. ([#2705](https://github.com/expo/eas-cli/pull/2705) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [13.3.0](https://github.com/expo/eas-cli/releases/tag/v13.3.0) - 2024-11-18
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added `eas workflow` commands. [#2650](https://github.com/expo/eas-cli/pull/2650), [#2669](https://github.com/expo/eas-cli/pull/2669), [#2678](https://github.com/expo/eas-cli/pull/2678) by [@jonsamp](https://github.com/jonsamp) and [@sjchmiela](https://github.com/sjchmiela) ([#2683](https://github.com/expo/eas-cli/pull/2683) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [13.2.3](https://github.com/expo/eas-cli/releases/tag/v13.2.3) - 2024-11-15
|
||||
|
||||
## [13.2.2](https://github.com/expo/eas-cli/releases/tag/v13.2.2) - 2024-11-15
|
||||
|
||||
## [13.2.1](https://github.com/expo/eas-cli/releases/tag/v13.2.1) - 2024-11-14
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "13.2.3",
|
||||
"version": "16.2.2",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+599
-171
File diff suppressed because it is too large.
Load diff
@@ -12,3 +12,6 @@ if (process.env.TMPDIR) {
|
||||
(fs.realpath as any).native = jest.fn();
|
||||
|
||||
module.exports = fs;
|
||||
|
||||
// Ensure requiring node:fs returns the mock too. This is needed for expo/config and expo/json-file.
|
||||
jest.mock('node:fs', () => fs);
|
||||
Generated
+4823
-167
File diff suppressed because it is too large.
Load diff
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "13.2.3",
|
||||
"version": "16.2.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.0",
|
||||
"@expo/apple-utils": "2.1.12",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "9.0.4",
|
||||
"@expo/config-plugins": "8.0.10",
|
||||
"@expo/eas-build-job": "1.0.149",
|
||||
"@expo/eas-json": "13.2.2",
|
||||
"@expo/env": "^0.3.0",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "1.0.173",
|
||||
"@expo/eas-json": "16.1.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "1.0.117",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.1.3",
|
||||
"@expo/package-manager": "1.5.2",
|
||||
"@expo/pkcs12": "0.1.2",
|
||||
"@expo/plist": "0.1.3",
|
||||
"@expo/multipart-body-parser": "2.0.0",
|
||||
"@expo/osascript": "2.1.4",
|
||||
"@expo/package-manager": "1.7.0",
|
||||
"@expo/pkcs12": "0.1.3",
|
||||
"@expo/plist": "0.2.0",
|
||||
"@expo/plugin-help": "5.1.23",
|
||||
"@expo/plugin-warn-if-update-available": "2.5.1",
|
||||
"@expo/prebuild-config": "7.0.9",
|
||||
"@expo/prebuild-config": "8.0.17",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "1.0.149",
|
||||
"@expo/steps": "1.0.173",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^1.26.2",
|
||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
||||
@@ -41,6 +41,7 @@
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.12.0",
|
||||
"dateformat": "4.6.3",
|
||||
"diff": "7.0.0",
|
||||
"dotenv": "16.3.1",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.11.0",
|
||||
@@ -64,9 +65,10 @@
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "5.1.2",
|
||||
"minizlib": "3.0.1",
|
||||
"nanoid": "3.3.4",
|
||||
"nanoid": "3.3.8",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
"node-stream-zip": "1.15.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
@@ -77,6 +79,7 @@
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.5.4",
|
||||
"set-interval-async": "3.0.3",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.2.1",
|
||||
"tar-stream": "3.1.7",
|
||||
@@ -94,8 +97,10 @@
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@types/cli-progress": "3.11.5",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/diff": "6.0.0",
|
||||
"@types/envinfo": "7.8.3",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
@@ -177,16 +182,22 @@
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"env": {
|
||||
"description": "manage project and account environment variables"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
"webhook": {
|
||||
"description": "manage webhooks"
|
||||
},
|
||||
"workflow": {
|
||||
"description": "manage workflows"
|
||||
}
|
||||
},
|
||||
"warn-if-update-available": {
|
||||
"timeoutInDays": 0.5,
|
||||
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
|
||||
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run:') %>\n<%= chalk.dim(chalk.bold('npm install -g eas-cli')) %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
|
||||
},
|
||||
"update": {
|
||||
"node": {
|
||||
|
||||
@@ -6,6 +6,7 @@ export class ApiV2Error extends Error {
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
readonly expoApiV2ErrorRequestId?: string;
|
||||
|
||||
constructor(response: {
|
||||
message: string;
|
||||
@@ -13,11 +14,13 @@ export class ApiV2Error extends Error {
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
requestId?: string;
|
||||
}) {
|
||||
super(response.message);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
this.expoApiV2ErrorRequestId = response.requestId;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ describe(ApiV2Client, () => {
|
||||
stack: 'line 1: hello',
|
||||
details: { who: 'world' },
|
||||
metadata: { an: 'object' },
|
||||
requestId: 'request-id',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -37,6 +38,7 @@ describe(ApiV2Client, () => {
|
||||
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
|
||||
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
|
||||
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
|
||||
expect(error.expoApiV2ErrorRequestId).toEqual('request-id');
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { BranchNotFoundError } from './utils';
|
||||
@@ -8,10 +9,11 @@ import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
UpdateBranchBasicInfoFragment,
|
||||
UpdateBranchFragment,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import { UpdateBranchBasicInfoFragmentNode } from '../graphql/types/UpdateBranchBasicInfo';
|
||||
import Log from '../log';
|
||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
@@ -126,7 +128,7 @@ function renderPageOfBranches(
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
): Promise<UpdateBranchBasicInfoFragment> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
@@ -135,15 +137,17 @@ export async function createUpdateBranchOnAppAsync(
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
name
|
||||
...UpdateBranchBasicInfoFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(UpdateBranchBasicInfoFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -163,22 +167,21 @@ export async function ensureBranchExistsAsync(
|
||||
appId: string;
|
||||
branchName: string;
|
||||
}
|
||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
||||
): Promise<{ branch: UpdateBranchBasicInfoFragment; createdBranch: boolean }> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
const { id } = updateBranch;
|
||||
return { branchId: id, createdBranch: false };
|
||||
return { branch: updateBranch, createdBranch: false };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
||||
return { branch: newUpdateBranch, createdBranch: true };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
|
||||
return (
|
||||
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
|
||||
);
|
||||
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string | null> {
|
||||
return await vcsClient.getBranchNameAsync();
|
||||
}
|
||||
|
||||
export class BranchNotFoundError extends Error {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe(ensureProjectConfiguredAsync, () => {
|
||||
}),
|
||||
});
|
||||
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
|
||||
const vcsClientMock = jest.mocked(new GitClient());
|
||||
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
|
||||
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
|
||||
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
|
||||
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
|
||||
@@ -60,7 +60,7 @@ describe(ensureProjectConfiguredAsync, () => {
|
||||
});
|
||||
});
|
||||
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy();
|
||||
const vcsClientMock = jest.mocked(new GitClient());
|
||||
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
|
||||
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
|
||||
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
|
||||
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json/build/build/types';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||
import { resolveVcsClient } from '../../../vcs';
|
||||
import { Client } from '../../../vcs/vcs';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
maybeResolveVersionsAsync,
|
||||
resolveRemoteVersionCodeAsync,
|
||||
} from '../version';
|
||||
|
||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
|
||||
jest.mock('../../../vcs/vcs');
|
||||
jest.mock('../../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../../graphql/mutations/AppVersionMutation');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn().mockImplementation(() => ({
|
||||
succeed: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
const vcsClient = resolveVcsClient();
|
||||
|
||||
@@ -216,12 +236,160 @@ describe(maybeResolveVersionsAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveRemoteVersionCodeAsync, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('uses current remote versionCode when remote version set, autoIncrement=false', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
|
||||
buildVersion: '11',
|
||||
storeVersion: '1.2.3',
|
||||
});
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject();
|
||||
|
||||
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationId: 'fakeApplicationId',
|
||||
buildProfile: {} as AndroidBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('11');
|
||||
expect(createAppVersionAsyncSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initializes versionCode from local files when remote version not set, autoIncrement=false', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ versionCode: 22, versionName: '2.3.4' });
|
||||
|
||||
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationId: 'fakeApplicationId',
|
||||
buildProfile: {} as AndroidBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('22');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('22');
|
||||
});
|
||||
|
||||
it('initializes versionCode starting with 1 when remote version not set and not set in local files, autoIncrement=false', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ versionCode: null, versionName: null });
|
||||
|
||||
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationId: 'fakeApplicationId',
|
||||
buildProfile: {} as AndroidBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('1');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
|
||||
});
|
||||
|
||||
it('increments current remote versionCode when remote version set, autoIncrement=true', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
|
||||
buildVersion: '11',
|
||||
storeVersion: '1.2.3',
|
||||
});
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject();
|
||||
|
||||
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationId: 'fakeApplicationId',
|
||||
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('12');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('3.0.0');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('12');
|
||||
});
|
||||
|
||||
it('increments versionCode from local files when remote version not set, autoIncrement=true', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ versionCode: 22, versionName: '2.3.4' });
|
||||
|
||||
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationId: 'fakeApplicationId',
|
||||
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('23');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('23');
|
||||
});
|
||||
|
||||
it('initializes versionCode starting with 1 when remote version not set and not set in local files, autoIncrement=true', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ versionCode: null, versionName: null });
|
||||
|
||||
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationId: 'fakeApplicationId',
|
||||
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('1');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
versionCode = 123,
|
||||
versionName = '3.0.0',
|
||||
}: {
|
||||
versionCode?: number;
|
||||
versionName?: string;
|
||||
versionCode?: number | null;
|
||||
versionName?: string | null;
|
||||
} = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
@@ -239,8 +407,8 @@ function initBareWorkflowProject({
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode ${versionCode}
|
||||
versionName "${versionName}"
|
||||
${versionCode ? `versionCode "${versionCode}"` : ''}
|
||||
${versionName ? `versionName "${versionName}"` : ''}
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
|
||||
@@ -151,6 +151,7 @@ async function ensureAndroidCredentialsAsync(
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
name: ctx.buildProfile.keystoreName,
|
||||
app: {
|
||||
account: nullthrows(
|
||||
ctx.user.accounts.find(a => a.name === ctx.accountName),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
@@ -29,6 +30,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: buildProfileEnvironmentToEnvironment(job.environment),
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -73,6 +73,7 @@ export async function prepareJobAsync(
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
corepack: buildProfile.corepack,
|
||||
pnpm: buildProfile.pnpm,
|
||||
bun: buildProfile.bun,
|
||||
yarn: buildProfile.yarn,
|
||||
@@ -92,6 +93,7 @@ export async function prepareJobAsync(
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
environment: ctx.buildProfile.environment,
|
||||
buildType,
|
||||
username,
|
||||
...(ctx.android.versionCodeOverride && {
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function maybeResolveVersionsAsync(
|
||||
exp: ExpoConfig,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>,
|
||||
vcsClient: Client
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string; isVersionInitialized?: boolean }> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
@@ -113,6 +113,11 @@ export async function maybeResolveVersionsAsync(
|
||||
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
isVersionInitialized: !resolveConfigValue(
|
||||
buildGradle,
|
||||
'versionCode',
|
||||
parsedGradleCommand?.flavor
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
@@ -204,14 +209,18 @@ export async function resolveRemoteVersionCodeAsync(
|
||||
|
||||
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
|
||||
let currentBuildVersion: string;
|
||||
let shouldInitializeVersionCode = false;
|
||||
if (remoteVersions?.buildVersion) {
|
||||
currentBuildVersion = remoteVersions.buildVersion;
|
||||
} else {
|
||||
if (localVersions.appBuildVersion) {
|
||||
Log.warn(
|
||||
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
|
||||
)
|
||||
);
|
||||
currentBuildVersion = localVersions.appBuildVersion;
|
||||
shouldInitializeVersionCode = localVersions.isVersionInitialized ?? false;
|
||||
} else {
|
||||
Log.error(
|
||||
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
|
||||
@@ -221,7 +230,10 @@ export async function resolveRemoteVersionCodeAsync(
|
||||
}
|
||||
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
|
||||
return currentBuildVersion;
|
||||
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
|
||||
} else if (
|
||||
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
|
||||
shouldInitializeVersionCode
|
||||
) {
|
||||
const spinner = ora(
|
||||
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
|
||||
).start();
|
||||
|
||||
@@ -35,8 +35,10 @@ import { collectMetadataAsync } from './metadata';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import {
|
||||
LocalFile,
|
||||
assertProjectTarballSizeDoesNotExceedLimit,
|
||||
makeProjectMetadataFileAsync,
|
||||
makeProjectTarballAsync,
|
||||
maybeWarnAboutProjectTarballSize,
|
||||
reviewAndCommitChangesAsync,
|
||||
} from './utils/repository';
|
||||
import { BuildEvent } from '../analytics/AnalyticsManager';
|
||||
@@ -44,6 +46,7 @@ import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasCommandError } from '../commandUtils/errors';
|
||||
import { createFingerprintAsync } from '../fingerprint/cli';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
@@ -66,7 +69,6 @@ import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAs
|
||||
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createFingerprintAsync } from '../utils/fingerprintCli';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
@@ -275,19 +277,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
);
|
||||
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
|
||||
|
||||
if (projectTarball.size > 1024 * 1024 * 100) {
|
||||
Log.warn(
|
||||
`Your project archive is ${formatBytes(
|
||||
projectTarball.size
|
||||
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
|
||||
'.easignore'
|
||||
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
|
||||
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
|
||||
}
|
||||
maybeWarnAboutProjectTarballSize(projectTarball.size);
|
||||
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size);
|
||||
|
||||
projectTarballPath = projectTarball.path;
|
||||
const [bucketKey, { metadataLocation }] = await Promise.all([
|
||||
@@ -671,7 +662,7 @@ async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends
|
||||
}> {
|
||||
const runtimeAndFingerprintMetadata =
|
||||
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
|
||||
if (!runtimeAndFingerprintMetadata?.fingerprint) {
|
||||
if (!runtimeAndFingerprintMetadata?.fingerprintHash) {
|
||||
const fingerprint = await computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync(ctx);
|
||||
return {
|
||||
...runtimeAndFingerprintMetadata,
|
||||
@@ -690,10 +681,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
|
||||
): Promise<{
|
||||
runtimeVersion?: string;
|
||||
fingerprintSource?: FingerprintSource;
|
||||
fingerprint?: {
|
||||
fingerprintSources: object[];
|
||||
isDebugFingerprintSource: boolean;
|
||||
};
|
||||
fingerprintHash?: string;
|
||||
}> {
|
||||
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
|
||||
exp: ctx.exp,
|
||||
@@ -712,7 +700,10 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
|
||||
* It's ok for fingerprintSources or runtimeVersion to be empty
|
||||
* fingerprintSources only exist if the project is using runtimeVersion.policy: fingerprint
|
||||
*/
|
||||
if (!resolvedRuntimeVersion.fingerprint || !resolvedRuntimeVersion.runtimeVersion) {
|
||||
if (
|
||||
!resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint ||
|
||||
!resolvedRuntimeVersion.runtimeVersion
|
||||
) {
|
||||
return {
|
||||
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
|
||||
};
|
||||
@@ -720,14 +711,14 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
|
||||
|
||||
const uploadedFingerprint = await maybeUploadFingerprintAsync({
|
||||
hash: resolvedRuntimeVersion.runtimeVersion,
|
||||
fingerprint: resolvedRuntimeVersion.fingerprint,
|
||||
fingerprint: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint,
|
||||
graphqlClient: ctx.graphqlClient,
|
||||
localBuildMode: ctx.localBuildOptions.localBuildMode,
|
||||
});
|
||||
return {
|
||||
runtimeVersion: uploadedFingerprint.hash,
|
||||
fingerprintSource: uploadedFingerprint.fingerprintSource,
|
||||
fingerprint: resolvedRuntimeVersion.fingerprint,
|
||||
fingerprintHash: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprintHash ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -740,7 +731,7 @@ async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends
|
||||
}> {
|
||||
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
|
||||
workflow: ctx.workflow,
|
||||
platform: ctx.platform,
|
||||
platforms: [ctx.platform],
|
||||
env: ctx.env,
|
||||
});
|
||||
if (!fingerprint) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
|
||||
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
@@ -52,6 +53,78 @@ async function configureAsync({
|
||||
}
|
||||
}
|
||||
|
||||
export async function doesBuildProfileExistAsync({
|
||||
projectDir,
|
||||
profileName,
|
||||
}: {
|
||||
projectDir: string;
|
||||
profileName: string;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const easJson = await easJsonAccessor.readRawJsonAsync();
|
||||
if (!easJson.build?.[profileName]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
Log.error(`We were unable to read ${chalk.bold('eas.json')} contents. Error: ${error}.`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBuildProfileAsync({
|
||||
projectDir,
|
||||
profileName,
|
||||
profileContents,
|
||||
vcsClient,
|
||||
nonInteractive,
|
||||
}: {
|
||||
projectDir: string;
|
||||
profileName: string;
|
||||
profileContents: Record<string, any>;
|
||||
vcsClient: Client;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<void> {
|
||||
const spinner = ora(`Adding "${profileName}" build profile to ${chalk.bold('eas.json')}`).start();
|
||||
try {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
return {
|
||||
...easJsonRawObject,
|
||||
build: {
|
||||
...easJsonRawObject.build,
|
||||
[profileName]: profileContents,
|
||||
},
|
||||
};
|
||||
});
|
||||
await easJsonAccessor.writeAsync();
|
||||
spinner.succeed(
|
||||
`Successfully added "${profileName}" build profile to ${chalk.bold('eas.json')}.`
|
||||
);
|
||||
|
||||
if (await vcsClient.isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync(
|
||||
vcsClient,
|
||||
`Add "${profileName}" build profile to eas.json`,
|
||||
{
|
||||
nonInteractive,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail(
|
||||
`We were not able to configure "${profileName}" build profile inside of ${chalk.bold(
|
||||
'eas.json'
|
||||
)}. Error: ${error}.`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const EAS_JSON_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { isEnvironment } from './utils/environment';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
|
||||
function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
|
||||
return Object.values(EnvironmentVariableEnvironment).includes(
|
||||
env as EnvironmentVariableEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
@@ -54,9 +49,10 @@ async function resolveEnvVarsAsync({
|
||||
projectId: string;
|
||||
}): Promise<Env> {
|
||||
const environment =
|
||||
buildProfile.environment?.toUpperCase() ?? process.env.EAS_CURRENT_ENVIRONMENT;
|
||||
buildProfile.environment?.toUpperCase() ??
|
||||
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
|
||||
|
||||
if (!environment || !isEnvironment(environment)) {
|
||||
if (!isEnvironment(environment)) {
|
||||
Log.log(
|
||||
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
|
||||
buildProfile.env && Object.keys(buildProfile.env).length > 0
|
||||
@@ -83,13 +79,13 @@ async function resolveEnvVarsAsync({
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
|
||||
serverEnvVars
|
||||
).join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +107,7 @@ async function resolveEnvVarsAsync({
|
||||
);
|
||||
if (overlappingKeys.length > 0) {
|
||||
Log.warn(
|
||||
`The following environment variables are defined in both the "${buildProfileName}" build profile "env" configuration and the "${environment.toLowerCase()}" environment on EAS servers: ${overlappingKeys.join(
|
||||
`The following environment variables are defined in both the "${buildProfileName}" build profile "env" configuration and the "${environment.toLowerCase()}" environment on EAS: ${overlappingKeys.join(
|
||||
', '
|
||||
)}. The values from the build profile configuration will be used.`
|
||||
);
|
||||
@@ -130,3 +126,22 @@ async function resolveEnvVarsAsync({
|
||||
return { ...buildProfile.env };
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
|
||||
buildProfile: BuildProfile
|
||||
): EnvironmentVariableEnvironment {
|
||||
const environment =
|
||||
buildProfile.distribution === 'store'
|
||||
? EnvironmentVariableEnvironment.Production
|
||||
: buildProfile.developmentClient
|
||||
? EnvironmentVariableEnvironment.Development
|
||||
: EnvironmentVariableEnvironment.Preview;
|
||||
|
||||
Log.log(
|
||||
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
|
||||
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
|
||||
)}`
|
||||
);
|
||||
|
||||
return environment;
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { IosBuildProfile } from '@expo/eas-json/build/build/types';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { learnMore } from '../../../log';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { Target } from '../../../credentials/ios/types';
|
||||
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import { resolveVcsClient } from '../../../vcs';
|
||||
import { Client } from '../../../vcs/vcs';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
@@ -16,9 +22,23 @@ import {
|
||||
getInfoPlistPath,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
resolveRemoteBuildNumberAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
|
||||
jest.mock('../../../vcs/vcs');
|
||||
jest.mock('../../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../../graphql/mutations/AppVersionMutation');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn().mockImplementation(() => ({
|
||||
succeed: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
const vcsClient = resolveVcsClient();
|
||||
|
||||
@@ -301,27 +321,6 @@ describe(readShortVersionAsync, () => {
|
||||
);
|
||||
expect(buildNumber).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('fails when build number is invalid', async () => {
|
||||
const exp = initBareWorkflowProject({
|
||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
|
||||
await expect(
|
||||
readShortVersionAsync(
|
||||
'/app',
|
||||
exp,
|
||||
{
|
||||
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
|
||||
},
|
||||
vcsClient
|
||||
)
|
||||
).rejects.toThrowError(
|
||||
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: 0.0.7.1.028. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
|
||||
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
@@ -381,6 +380,154 @@ describe(getInfoPlistPath, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveRemoteBuildNumberAsync, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('uses current remote buildNumber when remote version set, autoIncrement=false', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
|
||||
buildVersion: '11',
|
||||
storeVersion: '1.2.3',
|
||||
});
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject();
|
||||
|
||||
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationTarget: {} as Target,
|
||||
buildProfile: {} as IosBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('11');
|
||||
expect(createAppVersionAsyncSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initializes buildNumber from local files when remote version not set, autoIncrement=false', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ buildNumber: '22', appVersion: '2.3.4' });
|
||||
|
||||
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationTarget: {} as Target,
|
||||
buildProfile: {} as IosBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('22');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('22');
|
||||
});
|
||||
|
||||
it('initializes buildNumber starting with 1 when remote version not set and local version is 1, autoIncrement=false', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ buildNumber: '1', appVersion: '1.0.0' });
|
||||
|
||||
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationTarget: {} as Target,
|
||||
buildProfile: {} as IosBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('1');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
|
||||
});
|
||||
|
||||
it('increments current remote buildNumber when remote version set, autoIncrement=true', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
|
||||
buildVersion: '11',
|
||||
storeVersion: '1.2.3',
|
||||
});
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject();
|
||||
|
||||
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationTarget: {} as Target,
|
||||
buildProfile: { autoIncrement: true } as IosBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('12');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('12');
|
||||
});
|
||||
|
||||
it('increments buildNumber from local files when remote version not set, autoIncrement=true', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ buildNumber: '22', appVersion: '2.3.4' });
|
||||
|
||||
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationTarget: {} as Target,
|
||||
buildProfile: { autoIncrement: true } as IosBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('23');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('23');
|
||||
});
|
||||
|
||||
it('initializes buildNumber starting with 1 when remote version not set and local version is 1, autoIncrement=true', async () => {
|
||||
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
|
||||
const vcsClientMock = instance(mock<Client>());
|
||||
vcsClientMock.getRootPathAsync = async () => '/app';
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
|
||||
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
|
||||
const exp = initBareWorkflowProject({ buildNumber: '1', appVersion: '1.0.0' });
|
||||
|
||||
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
|
||||
projectDir: '/app',
|
||||
projectId: 'fakeProjectId',
|
||||
exp,
|
||||
applicationTarget: {} as Target,
|
||||
buildProfile: { autoIncrement: true } as IosBuildProfile,
|
||||
vcsClient: vcsClientMock,
|
||||
});
|
||||
|
||||
expect(result).toBe('1');
|
||||
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
|
||||
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
appVersion = '1.0.0',
|
||||
buildNumber = '1',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
@@ -28,6 +29,7 @@ export async function createIosContextAsync(
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
|
||||
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
@@ -31,6 +32,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: buildProfileEnvironmentToEnvironment(job.environment),
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function prepareJobAsync(
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
corepack: buildProfile.corepack,
|
||||
pnpm: buildProfile.pnpm,
|
||||
bun: buildProfile.bun,
|
||||
yarn: buildProfile.yarn,
|
||||
@@ -88,6 +89,7 @@ export async function prepareJobAsync(
|
||||
buildConfiguration: buildProfile.buildConfiguration,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
environment: ctx.buildProfile.environment,
|
||||
username,
|
||||
...(ctx.ios.buildNumberOverride && {
|
||||
version: {
|
||||
|
||||
@@ -109,14 +109,14 @@ function validateShortVersion({
|
||||
}): void {
|
||||
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
|
||||
if (workflow === Workflow.MANAGED) {
|
||||
throw new Error(
|
||||
`The required format for "version" field from app.json/app.config.ts is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the required format. ${learnMore(
|
||||
Log.warn(
|
||||
`The required format for "version" field from app.json/app.config.ts for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the format required by Apple. ${learnMore(
|
||||
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
|
||||
Log.warn(
|
||||
`The required format for "CFBundleShortVersionString" in Info.plist for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the format required by Apple. ${learnMore(
|
||||
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
|
||||
)}`
|
||||
);
|
||||
@@ -330,14 +330,18 @@ export async function resolveRemoteBuildNumberAsync(
|
||||
vcsClient
|
||||
);
|
||||
let currentBuildVersion: string;
|
||||
let shouldInitializeBuildNumber = false;
|
||||
if (remoteVersions?.buildVersion) {
|
||||
currentBuildVersion = remoteVersions.buildVersion;
|
||||
} else {
|
||||
if (localBuildNumber) {
|
||||
Log.warn(
|
||||
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
|
||||
)
|
||||
);
|
||||
currentBuildVersion = localBuildNumber;
|
||||
shouldInitializeBuildNumber = localBuildNumber === '1';
|
||||
} else {
|
||||
Log.error(
|
||||
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
|
||||
@@ -347,7 +351,10 @@ export async function resolveRemoteBuildNumberAsync(
|
||||
}
|
||||
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
|
||||
return currentBuildVersion;
|
||||
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
|
||||
} else if (
|
||||
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
|
||||
shouldInitializeBuildNumber
|
||||
) {
|
||||
const spinner = ora(
|
||||
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
|
||||
).start();
|
||||
|
||||
@@ -7,7 +7,7 @@ import Log from '../log';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '1.0.144';
|
||||
const PLUGIN_PACKAGE_VERSION = '1.0.171';
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,6 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
requiredPackageManager: ctx.requiredPackageManager ?? undefined,
|
||||
selectedImage: ctx.buildProfile.image,
|
||||
customNodeVersion: ctx.buildProfile.node,
|
||||
environment: ctx.buildProfile.environment,
|
||||
simulator: 'simulator' in ctx.buildProfile && ctx.buildProfile.simulator,
|
||||
};
|
||||
return sanitizeMetadata(metadata);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { LoggerLevel } from '@expo/logger';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
@@ -62,7 +63,7 @@ import {
|
||||
validateBuildProfileVersionSettingsAsync,
|
||||
} from '../project/remoteVersionSource';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { runAsync } from '../run/run';
|
||||
import { getEasBuildRunCachedAppPath, runAsync } from '../run/run';
|
||||
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
@@ -99,16 +100,29 @@ export interface BuildFlags {
|
||||
repack: boolean;
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
vcsClient: Client,
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
|
||||
): Promise<{
|
||||
export async function runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectDir,
|
||||
flags,
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
downloadSimBuildAutoConfirm,
|
||||
envOverride,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
vcsClient: Client;
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
actor: Actor;
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
downloadSimBuildAutoConfirm?: boolean;
|
||||
envOverride?: Env;
|
||||
}): Promise<{
|
||||
buildIds: string[];
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}> {
|
||||
await vcsClient.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
|
||||
@@ -192,13 +206,15 @@ export async function runBuildAndSubmitAsync(
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const platform = toAppPlatform(buildProfile.platform);
|
||||
|
||||
const { env } = await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
});
|
||||
const { env } = !envOverride
|
||||
? await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
})
|
||||
: { env: envOverride };
|
||||
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
@@ -308,7 +324,7 @@ export async function runBuildAndSubmitAsync(
|
||||
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
|
||||
);
|
||||
|
||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
|
||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm);
|
||||
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
if (flags.json) {
|
||||
@@ -333,6 +349,7 @@ export async function runBuildAndSubmitAsync(
|
||||
}
|
||||
return {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
buildProfiles,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -531,18 +548,29 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
||||
export async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
||||
assert(build.artifacts?.applicationArchiveUrl);
|
||||
const cachedAppPath = getEasBuildRunCachedAppPath(build.project.id, build.id, build.platform);
|
||||
|
||||
if (await pathExists(cachedAppPath)) {
|
||||
Log.newLine();
|
||||
Log.log(`Using cached app...`);
|
||||
await runAsync(cachedAppPath, build.platform);
|
||||
return;
|
||||
}
|
||||
|
||||
const buildPath = await downloadAndMaybeExtractAppAsync(
|
||||
build.artifacts.applicationArchiveUrl,
|
||||
build.platform
|
||||
build.platform,
|
||||
cachedAppPath
|
||||
);
|
||||
await runAsync(buildPath, build.platform);
|
||||
}
|
||||
|
||||
async function maybeDownloadAndRunSimulatorBuildsAsync(
|
||||
builds: MaybeBuildFragment[],
|
||||
flags: BuildFlags
|
||||
flags: BuildFlags,
|
||||
autoConfirm?: boolean
|
||||
): Promise<void> {
|
||||
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
|
||||
|
||||
@@ -550,11 +578,15 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
|
||||
for (const simBuild of simBuilds) {
|
||||
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
|
||||
Log.newLine();
|
||||
const confirm = await confirmAsync({
|
||||
message: `Install and run the ${
|
||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
||||
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
|
||||
});
|
||||
const confirm =
|
||||
autoConfirm ??
|
||||
(await confirmAsync({
|
||||
message: `Install and run the ${
|
||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
||||
} build on ${
|
||||
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'
|
||||
}?`,
|
||||
}));
|
||||
if (confirm) {
|
||||
await downloadAndRunAsync(simBuild);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
|
||||
type Environment = NonNullable<BuildProfile['environment']>;
|
||||
|
||||
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
|
||||
production: EnvironmentVariableEnvironment.Production,
|
||||
preview: EnvironmentVariableEnvironment.Preview,
|
||||
development: EnvironmentVariableEnvironment.Development,
|
||||
};
|
||||
|
||||
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
|
||||
return Object.values(EnvironmentVariableEnvironment).includes(
|
||||
env as EnvironmentVariableEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
export function buildProfileEnvironmentToEnvironment(
|
||||
environment: BuildProfile['environment']
|
||||
): EnvironmentVariableEnvironment | null {
|
||||
if (!environment) {
|
||||
return null;
|
||||
}
|
||||
return BuildProfileEnvironmentToEnvironment[environment];
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import path from 'path';
|
||||
import tar from 'tar';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Log from '../../log';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
@@ -169,6 +169,34 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
export function maybeWarnAboutProjectTarballSize(size: number): void {
|
||||
if (size <= 150 /* MiB */ * 1024 /* KiB */ * 1024 /* B */) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
`Your project archive is ${formatBytes(
|
||||
size
|
||||
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
|
||||
'.easignore'
|
||||
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_ALLOWED_PROJECT_TARBALL_SIZE =
|
||||
2 /* GiB */ * 1024 /* MiB */ * 1024 /* KiB */ * 1024; /* B */
|
||||
export function assertProjectTarballSizeDoesNotExceedLimit(size: number): void {
|
||||
if (size <= MAX_ALLOWED_PROJECT_TARBALL_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Project archive is too big. Maximum allowed size is ${formatBytes(
|
||||
MAX_ALLOWED_PROJECT_TARBALL_SIZE
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
|
||||
@@ -63,3 +63,21 @@ export function getWorkflowRunUrl(
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/github`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getHostingDeploymentsUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/hosting/deployments`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -27,24 +27,28 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const googleServicesEnvVar =
|
||||
ctx.platform === Platform.ANDROID
|
||||
? ctx.env.GOOGLE_SERVICES_JSON
|
||||
: ctx.env.GOOGLE_SERVICES_INFO_PLIST;
|
||||
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
(await ctx.vcsClient.isFileIgnoredAsync(
|
||||
path.relative(rootDir, absGoogleServicesFilePath)
|
||||
))) &&
|
||||
!googleServicesEnvVar
|
||||
) {
|
||||
Log.warn(
|
||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||
);
|
||||
Log.warn(
|
||||
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
|
||||
`Use EAS file environment variables with secret or sensitive visibility to pass all values that you don't want to include in your version control to build process. ${learnMore(
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
|
||||
import formatFields from '../utils/formatFields';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
@@ -289,7 +289,7 @@ export async function doesChannelExistAsync(
|
||||
{ appId, channelName }: { appId: string; channelName: string }
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
});
|
||||
@@ -303,7 +303,6 @@ export async function doesChannelExistAsync(
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates a channel and links it to a branch with the same name.
|
||||
*
|
||||
* @param appId the app ID, also known as the project ID
|
||||
@@ -319,7 +318,7 @@ export async function createAndLinkChannelAsync(
|
||||
}: { appId: string; channelName: string; shouldPrintJson?: boolean }
|
||||
): Promise<ChannelBasicInfo> {
|
||||
let branchId: string;
|
||||
let branchMessage: string;
|
||||
let hasCreatedBranch = false;
|
||||
|
||||
try {
|
||||
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
@@ -327,7 +326,6 @@ export async function createAndLinkChannelAsync(
|
||||
name: channelName,
|
||||
});
|
||||
branchId = branch.id;
|
||||
branchMessage = `We found a branch with the same name`;
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
@@ -335,7 +333,7 @@ export async function createAndLinkChannelAsync(
|
||||
name: channelName,
|
||||
});
|
||||
branchId = newBranch.id;
|
||||
branchMessage = `We also went ahead and made a branch with the same name`;
|
||||
hasCreatedBranch = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -358,26 +356,25 @@ export async function createAndLinkChannelAsync(
|
||||
if (shouldPrintJson) {
|
||||
printJsonOnlyOutput(newChannel);
|
||||
} else {
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, appId);
|
||||
const learnMoreLink = learnMore('https://docs.expo.dev/eas-update/eas-cli/');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.withTick(
|
||||
`Created a new channel on project ${chalk.bold(
|
||||
await getDisplayNameForProjectIdAsync(graphqlClient, appId)
|
||||
)}`
|
||||
);
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: newChannel.name },
|
||||
{ label: 'ID', value: newChannel.id },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: newChannel.name },
|
||||
{ label: 'ID', value: branchId },
|
||||
])
|
||||
);
|
||||
if (hasCreatedBranch) {
|
||||
Log.withTick(
|
||||
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} and branch ${chalk.bold(
|
||||
`"${newChannel.name}"`
|
||||
)} on ${chalk.bold(projectDisplayName)} project. ${learnMoreLink}`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} on ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} project and connected it with existing ${chalk.bold(
|
||||
`"${newChannel.name}"`
|
||||
)} branch. ${learnMoreLink}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return newChannel;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
import Log from '../log';
|
||||
import Log, { link } from '../log';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
@@ -264,6 +264,16 @@ export default abstract class EasCommand extends Command {
|
||||
const defaultMsg = `${messageLine}${requestIdLine}`;
|
||||
|
||||
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
|
||||
if (defaultMsg.includes('ScopedAccountActorViewerContext') && process.env.EAS_BUILD) {
|
||||
// We're in EAS, authenticated with a scoped account actor access token.
|
||||
// We may have not added the scoped actor privacy rule to the right place yet.
|
||||
return `${chalk.bold(
|
||||
`You don't have the required permissions to perform this operation.`
|
||||
)}\n\nWe are in the process of migrating EAS to a more granular permissioning system. If you believe what you're doing is a legitimate operation you should be able to perform, report this to us at ${link(
|
||||
'https://expo.dev/contact'
|
||||
)}\n\nOriginal error message: ${defaultMsg}`;
|
||||
}
|
||||
|
||||
return `${chalk.bold(
|
||||
`You don't have the required permissions to perform this operation.`
|
||||
)}\n\nThis can sometimes happen if you are logged in as incorrect user.\nRun ${chalk.bold(
|
||||
|
||||
@@ -38,7 +38,13 @@ export async function fetchBuildsAsync({
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
filters?: { statuses?: BuildStatus[]; platform?: RequestedPlatform; profile?: string };
|
||||
filters?: {
|
||||
statuses?: BuildStatus[];
|
||||
platform?: RequestedPlatform;
|
||||
profile?: string;
|
||||
hasFingerprint?: boolean;
|
||||
fingerprintHash?: string;
|
||||
};
|
||||
}): Promise<BuildFragment[]> {
|
||||
let builds: BuildFragment[];
|
||||
const queryFilters: InputMaybe<BuildFilter> = {};
|
||||
@@ -48,6 +54,12 @@ export async function fetchBuildsAsync({
|
||||
if (filters?.profile) {
|
||||
queryFilters['buildProfile'] = filters.profile;
|
||||
}
|
||||
if (filters?.hasFingerprint) {
|
||||
queryFilters['hasFingerprint'] = filters.hasFingerprint;
|
||||
}
|
||||
if (filters?.fingerprintHash) {
|
||||
queryFilters['fingerprintHash'] = filters.fingerprintHash;
|
||||
}
|
||||
if (!filters?.statuses) {
|
||||
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import { getPublicExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
type GetServerSideEnvironmentVariablesFn = (
|
||||
export type GetServerSideEnvironmentVariablesFn = (
|
||||
maybeEnv?: Record<string, string>
|
||||
) => Promise<Record<string, string>>;
|
||||
|
||||
|
||||
+174
-79
@@ -78,7 +78,12 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
it('gets the project ID from app config if exists', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
exp: {
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
@@ -91,7 +96,7 @@ describe(getProjectIdAsync, () => {
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ sdkVersion: '52.0.0', name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).resolves.toEqual('1234');
|
||||
@@ -99,7 +104,13 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
it('throws when the owner is out of sync', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
exp: {
|
||||
sdkVersion: '54.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
owner: 'wat',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
@@ -112,7 +123,13 @@ describe(getProjectIdAsync, () => {
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
{
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
owner: 'wat',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
@@ -122,83 +139,143 @@ describe(getProjectIdAsync, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the owner is not specified and is different than logged in user', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
describe('when sdkVersion is less than 53', () => {
|
||||
it('throws when the owner is not specified and is different than logged in user', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: {
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'Robot',
|
||||
id: 'robot_id',
|
||||
accounts: [
|
||||
{
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
|
||||
},
|
||||
{
|
||||
id: 'account_id_2',
|
||||
name: 'dominik',
|
||||
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
},
|
||||
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
|
||||
});
|
||||
const sessionManagerRobot = instance(sessionManagerMock);
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: {
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManagerRobot,
|
||||
{
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'Robot',
|
||||
id: 'robot_id',
|
||||
accounts: [
|
||||
{
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
|
||||
},
|
||||
{
|
||||
id: 'account_id_2',
|
||||
name: 'dominik',
|
||||
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
},
|
||||
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
|
||||
});
|
||||
const sessionManagerRobot = instance(sessionManagerMock);
|
||||
describe('when sdkVersion is 53 or higher', () => {
|
||||
it('does not throw when the owner is not specified', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: {
|
||||
sdkVersion: '53.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
});
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{
|
||||
sdkVersion: '53.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManagerRobot,
|
||||
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the slug is out of sync', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
exp: {
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'wat',
|
||||
extra: { eas: { projectId: '1234' } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
@@ -211,7 +288,7 @@ describe(getProjectIdAsync, () => {
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
@@ -222,10 +299,17 @@ describe(getProjectIdAsync, () => {
|
||||
});
|
||||
|
||||
it('fetches the project ID when not in app config, and sets it in the config', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest
|
||||
.mocked(getConfig)
|
||||
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'success',
|
||||
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
|
||||
config: {
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
},
|
||||
});
|
||||
jest
|
||||
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
|
||||
@@ -233,7 +317,7 @@ describe(getProjectIdAsync, () => {
|
||||
|
||||
const projectId = await getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'test' },
|
||||
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
|
||||
{
|
||||
nonInteractive: false,
|
||||
}
|
||||
@@ -252,7 +336,9 @@ describe(getProjectIdAsync, () => {
|
||||
});
|
||||
|
||||
it('throws if writing the ID back to the config fails', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest
|
||||
.mocked(getConfig)
|
||||
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'fail',
|
||||
config: null,
|
||||
@@ -262,13 +348,22 @@ describe(getProjectIdAsync, () => {
|
||||
.mockImplementation(async () => '4567');
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws if extra.eas.projectId is not a string', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
|
||||
exp: {
|
||||
sdkVersion: '52.0.0',
|
||||
name: 'test',
|
||||
slug: 'test',
|
||||
extra: { eas: { projectId: 1234 } },
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
@@ -281,7 +376,7 @@ describe(getProjectIdAsync, () => {
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
|
||||
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
|
||||
+7
-2
@@ -42,7 +42,7 @@ async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
|
||||
}
|
||||
|
||||
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
|
||||
const maybeRepoRoot = PackageManagerUtils.resolveWorkspaceRoot(projectDir) ?? projectDir;
|
||||
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
|
||||
@@ -103,6 +103,8 @@ export async function findProjectRootAsync({
|
||||
}
|
||||
}
|
||||
|
||||
let ranEnsureEasCliIsNotInDependencies = false;
|
||||
|
||||
/**
|
||||
* Determine the project root directory and ensure some constraints about the project setup
|
||||
* like CLI version and dependencies.
|
||||
@@ -113,6 +115,9 @@ export async function findProjectRootAsync({
|
||||
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await applyCliConfigAsync(projectDir);
|
||||
await ensureEasCliIsNotInDependenciesAsync(projectDir);
|
||||
if (!ranEnsureEasCliIsNotInDependencies) {
|
||||
ranEnsureEasCliIsNotInDependencies = true;
|
||||
await ensureEasCliIsNotInDependenciesAsync(projectDir);
|
||||
}
|
||||
return projectDir;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ExpoConfig, getProjectConfigDescription } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
|
||||
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
getPrivateExpoConfigAsync,
|
||||
} from '../../../project/expoConfig';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { toAppPrivacy } from '../../../project/projectUtils';
|
||||
import SessionManager from '../../../user/SessionManager';
|
||||
import { Actor, getActorUsername } from '../../../user/User';
|
||||
|
||||
@@ -128,29 +128,33 @@ export async function validateOrSetProjectIdAsync({
|
||||
);
|
||||
}
|
||||
|
||||
const actorUsername = getActorUsername(actor);
|
||||
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
|
||||
if (actorUsername) {
|
||||
throw new Error(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
|
||||
appForProjectId.ownerAccount.name
|
||||
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
// robot caller
|
||||
throw new Error(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
|
||||
appForProjectId.ownerAccount.name
|
||||
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
const sdkVersion = exp.sdkVersion;
|
||||
// SDK 53 and above no longer require owner field in app config
|
||||
if (sdkVersion && semver.satisfies(sdkVersion, '< 53.0.0')) {
|
||||
const actorUsername = getActorUsername(actor);
|
||||
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
|
||||
if (actorUsername) {
|
||||
throw new Error(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
|
||||
appForProjectId.ownerAccount.name
|
||||
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
// robot caller
|
||||
throw new Error(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
|
||||
appForProjectId.ownerAccount.name
|
||||
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +201,6 @@ export async function validateOrSetProjectIdAsync({
|
||||
{
|
||||
accountName: getAccountNameForEASProjectSync(exp, actor),
|
||||
projectName: exp.slug,
|
||||
privacy: toAppPrivacy(exp.privacy),
|
||||
},
|
||||
{
|
||||
nonInteractive: options.nonInteractive,
|
||||
|
||||
+2
-2
@@ -42,13 +42,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
|
||||
serverEnvVars
|
||||
).join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../graphql/generated';
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
|
||||
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
|
||||
// are used in tandem, it saves on unnecessary typying in commands
|
||||
@@ -52,18 +52,19 @@ export const EASVariableFormatFlag = {
|
||||
};
|
||||
|
||||
export const EASVariableVisibilityFlag = {
|
||||
visibility: Flags.enum<'plaintext' | 'sensitive' | 'encrypted'>({
|
||||
visibility: Flags.enum<'plaintext' | 'sensitive' | 'secret'>({
|
||||
description: 'Visibility of the variable',
|
||||
options: ['plaintext', 'sensitive', 'encrypted'],
|
||||
options: ['plaintext', 'sensitive', 'secret'],
|
||||
}),
|
||||
};
|
||||
|
||||
export const EASVariableScopeFlag = {
|
||||
scope: Flags.enum<EnvironmentVariableScope>({
|
||||
export type EASEnvironmentVariableScopeFlagValue = 'project' | 'account';
|
||||
|
||||
export const EASEnvironmentVariableScopeFlag = {
|
||||
scope: Flags.enum<EASEnvironmentVariableScopeFlagValue>({
|
||||
description: 'Scope for the variable',
|
||||
options: mapToLowercase([EnvironmentVariableScope.Shared, EnvironmentVariableScope.Project]),
|
||||
parse: upperCaseAsync,
|
||||
default: EnvironmentVariableScope.Project,
|
||||
options: ['project', 'account'],
|
||||
default: 'project',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -82,11 +83,10 @@ export const EasJsonOnlyFlag = {
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
|
||||
description:
|
||||
'Environment to use the server-side defined EAS environment variables for during command execution.',
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution.',
|
||||
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
|
||||
parse: upperCaseAsync,
|
||||
required: false,
|
||||
hidden: true,
|
||||
default: null,
|
||||
}),
|
||||
};
|
||||
@@ -58,7 +58,7 @@ export default class BranchCreate extends EasCommand {
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(vcsClient),
|
||||
initial: (await getDefaultBranchNameAsync(vcsClient)) ?? undefined,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
createBuildProfileAsync,
|
||||
doesBuildProfileExistAsync,
|
||||
ensureProjectConfiguredAsync,
|
||||
} from '../../build/configure';
|
||||
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
|
||||
import { downloadAndRunAsync, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { createFingerprintAsync } from '../../fingerprint/cli';
|
||||
import { BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { expoCommandAsync } from '../../utils/expoCli';
|
||||
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
|
||||
import { Client } from '../../vcs/vcs';
|
||||
|
||||
const DEFAULT_EAS_BUILD_RUN_PROFILE_NAME = 'development-simulator';
|
||||
|
||||
export default class BuildDev extends EasCommand {
|
||||
static override hidden: true;
|
||||
|
||||
static override description =
|
||||
'run dev client simulator/emulator build with matching fingerprint or create a new one';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
|
||||
char: 'p',
|
||||
options: [Platform.IOS, Platform.ANDROID],
|
||||
}),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Vcs,
|
||||
...this.ContextOptions.Analytics,
|
||||
...this.ContextOptions.ProjectId,
|
||||
};
|
||||
|
||||
protected override async runAsync(): Promise<any> {
|
||||
const { flags } = await this.parse(BuildDev);
|
||||
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectId,
|
||||
} = await this.getContextAsync(BuildDev, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const platform = await this.selectPlatformAsync(flags.platform);
|
||||
if (process.platform !== 'darwin' && platform === Platform.IOS) {
|
||||
Errors.error('Running iOS builds in simulator is only supported on macOS.', { exit: 1 });
|
||||
}
|
||||
|
||||
await vcsClient.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
|
||||
await ensureProjectConfiguredAsync({
|
||||
projectDir,
|
||||
nonInteractive: false,
|
||||
vcsClient,
|
||||
});
|
||||
|
||||
const buildProfile = await this.ensureValidBuildRunProfileExistsAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
selectedBuildProfileName: flags.profile,
|
||||
vcsClient,
|
||||
});
|
||||
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
|
||||
|
||||
const { env } = await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
});
|
||||
|
||||
const fingerprint = await createFingerprintAsync(projectDir, {
|
||||
env,
|
||||
workflow,
|
||||
platforms: [platform],
|
||||
});
|
||||
if (!fingerprint) {
|
||||
Errors.error('Failed to calculate fingerprint', { exit: 1 });
|
||||
}
|
||||
Log.log(`✨ Calculated fingerprint hash: ${fingerprint.hash}`);
|
||||
Log.newLine();
|
||||
|
||||
const builds = await this.getBuildsAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform,
|
||||
fingerprint,
|
||||
});
|
||||
if (builds.length !== 0) {
|
||||
const build = builds[0];
|
||||
Log.succeed(
|
||||
`🎯 Found successful build with matching fingerprint on EAS servers. Running it...`
|
||||
);
|
||||
|
||||
if (build.artifacts?.applicationArchiveUrl) {
|
||||
await downloadAndRunAsync(build);
|
||||
await this.startDevServerAsync({ projectDir, platform });
|
||||
return;
|
||||
} else {
|
||||
Log.warn('Artifacts for this build expired. New build will be started.');
|
||||
}
|
||||
}
|
||||
|
||||
Log.log('🚀 No successful build with matching fingerprint found. Starting a new build...');
|
||||
|
||||
const previousBuildsForSelectedProfile = await this.getBuildsAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform,
|
||||
});
|
||||
if (
|
||||
previousBuildsForSelectedProfile.length > 0 &&
|
||||
previousBuildsForSelectedProfile[0].metrics?.buildDuration
|
||||
) {
|
||||
Log.log(
|
||||
`🕒 Previous build for "${buildProfile.profileName}" profile completed in ${Math.floor(
|
||||
previousBuildsForSelectedProfile[0].metrics.buildDuration / 60000
|
||||
)} minutes.`
|
||||
);
|
||||
}
|
||||
|
||||
await runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectDir,
|
||||
flags: {
|
||||
requestedPlatform:
|
||||
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
|
||||
nonInteractive: false,
|
||||
freezeCredentials: false,
|
||||
wait: true,
|
||||
clearCache: false,
|
||||
json: false,
|
||||
autoSubmit: false,
|
||||
localBuildOptions: {},
|
||||
repack: false,
|
||||
profile: flags.profile ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
|
||||
},
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
downloadSimBuildAutoConfirm: true,
|
||||
envOverride: env,
|
||||
});
|
||||
await this.startDevServerAsync({ projectDir, platform });
|
||||
}
|
||||
|
||||
private async selectPlatformAsync(platform?: Platform): Promise<Platform> {
|
||||
if (platform) {
|
||||
return platform;
|
||||
}
|
||||
const { resolvedPlatform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Select platform',
|
||||
name: 'resolvedPlatform',
|
||||
choices: [
|
||||
{ title: 'Android', value: Platform.ANDROID },
|
||||
{ title: 'iOS', value: Platform.IOS },
|
||||
],
|
||||
});
|
||||
return resolvedPlatform;
|
||||
}
|
||||
|
||||
private async validateBuildRunProfileAsync({
|
||||
platform,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
}: {
|
||||
platform: Platform;
|
||||
buildProfile: BuildProfile;
|
||||
buildProfileName: string;
|
||||
}): Promise<void> {
|
||||
if (buildProfile.developmentClient !== true) {
|
||||
Errors.error(
|
||||
`Profile "${buildProfileName}" must specify "developmentClient: true" to create a dev client build. Select a different profile or update the profile in eas.json.`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
if (buildProfile.distribution !== 'internal') {
|
||||
Errors.error(
|
||||
`Profile "${buildProfileName}" must specify "distribution: internal" in order to work with eas build:dev command. Select a different profile or update the profile in eas.json.`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
if (platform === Platform.IOS) {
|
||||
const iosProfile = buildProfile as BuildProfile<Platform.IOS>;
|
||||
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
|
||||
Errors.error(
|
||||
`Profile "${buildProfileName}" must specify "ios.simulator: true" or "withoutCredentials: true" to create an iOS simulator build. Select a different profile or update the profile in eas.json.`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const androidProfile = buildProfile as BuildProfile<Platform.ANDROID>;
|
||||
if (
|
||||
androidProfile.distribution !== 'internal' &&
|
||||
androidProfile.withoutCredentials !== true
|
||||
) {
|
||||
Errors.error(
|
||||
`Profile "${buildProfileName}" must specify "distribution: internal" or "withoutCredentials: true" to create an Android emulator build. Select a different profile or update the profile in eas.json.`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureValidBuildRunProfileExistsAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
selectedBuildProfileName,
|
||||
vcsClient,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
selectedBuildProfileName?: string;
|
||||
vcsClient: Client;
|
||||
}): Promise<ProfileData<'build'>> {
|
||||
if (
|
||||
!!selectedBuildProfileName ||
|
||||
(await doesBuildProfileExistAsync({
|
||||
projectDir,
|
||||
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
|
||||
}))
|
||||
) {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const [buildProfile] = await getProfilesAsync({
|
||||
type: 'build',
|
||||
easJsonAccessor,
|
||||
platforms: [platform],
|
||||
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
|
||||
projectDir,
|
||||
});
|
||||
await this.validateBuildRunProfileAsync({
|
||||
buildProfileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
|
||||
platform,
|
||||
buildProfile: buildProfile.profile,
|
||||
});
|
||||
} else {
|
||||
const createBuildProfile = await confirmAsync({
|
||||
message: `We want to go ahead and generate "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile for you, that matches eas build:dev criteria. Do you want to proceed?`,
|
||||
});
|
||||
if (!createBuildProfile) {
|
||||
Errors.error(
|
||||
'Come back later or specify different build compliant with eas build:dev requirements by using "--profile" flag.',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
await createBuildProfileAsync({
|
||||
projectDir,
|
||||
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
|
||||
profileContents: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
simulator: true,
|
||||
},
|
||||
environment: 'development',
|
||||
},
|
||||
nonInteractive: false,
|
||||
vcsClient,
|
||||
});
|
||||
}
|
||||
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const [buildProfile] = await getProfilesAsync({
|
||||
type: 'build',
|
||||
easJsonAccessor,
|
||||
platforms: [platform],
|
||||
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
|
||||
projectDir,
|
||||
});
|
||||
return buildProfile;
|
||||
}
|
||||
|
||||
private async getBuildsAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform,
|
||||
fingerprint,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
platform: Platform;
|
||||
fingerprint?: { hash: string };
|
||||
}): Promise<BuildFragment[]> {
|
||||
return await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
filter: {
|
||||
platform: toAppPlatform(platform),
|
||||
fingerprintHash: fingerprint?.hash,
|
||||
status: BuildStatus.Finished,
|
||||
simulator: platform === Platform.IOS ? true : undefined,
|
||||
distribution: platform === Platform.ANDROID ? DistributionType.Internal : undefined,
|
||||
developmentClient: true,
|
||||
},
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private async startDevServerAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
}): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`Starting development server: ${chalk.dim(
|
||||
`npx expo start --dev-client ${platform === Platform.IOS ? '--ios' : '--android'}`
|
||||
)}`
|
||||
);
|
||||
await expoCommandAsync(projectDir, [
|
||||
'start',
|
||||
'--dev-client',
|
||||
platform === Platform.IOS ? '--ios' : '--android',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
|
||||
import { BuildProfile, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
|
||||
import { LoggerLevel } from '@expo/logger';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
@@ -17,6 +17,7 @@ import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform'
|
||||
import { selectAsync } from '../../prompts';
|
||||
import uniq from '../../utils/expodash/uniq';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { ProfileData } from '../../utils/profiles';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
|
||||
interface RawBuildFlags {
|
||||
@@ -78,10 +79,12 @@ export default class Build extends EasCommand {
|
||||
description: 'Clear cache before the build',
|
||||
}),
|
||||
'auto-submit': Flags.boolean({
|
||||
char: 's',
|
||||
default: false,
|
||||
description:
|
||||
'Submit on build complete using the submit profile with the same name as the build profile',
|
||||
exclusive: ['auto-submit-with-profile'],
|
||||
aliases: ['submit'],
|
||||
}),
|
||||
'auto-submit-with-profile': Flags.string({
|
||||
description: 'Submit on build complete using the submit profile with provided name',
|
||||
@@ -158,15 +161,20 @@ export default class Build extends EasCommand {
|
||||
|
||||
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
const { buildProfiles } = await runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectDir,
|
||||
flagsWithPlatform,
|
||||
flags: flagsWithPlatform,
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync
|
||||
);
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
});
|
||||
|
||||
this.maybeSuggestUsingEasBuildDev({
|
||||
buildProfiles,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
private sanitizeFlags(
|
||||
@@ -266,6 +274,51 @@ export default class Build extends EasCommand {
|
||||
requestedPlatform,
|
||||
};
|
||||
}
|
||||
|
||||
private maybeSuggestUsingEasBuildDev({
|
||||
buildProfiles,
|
||||
nonInteractive,
|
||||
}: {
|
||||
buildProfiles: ProfileData<'build'>[] | undefined;
|
||||
nonInteractive: boolean;
|
||||
}): void {
|
||||
// suggest using eas build:dev if the build configuration results in simulator/emulator dev client build
|
||||
if (
|
||||
!nonInteractive &&
|
||||
!process.env.CI &&
|
||||
buildProfiles?.some(({ profile, platform }) => {
|
||||
if (profile.developmentClient !== true) {
|
||||
return false;
|
||||
}
|
||||
if (profile.distribution !== 'internal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (platform === Platform.IOS) {
|
||||
const iosProfile = profile as BuildProfile<Platform.IOS>;
|
||||
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const androidProfile = profile as BuildProfile<Platform.ANDROID>;
|
||||
if (
|
||||
androidProfile.distribution !== 'internal' &&
|
||||
androidProfile.withoutCredentials !== true
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`🔎 TIP: You are using a build configuration that could benefit from using ${chalk.bold(
|
||||
'eas build:dev'
|
||||
)} command. Run it to install and run cached development build, or create a new one if a compatible build doesn't exist yet.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeprecatedEasJsonAsync(
|
||||
|
||||
@@ -99,12 +99,12 @@ export default class BuildInspect extends EasCommand {
|
||||
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
|
||||
} else {
|
||||
try {
|
||||
await runBuildAndSubmitAsync(
|
||||
await runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectDir,
|
||||
{
|
||||
flags: {
|
||||
nonInteractive: false,
|
||||
freezeCredentials: false,
|
||||
wait: true,
|
||||
@@ -124,8 +124,8 @@ export default class BuildInspect extends EasCommand {
|
||||
repack: false,
|
||||
},
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync
|
||||
);
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
});
|
||||
if (!flags.verbose) {
|
||||
Log.log(chalk.green('Build successful'));
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
|
||||
import NoVcsClient from '../../vcs/clients/noVcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
|
||||
/**
|
||||
* This command will be run on the EAS Build workers, when building
|
||||
@@ -64,18 +63,24 @@ export default class BuildInternal extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildInternal, {
|
||||
nonInteractive: true,
|
||||
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
if (vcsClient instanceof GitClient) {
|
||||
// `build:internal` is run on EAS workers and the repo may have been changed
|
||||
// by pre-install hooks or other scripts. We don't want to require committing changes
|
||||
// to continue the build.
|
||||
vcsClient.requireCommit = false;
|
||||
}
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
await runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectDir,
|
||||
{
|
||||
flags: {
|
||||
requestedPlatform: flags.platform as RequestedPlatform,
|
||||
profile: flags.profile,
|
||||
nonInteractive: true,
|
||||
@@ -91,7 +96,7 @@ export default class BuildInternal extends EasCommand {
|
||||
repack: false,
|
||||
},
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync
|
||||
);
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@ export default class BuildList extends EasCommand {
|
||||
aliases: ['gitCommitHash'],
|
||||
description: 'Filter only builds created with the specified git commit hash',
|
||||
}),
|
||||
'fingerprint-hash': Flags.string({
|
||||
description: 'Filter only builds with the specified fingerprint hash',
|
||||
}),
|
||||
...EasPaginatedQueryFlags,
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: BUILDS_LIMIT }),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
@@ -130,6 +133,7 @@ export default class BuildList extends EasCommand {
|
||||
buildProfile: flags['build-profile'],
|
||||
gitCommitHash: flags['git-commit-hash'],
|
||||
simulator: flags.simulator,
|
||||
fingerprintHash: flags['fingerprint-hash'],
|
||||
},
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { getLatestBuildAsync, listAndSelectBuildOnAppAsync } from '../../build/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -17,13 +16,12 @@ import Log from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { RunArchiveFlags, runAsync } from '../../run/run';
|
||||
import { RunArchiveFlags, getEasBuildRunCachedAppPath, runAsync } from '../../run/run';
|
||||
import { isRunnableOnSimulatorOrEmulator } from '../../run/utils';
|
||||
import {
|
||||
downloadAndMaybeExtractAppAsync,
|
||||
extractAppFromLocalArchiveAsync,
|
||||
} from '../../utils/download';
|
||||
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
|
||||
|
||||
interface RawRunFlags {
|
||||
latest?: boolean;
|
||||
@@ -245,17 +243,6 @@ async function maybeGetBuildAsync(
|
||||
}
|
||||
}
|
||||
|
||||
function getEasBuildRunCachedAppPath(
|
||||
projectId: string,
|
||||
buildId: string,
|
||||
platform: AppPlatform
|
||||
): string {
|
||||
return path.join(
|
||||
getEasBuildRunCacheDirectoryPath(),
|
||||
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
|
||||
);
|
||||
}
|
||||
|
||||
async function getPathToSimulatorBuildAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
|
||||
@@ -120,7 +120,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, profileInfo.platform, vcsClient);
|
||||
if (!remoteVersions?.buildVersion) {
|
||||
Log.warn(
|
||||
`Skipping versions sync for ${platformDisplayName}. There are no versions configured on Expo servers, use "eas build:version:set" or run a build to initialize it.`
|
||||
`Skipping versions sync for ${platformDisplayName}. There are no versions configured on EAS, use "eas build:version:set" or run a build to initialize it.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default class ChannelDelete extends EasCommand {
|
||||
|
||||
let channelId, channelName;
|
||||
if (nameArg) {
|
||||
const { id, name } = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
const { id, name } = await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: nameArg,
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
}
|
||||
|
||||
const existingChannel = args.name
|
||||
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: args.name,
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class ChannelPause extends EasCommand {
|
||||
}
|
||||
|
||||
const existingChannel = args.name
|
||||
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: args.name,
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class ChannelResume extends EasCommand {
|
||||
}
|
||||
|
||||
const existingChannel = args.name
|
||||
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: args.name,
|
||||
})
|
||||
|
||||
+4
-7
@@ -33,12 +33,9 @@ interface RawDeployAliasFlags {
|
||||
}
|
||||
|
||||
export default class WorkerAlias extends EasCommand {
|
||||
static override description = 'Assign deployment aliases';
|
||||
static override aliases = ['deploy:alias', 'deploy:promote'];
|
||||
|
||||
// TODO(@kitten): Keep command hidden until worker deployments are live
|
||||
static override hidden = true;
|
||||
static override state = 'beta';
|
||||
static override description = 'Assign deployment aliases.';
|
||||
static override aliases = ['worker:alias', 'deploy:promote'];
|
||||
static override state = 'preview';
|
||||
|
||||
static override flags = {
|
||||
prod: Flags.boolean({
|
||||
@@ -73,7 +70,7 @@ export default class WorkerAlias extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
+71
-19
@@ -1,14 +1,17 @@
|
||||
import { format as formatTimeAgo } from '@expo/timeago.js';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { getHostingDeploymentsUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { createProgressTracker } from '../../utils/progress';
|
||||
import * as WorkerAssets from '../../worker/assets';
|
||||
@@ -43,16 +46,18 @@ interface DeployFlags {
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
deploymentIdentifier?: string;
|
||||
exportDir: string;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
interface RawDeployFlags {
|
||||
'non-interactive': boolean;
|
||||
environment?: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
json: boolean;
|
||||
prod: boolean;
|
||||
alias?: string;
|
||||
id?: string;
|
||||
'export-dir': string;
|
||||
'dry-run': boolean;
|
||||
}
|
||||
|
||||
interface DeployInProgressParams {
|
||||
@@ -63,13 +68,10 @@ interface DeployInProgressParams {
|
||||
}
|
||||
|
||||
export default class WorkerDeploy extends EasCommand {
|
||||
static override description = 'Deploy your Expo web build';
|
||||
static override aliases = ['deploy'];
|
||||
static override description = 'deploy your Expo Router web build and API Routes';
|
||||
static override aliases = ['worker:deploy'];
|
||||
static override usage = [chalk`deploy {dim [options]}`, `deploy --prod`];
|
||||
|
||||
// TODO(@kitten): Keep command hidden until worker deployments are live
|
||||
static override hidden = true;
|
||||
static override state = 'beta';
|
||||
static override state = 'preview';
|
||||
|
||||
static override flags = {
|
||||
prod: Flags.boolean({
|
||||
@@ -90,10 +92,11 @@ export default class WorkerDeploy extends EasCommand {
|
||||
helpValue: 'dir',
|
||||
default: 'dist',
|
||||
}),
|
||||
environment: {
|
||||
...EASEnvironmentFlag.environment,
|
||||
description: 'Deploy with EAS Environment Variables matching the specified environment.',
|
||||
},
|
||||
'dry-run': Flags.boolean({
|
||||
description: 'Outputs a tarball of the new deployment instead of uploading it.',
|
||||
default: false,
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
@@ -111,7 +114,7 @@ export default class WorkerDeploy extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
@@ -120,7 +123,10 @@ export default class WorkerDeploy extends EasCommand {
|
||||
} = await this.getContextAsync(WorkerDeploy, { ...flags, withServerSideEnvironment: null });
|
||||
|
||||
const projectDist = await resolveExportedProjectAsync(flags, projectDir);
|
||||
const { projectId } = await getDynamicPrivateProjectConfigAsync();
|
||||
const { projectId, exp } = await getDynamicPrivateProjectConfigAsync();
|
||||
|
||||
const projectName = exp.slug;
|
||||
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
|
||||
|
||||
logExportedProjectInfo(projectDist);
|
||||
|
||||
@@ -250,7 +256,7 @@ export default class WorkerDeploy extends EasCommand {
|
||||
let progress = ora('Preparing project').start();
|
||||
|
||||
try {
|
||||
const manifest = await WorkerAssets.createManifestAsync(
|
||||
const manifestResult = await WorkerAssets.createManifestAsync(
|
||||
{
|
||||
environment: flags.environment,
|
||||
projectDir,
|
||||
@@ -258,16 +264,33 @@ export default class WorkerDeploy extends EasCommand {
|
||||
},
|
||||
graphqlClient
|
||||
);
|
||||
if (manifestResult.conflictingVariableNames?.length) {
|
||||
Log.warn(
|
||||
'> The following environment variables were present in local .env files as well as EAS environment variables. ' +
|
||||
'In case of conflict, the EAS environment variable values will be used: ' +
|
||||
manifestResult.conflictingVariableNames.join(' ')
|
||||
);
|
||||
}
|
||||
assetMap = await WorkerAssets.createAssetMapAsync(
|
||||
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path
|
||||
);
|
||||
tarPath = await WorkerAssets.packFilesIterableAsync(
|
||||
emitWorkerTarballAsync({
|
||||
assetMap,
|
||||
manifest,
|
||||
manifest: manifestResult.manifest,
|
||||
})
|
||||
);
|
||||
|
||||
if (flags.dryRun) {
|
||||
const DRY_RUN_OUTPUT_PATH = 'deploy.tar.gz';
|
||||
await fs.promises.copyFile(tarPath, DRY_RUN_OUTPUT_PATH);
|
||||
progress.succeed('Saved deploy.tar.gz tarball');
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput({ tarPath: DRY_RUN_OUTPUT_PATH });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadUrl = await getSignedDeploymentUrlAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
deploymentIdentifier: flags.deploymentIdentifier,
|
||||
@@ -282,6 +305,32 @@ export default class WorkerDeploy extends EasCommand {
|
||||
progress.succeed('Created deployment');
|
||||
} catch (error: any) {
|
||||
progress.fail('Failed to create deployment');
|
||||
|
||||
if (
|
||||
flags.isProduction &&
|
||||
error instanceof CombinedError &&
|
||||
error.graphQLErrors.some(err => {
|
||||
return (
|
||||
err.extensions?.errorCode === 'UNAUTHORIZED_ERROR' &&
|
||||
err.message.includes('AppDevDomainNameEntity') &&
|
||||
err.message.includes('CREATE')
|
||||
);
|
||||
})
|
||||
) {
|
||||
throw new Error(
|
||||
`You have specified the new deployment should be a production deployment, but a production domain has not been set up yet for this app.\n\nRun ${chalk.bold(
|
||||
'eas deploy --prod'
|
||||
)} as an app admin on your machine or promote an existing deployment to production on the ${link(
|
||||
getHostingDeploymentsUrl(accountName, projectName),
|
||||
{
|
||||
dim: false,
|
||||
text: 'Hosting Dashboard',
|
||||
fallback: `Hosting Dashboard (${getHostingDeploymentsUrl(accountName, projectName)})`,
|
||||
}
|
||||
)} to set up a production domain and then try again.`
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -382,6 +431,8 @@ export default class WorkerDeploy extends EasCommand {
|
||||
aliasName: flags.alias?.trim().toLowerCase(),
|
||||
deploymentIdentifier: flags.id?.trim(),
|
||||
exportDir: flags['export-dir'],
|
||||
environment: flags['environment'],
|
||||
dryRun: flags['dry-run'],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -411,7 +462,7 @@ async function resolveExportedProjectAsync(
|
||||
|
||||
if (!exportDirStat?.isDirectory()) {
|
||||
throw new Error(
|
||||
`No "${flags.exportDir}/" folder found. Prepare your project for deployment with "npx expo export --platform web"`
|
||||
`No "${flags.exportDir}/" folder found. Export your app with "npx expo export --platform web"`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -436,7 +487,8 @@ function logExportedProjectInfo(
|
||||
// Only show the timestamp for exports older than 1 minute
|
||||
if (project.modifiedAt && Date.now() - project.modifiedAt.getTime() > 60_000) {
|
||||
modifiedAgo = ` - exported ${formatTimeAgo(project.modifiedAt)}`;
|
||||
Log.warn(`> Project export: ${project.type}${modifiedAgo}`);
|
||||
} else {
|
||||
Log.log(chalk`{dim > Project export: ${project.type}}`);
|
||||
}
|
||||
|
||||
Log.log(chalk`{dim > Project export: ${project.type}${modifiedAgo}}`);
|
||||
}
|
||||
@@ -147,10 +147,10 @@ export default class DeviceDelete extends EasCommand {
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
chosenDevice: AppleDevice | AppleDeviceQueryResult
|
||||
): Promise<void> {
|
||||
const removalSpinner = ora(`Removing Apple device on Expo`).start();
|
||||
const removalSpinner = ora(`Removing Apple device on EAS`).start();
|
||||
try {
|
||||
await AppleDeviceMutation.deleteAppleDeviceAsync(graphqlClient, chosenDevice.id);
|
||||
removalSpinner.succeed('Removed Apple device from Expo');
|
||||
removalSpinner.succeed('Removed Apple device from EAS');
|
||||
} catch (err) {
|
||||
removalSpinner.fail();
|
||||
throw err;
|
||||
|
||||
@@ -105,12 +105,12 @@ export default class DeviceRename extends EasCommand {
|
||||
chosenDevice: AppleDevice | AppleDeviceQueryResult,
|
||||
newDeviceName: string
|
||||
): Promise<void> {
|
||||
const removalSpinner = ora(`Renaming Apple device on Expo`).start();
|
||||
const removalSpinner = ora(`Renaming Apple device on EAS`).start();
|
||||
try {
|
||||
await AppleDeviceMutation.updateAppleDeviceAsync(graphqlClient, chosenDevice.id, {
|
||||
name: newDeviceName,
|
||||
});
|
||||
removalSpinner.succeed('Renamed Apple device on Expo');
|
||||
removalSpinner.succeed('Renamed Apple device on EAS');
|
||||
} catch (err) {
|
||||
removalSpinner.fail();
|
||||
throw err;
|
||||
|
||||
+21
-19
@@ -19,14 +19,14 @@ import {
|
||||
promptVariableTypeAsync,
|
||||
promptVariableValueAsync,
|
||||
} from '../../../utils/prompts';
|
||||
import EnvironmentVariableCreate from '../create';
|
||||
import EnvCreate from '../create';
|
||||
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../utils/prompts');
|
||||
|
||||
describe(EnvironmentVariableCreate, () => {
|
||||
describe(EnvCreate, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const variableId = 'testId';
|
||||
@@ -74,7 +74,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
|
||||
describe('in interactive mode', () => {
|
||||
it('creates a project variable', async () => {
|
||||
const command = new EnvironmentVariableCreate(
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
@@ -83,7 +83,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
'--environment',
|
||||
'production',
|
||||
'--visibility',
|
||||
'encrypted',
|
||||
'secret',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
@@ -110,7 +110,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
});
|
||||
|
||||
it('updates an existing variable in the same environment', async () => {
|
||||
const command = new EnvironmentVariableCreate(
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
@@ -119,7 +119,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
'--environment',
|
||||
'production',
|
||||
'--visibility',
|
||||
'encrypted',
|
||||
'secret',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
@@ -171,8 +171,8 @@ describe(EnvironmentVariableCreate, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a shared variable', async () => {
|
||||
const command = new EnvironmentVariableCreate(
|
||||
it('creates an account-wide variable', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
@@ -181,7 +181,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
'--environment',
|
||||
'production',
|
||||
'--scope',
|
||||
'shared',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
@@ -199,6 +199,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
isGlobal: true,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
@@ -207,8 +208,8 @@ describe(EnvironmentVariableCreate, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if a shared variable already exists', async () => {
|
||||
const command = new EnvironmentVariableCreate(
|
||||
it('throws if an account-wide variable already exists', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
@@ -217,7 +218,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
'--environment',
|
||||
'production',
|
||||
'--scope',
|
||||
'shared',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
@@ -242,8 +243,8 @@ describe(EnvironmentVariableCreate, () => {
|
||||
await expect(command.runAsync()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('updates if a shared variable already exists and --force flag is set', async () => {
|
||||
const command = new EnvironmentVariableCreate(
|
||||
it('updates if an account-wide variable already exists and --force flag is set', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
@@ -253,7 +254,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
'production',
|
||||
'--force',
|
||||
'--scope',
|
||||
'shared',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
@@ -288,8 +289,8 @@ describe(EnvironmentVariableCreate, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a shared variable and links it', async () => {
|
||||
const command = new EnvironmentVariableCreate(
|
||||
it('creates an account-wide variable and links it', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
@@ -300,7 +301,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
'--environment',
|
||||
'development',
|
||||
'--scope',
|
||||
'shared',
|
||||
'account',
|
||||
'--link',
|
||||
],
|
||||
mockConfig
|
||||
@@ -324,6 +325,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
isGlobal: true,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
testAccountId
|
||||
@@ -343,7 +345,7 @@ describe(EnvironmentVariableCreate, () => {
|
||||
});
|
||||
|
||||
it('prompts for missing arguments', async () => {
|
||||
const command = new EnvironmentVariableCreate([], mockConfig);
|
||||
const command = new EnvCreate([], mockConfig);
|
||||
|
||||
jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
|
||||
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
|
||||
+7
-10
@@ -8,14 +8,14 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvironmentVariableDelete from '../delete';
|
||||
import EnvDelete from '../delete';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvironmentVariableDelete, () => {
|
||||
describe(EnvDelete, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
@@ -40,7 +40,7 @@ describe(EnvironmentVariableDelete, () => {
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableDelete(
|
||||
const command = new EnvDelete(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
@@ -66,7 +66,7 @@ describe(EnvironmentVariableDelete, () => {
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableDelete([], mockConfig);
|
||||
const command = new EnvDelete([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
@@ -82,10 +82,7 @@ describe(EnvironmentVariableDelete, () => {
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableDelete(
|
||||
['--variable-name', 'NON_EXISTENT_VARIABLE'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvDelete(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
@@ -99,7 +96,7 @@ describe(EnvironmentVariableDelete, () => {
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
|
||||
const command = new EnvDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
@@ -116,7 +113,7 @@ describe(EnvironmentVariableDelete, () => {
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
const command = new EnvironmentVariableDelete(['--non-interactive'], mockConfig);
|
||||
const command = new EnvDelete(['--non-interactive'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
+5
-5
@@ -12,14 +12,14 @@ import {
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../../utils/prompts';
|
||||
import EnvironmentVariableGet from '../get';
|
||||
import EnvGet from '../get';
|
||||
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../utils/prompts');
|
||||
|
||||
describe(EnvironmentVariableGet, () => {
|
||||
describe(EnvGet, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockVariables: EnvironmentVariableFragment[] = [
|
||||
@@ -42,7 +42,7 @@ describe(EnvironmentVariableGet, () => {
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
|
||||
const command = new EnvironmentVariableGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
|
||||
const command = new EnvGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -68,7 +68,7 @@ describe(EnvironmentVariableGet, () => {
|
||||
const errorMessage =
|
||||
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag";
|
||||
|
||||
const command = new EnvironmentVariableGet([], mockConfig);
|
||||
const command = new EnvGet([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -85,7 +85,7 @@ describe(EnvironmentVariableGet, () => {
|
||||
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
|
||||
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
|
||||
|
||||
const command = new EnvironmentVariableGet([], mockConfig);
|
||||
const command = new EnvGet([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
+13
-19
@@ -11,7 +11,7 @@ import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvironmentVariableLink from '../link';
|
||||
import EnvLink from '../link';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
@@ -19,7 +19,7 @@ jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvironmentVariableLink, () => {
|
||||
describe(EnvLink, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
@@ -39,7 +39,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('links a shared variable to the current project in non-interactive mode', async () => {
|
||||
it('links an account-wide variable to the current project in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
@@ -53,7 +53,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
mockVariables[0]
|
||||
);
|
||||
|
||||
const command = new EnvironmentVariableLink(
|
||||
const command = new EnvLink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
@@ -75,7 +75,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('links a shared variable to the current project to a specified environment', async () => {
|
||||
it('links an account-wide variable to the current project to a specified environment', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
@@ -89,7 +89,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
mockVariables[0]
|
||||
);
|
||||
|
||||
const command = new EnvironmentVariableLink(
|
||||
const command = new EnvLink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
@@ -137,7 +137,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
);
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableLink(
|
||||
const command = new EnvLink(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
@@ -186,7 +186,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableLink([], mockConfig);
|
||||
const command = new EnvLink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
@@ -207,15 +207,12 @@ describe(EnvironmentVariableLink, () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableLink(
|
||||
['--variable-name', 'NON_EXISTENT_VARIABLE'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -239,7 +236,7 @@ describe(EnvironmentVariableLink, () => {
|
||||
});
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableLink([], mockConfig);
|
||||
const command = new EnvLink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
@@ -263,15 +260,12 @@ describe(EnvironmentVariableLink, () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableLink(
|
||||
['--variable-name', 'NON_EXISTENT_VARIABLE'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
+9
-12
@@ -13,7 +13,7 @@ import {
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import EnvironmentVariableList from '../list';
|
||||
import EnvList from '../list';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
@@ -44,7 +44,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
|
||||
},
|
||||
];
|
||||
|
||||
describe(EnvironmentVariableList, () => {
|
||||
describe(EnvList, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
|
||||
@@ -56,7 +56,7 @@ describe(EnvironmentVariableList, () => {
|
||||
it('lists project environment variables successfully', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableList([], mockConfig);
|
||||
const command = new EnvList([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -77,7 +77,7 @@ describe(EnvironmentVariableList, () => {
|
||||
it('lists project environment variables in specified environments', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableList(['--environment', 'production'], mockConfig);
|
||||
const command = new EnvList(['--environment', 'production'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -100,7 +100,7 @@ describe(EnvironmentVariableList, () => {
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableList(['--include-sensitive'], mockConfig);
|
||||
const command = new EnvList(['--include-sensitive'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -121,10 +121,10 @@ describe(EnvironmentVariableList, () => {
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('lists shared environment variables successfully', async () => {
|
||||
it('lists account-wide environment variables successfully', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableList(['--scope', 'shared'], mockConfig);
|
||||
const command = new EnvList(['--scope', 'account'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -142,15 +142,12 @@ describe(EnvironmentVariableList, () => {
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('lists shared environment variables including sensitive values', async () => {
|
||||
it('lists account-wide environment variables including sensitive values', async () => {
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync)
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableList(
|
||||
['--include-sensitive', '--scope', 'shared'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvList(['--include-sensitive', '--scope', 'account'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
+12
-18
@@ -11,7 +11,7 @@ import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvironmentVariableUnlink from '../unlink';
|
||||
import EnvUnlink from '../unlink';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
@@ -19,7 +19,7 @@ jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvironmentVariableUnlink, () => {
|
||||
describe(EnvUnlink, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
@@ -39,7 +39,7 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('unlinks a shared variable from the current project in non-interactive mode', async () => {
|
||||
it('unlinks an account-wide variable from the current project in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
@@ -54,7 +54,7 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
|
||||
).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvironmentVariableUnlink(
|
||||
const command = new EnvUnlink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
@@ -76,7 +76,7 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('unlinks a shared variable from the current project in a specified environment', async () => {
|
||||
it('unlinks an account-wide variable from the current project in a specified environment', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
@@ -91,7 +91,7 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
|
||||
).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvironmentVariableUnlink(
|
||||
const command = new EnvUnlink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
@@ -141,7 +141,7 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
});
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableUnlink([], mockConfig);
|
||||
const command = new EnvUnlink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
@@ -162,15 +162,12 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableUnlink(
|
||||
['--variable-name', 'NON_EXISTENT_VARIABLE'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -194,7 +191,7 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
});
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableUnlink([], mockConfig);
|
||||
const command = new EnvUnlink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
@@ -218,15 +215,12 @@ describe(EnvironmentVariableUnlink, () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableUnlink(
|
||||
['--variable-name', 'NON_EXISTENT_VARIABLE'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
+8
-11
@@ -12,7 +12,7 @@ import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvironmentVariableUpdate from '../update';
|
||||
import EnvUpdate from '../update';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
@@ -20,7 +20,7 @@ jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvironmentVariableUpdate, () => {
|
||||
describe(EnvUpdate, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
@@ -47,7 +47,7 @@ describe(EnvironmentVariableUpdate, () => {
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvironmentVariableUpdate(
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
@@ -81,7 +81,7 @@ describe(EnvironmentVariableUpdate, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('updates a shared variable by selected name in non-interactive mode', async () => {
|
||||
it('updates an account-wide variable by selected name in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
@@ -93,7 +93,7 @@ describe(EnvironmentVariableUpdate, () => {
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvironmentVariableUpdate(
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
@@ -107,7 +107,7 @@ describe(EnvironmentVariableUpdate, () => {
|
||||
'--environment',
|
||||
'production',
|
||||
'--scope',
|
||||
'shared',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
@@ -135,7 +135,7 @@ describe(EnvironmentVariableUpdate, () => {
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ name: mockVariables[0].name });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvironmentVariableUpdate([], mockConfig);
|
||||
const command = new EnvUpdate([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
@@ -154,10 +154,7 @@ describe(EnvironmentVariableUpdate, () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvironmentVariableUpdate(
|
||||
['--variable-name', 'NON_EXISTENT_VARIABLE'],
|
||||
mockConfig
|
||||
);
|
||||
const command = new EnvUpdate(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EnvDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with [1m--variable-name VARIABLE_NAME[22m flag to fix the issue"`;
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EnvironmentVariableDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with [1m--variable-name VARIABLE_NAME[22m flag to fix the issue"`;
|
||||
+44
-36
@@ -5,9 +5,10 @@ import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASMultiEnvironmentFlag,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableScopeFlag,
|
||||
EASVariableVisibilityFlag,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
@@ -34,23 +35,32 @@ import {
|
||||
} from '../../utils/prompts';
|
||||
import { isEnvironment, performForEnvironmentsAsync } from '../../utils/variableUtils';
|
||||
|
||||
type CreateFlags = {
|
||||
interface RawCreateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
link?: boolean;
|
||||
force?: boolean;
|
||||
type?: 'string' | 'file';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'encrypted';
|
||||
scope?: EnvironmentVariableScope;
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
'non-interactive': boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default class EnvironmentVariableCreate extends EasCommand {
|
||||
static override description =
|
||||
'create an environment variable on the current project or owner account';
|
||||
interface CreateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
link?: boolean;
|
||||
force?: boolean;
|
||||
type?: 'string' | 'file';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
scope: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvCreate extends EasCommand {
|
||||
static override description = 'create an environment variable for the current project or account';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
@@ -69,7 +79,8 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
description: 'Text value or the variable',
|
||||
}),
|
||||
link: Flags.boolean({
|
||||
description: 'Link shared variable to the current project',
|
||||
description: 'Link account-wide variable to the current project',
|
||||
hidden: true, // every account-wide variable is global for now so it's not user facing
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Overwrite existing variable',
|
||||
@@ -80,7 +91,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
options: ['string', 'file'],
|
||||
}),
|
||||
...EASVariableVisibilityFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
@@ -92,9 +103,9 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariableCreate);
|
||||
const { args, flags } = await this.parse(EnvCreate);
|
||||
|
||||
const validatedFlags = this.validateFlags(flags);
|
||||
const validatedFlags = this.sanitizeFlags(flags);
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -112,7 +123,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableCreate, {
|
||||
} = await this.getContextAsync(EnvCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -147,12 +158,12 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
await this.promptForOverwriteAsync({
|
||||
nonInteractive,
|
||||
force,
|
||||
message: `Shared variable with ${name} name already exists on this account.`,
|
||||
message: `Account-wide variable with ${name} name already exists on this account.`,
|
||||
suggestion: 'Do you want to unlink it first?',
|
||||
});
|
||||
|
||||
Log.withTick(
|
||||
`Unlinking shared variable ${chalk.bold(name)} on project ${chalk.bold(
|
||||
`Unlinking account-wide variable ${chalk.bold(name)} on project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
@@ -216,8 +227,8 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
overwrite = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Shared variable with ${name} name already exists on this account.\n` +
|
||||
`Use a different name or delete the existing variable on website or by using eas env:delete --name ${name} --scope shared command.`
|
||||
`Account-wide variable with ${name} name already exists on this account.\n` +
|
||||
`Use a different name or delete the existing variable on website or by using the "eas env:delete --name ${name} --scope account" command.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -239,6 +250,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
value,
|
||||
visibility,
|
||||
environments,
|
||||
isGlobal: true, // TODO: every account-wide variable is global for now so it's not user facing
|
||||
type: type ?? EnvironmentSecretType.String,
|
||||
},
|
||||
ownerAccount.id
|
||||
@@ -256,7 +268,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
|
||||
if (link) {
|
||||
Log.withTick(
|
||||
`Linking shared variable ${chalk.bold(name)} to project ${chalk.bold(
|
||||
`Linking account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
@@ -271,7 +283,9 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
});
|
||||
|
||||
Log.withTick(
|
||||
`Linked shared variable ${chalk.bold(name)} to project ${chalk.bold(projectDisplayName)}.`
|
||||
`Linked account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -390,7 +404,6 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
visibility: newVisibility,
|
||||
link: rest.link ?? false,
|
||||
force: rest.force ?? false,
|
||||
scope: rest.scope ?? EnvironmentVariableScope.Project,
|
||||
'non-interactive': nonInteractive,
|
||||
type: newType,
|
||||
fileName,
|
||||
@@ -398,24 +411,19 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
};
|
||||
}
|
||||
|
||||
private validateFlags(flags: CreateFlags & { type?: string; visibility?: string }): CreateFlags {
|
||||
if (flags.scope !== EnvironmentVariableScope.Shared && flags.link) {
|
||||
private sanitizeFlags(flags: RawCreateFlags): CreateFlags {
|
||||
if (flags.scope !== 'account' && flags.link) {
|
||||
throw new Error(
|
||||
`Unexpected argument: --link can only be used when creating shared variables`
|
||||
`Unexpected argument: --link can only be used when creating account-wide variables`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
flags.scope === EnvironmentVariableScope.Shared &&
|
||||
flags.environment &&
|
||||
!flags.link &&
|
||||
flags['non-interactive']
|
||||
) {
|
||||
throw new Error(
|
||||
'Unexpected argument: --environment in non-interactive mode can only be used with --link flag.'
|
||||
);
|
||||
}
|
||||
|
||||
return flags;
|
||||
return {
|
||||
...flags,
|
||||
scope:
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project,
|
||||
};
|
||||
}
|
||||
}
|
||||
+26
-15
@@ -4,8 +4,9 @@ import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableScopeFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
|
||||
@@ -15,17 +16,22 @@ import Log from '../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type DeleteFlags = {
|
||||
interface DeleteFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
scope?: EnvironmentVariableScope;
|
||||
};
|
||||
scope: EnvironmentVariableScope;
|
||||
}
|
||||
|
||||
export default class EnvironmentVariableDelete extends EasCommand {
|
||||
static override description = 'delete an environment variable by name';
|
||||
interface RawDeleteFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
}
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvDelete extends EasCommand {
|
||||
static override description = 'delete an environment variable for the current project or account';
|
||||
|
||||
static override flags = {
|
||||
'variable-name': Flags.string({
|
||||
@@ -35,7 +41,7 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to delete',
|
||||
}),
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
@@ -54,17 +60,17 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariableDelete);
|
||||
const { args, flags } = await this.parse(EnvDelete);
|
||||
const {
|
||||
'variable-name': name,
|
||||
'variable-environment': environment,
|
||||
'non-interactive': nonInteractive,
|
||||
scope,
|
||||
} = this.validateInputs(flags, args);
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableDelete, {
|
||||
} = await this.getContextAsync(EnvDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -139,8 +145,8 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
Log.withTick(`️Deleted variable ${selectedVariable.name}".`);
|
||||
}
|
||||
|
||||
private validateInputs(
|
||||
flags: DeleteFlags,
|
||||
private sanitizeInputs(
|
||||
flags: RawDeleteFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): DeleteFlags {
|
||||
if (flags['non-interactive']) {
|
||||
@@ -153,6 +159,11 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const scope =
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
|
||||
@@ -161,9 +172,9 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return { ...flags, 'variable-environment': environment };
|
||||
return { ...flags, 'variable-environment': environment, scope };
|
||||
}
|
||||
|
||||
return flags;
|
||||
return { ...flags, scope };
|
||||
}
|
||||
}
|
||||
+71
-31
@@ -28,9 +28,7 @@ interface RawFlags {
|
||||
|
||||
export default class EnvExec extends EasCommand {
|
||||
static override description =
|
||||
'execute a bash command with environment variables from the selected environment';
|
||||
|
||||
static override hidden = true;
|
||||
'execute a command with environment variables from the selected environment';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
@@ -55,9 +53,10 @@ export default class EnvExec extends EasCommand {
|
||||
},
|
||||
];
|
||||
|
||||
private isNonInteractive: boolean = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(EnvExec);
|
||||
|
||||
const parsedFlags = this.sanitizeFlagsAndArgs(flags, args);
|
||||
|
||||
const {
|
||||
@@ -67,6 +66,8 @@ export default class EnvExec extends EasCommand {
|
||||
nonInteractive: parsedFlags.nonInteractive,
|
||||
});
|
||||
|
||||
this.isNonInteractive = parsedFlags.nonInteractive;
|
||||
|
||||
const environment =
|
||||
parsedFlags.environment ??
|
||||
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
|
||||
@@ -74,12 +75,20 @@ export default class EnvExec extends EasCommand {
|
||||
graphqlClient,
|
||||
projectId,
|
||||
environment,
|
||||
nonInteractive: parsedFlags.nonInteractive,
|
||||
});
|
||||
|
||||
await this.runCommandWithEnvVarsAsync({
|
||||
command: parsedFlags.command,
|
||||
environmentVariables,
|
||||
});
|
||||
if (parsedFlags.nonInteractive) {
|
||||
await this.runCommandNonInteractiveWithEnvVarsAsync({
|
||||
command: parsedFlags.command,
|
||||
environmentVariables,
|
||||
});
|
||||
} else {
|
||||
await this.runCommandWithEnvVarsAsync({
|
||||
command: parsedFlags.command,
|
||||
environmentVariables,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeFlagsAndArgs(
|
||||
@@ -98,13 +107,48 @@ export default class EnvExec extends EasCommand {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const firstChar = bash_command[0];
|
||||
const lastChar = bash_command[bash_command.length - 1];
|
||||
const cleanCommand =
|
||||
(firstChar === '"' && lastChar === '"') || (firstChar === "'" && lastChar === "'")
|
||||
? bash_command.slice(1, -1)
|
||||
: bash_command;
|
||||
|
||||
return {
|
||||
nonInteractive: rawFlags['non-interactive'],
|
||||
environment,
|
||||
command: bash_command,
|
||||
command: cleanCommand,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
protected override async catch(err: Error): Promise<any> {
|
||||
// when in non-interactive, make the behavior of this command a pure passthrough, outputting only the subprocess' stdout and stderr and exiting with the
|
||||
// sub-command's exit status
|
||||
if (this.isNonInteractive) {
|
||||
process.exitCode = process.exitCode ?? (err as any).status ?? 1;
|
||||
} else {
|
||||
await super.catch(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async runCommandNonInteractiveWithEnvVarsAsync({
|
||||
command,
|
||||
environmentVariables,
|
||||
}: {
|
||||
command: string;
|
||||
environmentVariables: Record<string, string>;
|
||||
}): Promise<void> {
|
||||
await spawnAsync(command, [], {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
...environmentVariables,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async runCommandWithEnvVarsAsync({
|
||||
command,
|
||||
environmentVariables,
|
||||
@@ -113,7 +157,8 @@ export default class EnvExec extends EasCommand {
|
||||
environmentVariables: Record<string, string>;
|
||||
}): Promise<void> {
|
||||
Log.log(`Running command: ${chalk.bold(command)}`);
|
||||
const spawnPromise = spawnAsync('bash', ['-c', command], {
|
||||
const spawnPromise = spawnAsync(command, [], {
|
||||
shell: true,
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -149,10 +194,12 @@ export default class EnvExec extends EasCommand {
|
||||
graphqlClient,
|
||||
projectId,
|
||||
environment,
|
||||
nonInteractive,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<Record<string, string>> {
|
||||
const environmentVariablesQueryResult =
|
||||
await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
|
||||
@@ -160,31 +207,24 @@ export default class EnvExec extends EasCommand {
|
||||
environment,
|
||||
});
|
||||
|
||||
const secretEnvironmentVariables = environmentVariablesQueryResult.filter(
|
||||
({ value }) => !value
|
||||
);
|
||||
if (secretEnvironmentVariables.length > 0) {
|
||||
Log.warn(`The following environment variables are secret and cannot be downloaded locally:`);
|
||||
for (const { name } of secretEnvironmentVariables) {
|
||||
Log.warn(`- ${name}`);
|
||||
}
|
||||
Log.warn('Proceeding with the rest of the environment variables.');
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const nonSecretEnvironmentVariables = environmentVariablesQueryResult.filter(
|
||||
({ value }) => !!value
|
||||
);
|
||||
if (nonSecretEnvironmentVariables.length === 0) {
|
||||
throw new Error('No readable environment variables found for the selected environment.');
|
||||
|
||||
if (!nonInteractive) {
|
||||
if (nonSecretEnvironmentVariables.length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${nonSecretEnvironmentVariables
|
||||
.map(e => e.name)
|
||||
.join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
Log.log(
|
||||
`Loaded environment variables for the selected environment "${environment.toLowerCase()}":`
|
||||
);
|
||||
for (const { name } of nonSecretEnvironmentVariables) {
|
||||
Log.log(`- ${name}`);
|
||||
}
|
||||
Log.newLine();
|
||||
|
||||
const environmentVariables: Record<string, string> = {};
|
||||
for (const { name, value } of nonSecretEnvironmentVariables) {
|
||||
|
||||
+26
-14
@@ -4,9 +4,10 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableFormatFlag,
|
||||
EASVariableScopeFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
@@ -20,18 +21,24 @@ import Log from '../../log';
|
||||
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
|
||||
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type GetFlags = {
|
||||
interface RawGetFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
format?: string;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
}
|
||||
|
||||
interface GetFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
format?: string;
|
||||
scope: EnvironmentVariableScope;
|
||||
};
|
||||
}
|
||||
|
||||
export default class EnvironmentVariableGet extends EasCommand {
|
||||
static override description = 'get environment variable';
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvGet extends EasCommand {
|
||||
static override description = 'view an environment variable for the current project or account';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
@@ -56,12 +63,12 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
description: 'Current environment of the variable',
|
||||
}),
|
||||
...EASVariableFormatFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariableGet);
|
||||
const { args, flags } = await this.parse(EnvGet);
|
||||
|
||||
let {
|
||||
'variable-environment': environment,
|
||||
@@ -69,12 +76,12 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
format,
|
||||
scope,
|
||||
} = this.validateInputs(flags, args);
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableGet, {
|
||||
} = await this.getContextAsync(EnvGet, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -126,7 +133,7 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private validateInputs(flags: GetFlags, { environment }: { environment?: string }): GetFlags {
|
||||
private sanitizeInputs(flags: RawGetFlags, { environment }: { environment?: string }): GetFlags {
|
||||
if (flags['non-interactive']) {
|
||||
if (!flags['variable-name']) {
|
||||
throw new Error('Variable name is required. Run the command with --variable-name flag.');
|
||||
@@ -144,6 +151,10 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
const scope =
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
@@ -152,11 +163,12 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
);
|
||||
}
|
||||
return {
|
||||
'variable-environment': environment,
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
return flags;
|
||||
return { ...flags, scope };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-10
@@ -23,9 +23,10 @@ type LinkFlags = {
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
};
|
||||
|
||||
export default class EnvironmentVariableLink extends EasCommand {
|
||||
static override description = 'link a shared environment variable to the current project';
|
||||
export default class EnvLink extends EasCommand {
|
||||
static override description = 'link an account-wide environment variable to the current project';
|
||||
|
||||
// for now we only roll out global account-wide env variables so this should stay hidden
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
@@ -55,7 +56,7 @@ export default class EnvironmentVariableLink extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariableLink);
|
||||
const { args, flags } = await this.parse(EnvLink);
|
||||
|
||||
let {
|
||||
'variable-name': name,
|
||||
@@ -67,7 +68,7 @@ export default class EnvironmentVariableLink extends EasCommand {
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableLink, {
|
||||
} = await this.getContextAsync(EnvLink, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -87,7 +88,7 @@ export default class EnvironmentVariableLink extends EasCommand {
|
||||
);
|
||||
}
|
||||
selectedVariable = await selectAsync(
|
||||
'Select shared variable',
|
||||
'Select account-wide variable',
|
||||
variables.map(variable => ({
|
||||
title: formatVariableName(variable),
|
||||
value: variable,
|
||||
@@ -96,7 +97,7 @@ export default class EnvironmentVariableLink extends EasCommand {
|
||||
}
|
||||
|
||||
if (!selectedVariable) {
|
||||
throw new Error(`Shared variable ${name} doesn't exist`);
|
||||
throw new Error(`Account-wide variable ${name} doesn't exist`);
|
||||
}
|
||||
|
||||
let explicitSelect = false;
|
||||
@@ -172,7 +173,7 @@ export default class EnvironmentVariableLink extends EasCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
private validateInputs(flags: LinkFlags, { environment }: Record<string, string>): LinkFlags {
|
||||
private validateInputs(flags: LinkFlags, { environment }: { environment?: string }): LinkFlags {
|
||||
environment = environment?.toUpperCase();
|
||||
|
||||
if (environment && !isEnvironment(environment)) {
|
||||
@@ -196,9 +197,7 @@ export default class EnvironmentVariableLink extends EasCommand {
|
||||
}
|
||||
|
||||
return {
|
||||
'variable-name': flags['variable-name'],
|
||||
'variable-environment': flags['variable-environment'],
|
||||
'non-interactive': flags['non-interactive'],
|
||||
...flags,
|
||||
environment: environments,
|
||||
};
|
||||
}
|
||||
|
||||
+28
-16
@@ -4,9 +4,10 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASMultiEnvironmentFlag,
|
||||
EASVariableFormatFlag,
|
||||
EASVariableScopeFlag,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import {
|
||||
@@ -66,19 +67,26 @@ async function getVariablesForScopeAsync(
|
||||
});
|
||||
}
|
||||
|
||||
type ListFlags = {
|
||||
scope: EnvironmentVariableScope;
|
||||
interface RawListFlags {
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive'?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default class EnvironmentValueList extends EasCommand {
|
||||
static override description = 'list environment variables for the current project';
|
||||
interface ListFlags {
|
||||
scope: EnvironmentVariableScope;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvList extends EasCommand {
|
||||
static override description = 'list environment variables for the current project or account';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
@@ -96,7 +104,7 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
}),
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASVariableFormatFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
@@ -109,7 +117,7 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentValueList);
|
||||
const { args, flags } = await this.parse(EnvList);
|
||||
|
||||
let {
|
||||
format,
|
||||
@@ -118,12 +126,12 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
'include-sensitive': includeSensitive,
|
||||
'include-file-content': includeFileContent,
|
||||
'non-interactive': nonInteractive,
|
||||
} = this.validateInputs(flags, args);
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentValueList, {
|
||||
} = await this.getContextAsync(EnvList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
@@ -156,7 +164,7 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
}
|
||||
} else {
|
||||
if (scope === EnvironmentVariableScope.Shared) {
|
||||
Log.log(chalk.bold('Shared variables for this account:'));
|
||||
Log.log(chalk.bold('Account-wide variables for this account:'));
|
||||
} else {
|
||||
Log.log(chalk.bold(`Variables for this project:`));
|
||||
}
|
||||
@@ -167,10 +175,10 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
});
|
||||
}
|
||||
|
||||
private validateInputs(
|
||||
flags: ListFlags,
|
||||
{ environment }: Record<string, string>
|
||||
): ListFlags & { 'non-interactive': boolean } {
|
||||
private sanitizeInputs(
|
||||
flags: RawListFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): ListFlags {
|
||||
if (environment && !isEnvironment(environment.toUpperCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
@@ -185,6 +193,10 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
...flags,
|
||||
'non-interactive': flags['non-interactive'] ?? false,
|
||||
environment: environments,
|
||||
scope:
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project,
|
||||
};
|
||||
}
|
||||
}
|
||||
+12
-13
@@ -15,10 +15,9 @@ import { confirmAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvironmentVariablePull extends EasCommand {
|
||||
static override description = 'pull env file';
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvPull extends EasCommand {
|
||||
static override description =
|
||||
'pull environment variables for the selected environment to .env file';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
@@ -48,7 +47,7 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
let {
|
||||
args: { environment: argEnvironment },
|
||||
flags: { environment: flagEnvironment, path: targetPath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentVariablePull);
|
||||
} = await this.parse(EnvPull);
|
||||
|
||||
let environment = flagEnvironment?.toUpperCase() ?? argEnvironment?.toUpperCase();
|
||||
|
||||
@@ -64,7 +63,7 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
projectDir,
|
||||
} = await this.getContextAsync(EnvironmentVariablePull, {
|
||||
} = await this.getContextAsync(EnvPull, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -117,7 +116,7 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
return `${variable.name}=${currentEnvLocal[variable.name]}`;
|
||||
}
|
||||
skippedSecretVariables.push(variable.name);
|
||||
return `# ${variable.name}=***** (secret variables are not available for reading)`;
|
||||
return `# ${variable.name}=***** (secret)`;
|
||||
}
|
||||
if (variable.type === EnvironmentSecretType.FileBase64 && variable.valueWithFileContent) {
|
||||
const filePath = path.join(envDir, variable.name);
|
||||
@@ -131,20 +130,20 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
await fs.writeFile(targetPath, filePrefix + envFileContentLines.join('\n'));
|
||||
|
||||
Log.log(
|
||||
`Pulled environment variables from ${environment.toLowerCase()} environment to ${targetPath}.`
|
||||
`Pulled plain text and sensitive environment variables from "${environment.toLowerCase()}" environment to ${targetPath}.`
|
||||
);
|
||||
|
||||
if (overridenSecretVariables.length > 0) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`Reused local values for following secrets: ${overridenSecretVariables.join('\n')}`);
|
||||
Log.log(`Reused local values for following secrets: ${overridenSecretVariables.join('\n')}.`);
|
||||
}
|
||||
|
||||
if (skippedSecretVariables.length > 0) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`The following variables have the encrypted visibility and can not be read outside of EAS servers. Set their values manually in .env.local: ${skippedSecretVariables.join(
|
||||
'\n'
|
||||
)}`
|
||||
Log.log(
|
||||
`The following variables have the secret visibility and can't be read outside of EAS servers. Set their values manually in your .env file: ${skippedSecretVariables.join(
|
||||
', '
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-8
@@ -21,10 +21,9 @@ import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvironmentVariablePush extends EasCommand {
|
||||
static override description = 'push env file';
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvPush extends EasCommand {
|
||||
static override description =
|
||||
'push environment variables from .env file to the selected environment';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
@@ -49,14 +48,14 @@ export default class EnvironmentVariablePush extends EasCommand {
|
||||
];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariablePush);
|
||||
const { args, flags } = await this.parse(EnvPush);
|
||||
|
||||
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariablePush, {
|
||||
} = await this.getContextAsync(EnvPush, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -106,12 +105,12 @@ export default class EnvironmentVariablePush extends EasCommand {
|
||||
const existingDifferentSharedVariablesNames = existingDifferentSharedVariables.map(
|
||||
variable => variable.name
|
||||
);
|
||||
Log.error('Shared variables cannot be overwritten by eas env:push command.');
|
||||
Log.error('Account-wide variables cannot be overwritten by eas env:push command.');
|
||||
Log.error('Remove them from the env file or unlink them from the project to continue:');
|
||||
existingDifferentSharedVariablesNames.forEach(name => {
|
||||
Log.error(`- ${name}`);
|
||||
});
|
||||
throw new Error('Shared variables cannot be overwritten by eas env:push command');
|
||||
throw new Error('Account-wide variables cannot be overwritten by eas env:push command');
|
||||
}
|
||||
|
||||
if (existingDifferentVariables.length > 0) {
|
||||
|
||||
+8
-6
@@ -18,9 +18,11 @@ type UnlinkFlags = {
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
};
|
||||
|
||||
export default class EnvironmentVariableUnlink extends EasCommand {
|
||||
static override description = 'unlink a shared environment variable to the current project';
|
||||
export default class EnvUnlink extends EasCommand {
|
||||
static override description =
|
||||
'unlink an account-wide environment variable from the current project';
|
||||
|
||||
// for now we only roll out global account-wide env variables so this should stay hidden
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
@@ -46,7 +48,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
|
||||
];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariableUnlink);
|
||||
const { args, flags } = await this.parse(EnvUnlink);
|
||||
|
||||
let {
|
||||
'variable-name': name,
|
||||
@@ -57,7 +59,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableUnlink, {
|
||||
} = await this.getContextAsync(EnvUnlink, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -74,7 +76,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
|
||||
throw new Error("Multiple variables found, please select one using '--variable-name'");
|
||||
}
|
||||
selectedVariable = await selectAsync(
|
||||
'Select shared variable',
|
||||
'Select account-wide variable',
|
||||
variables.map(variable => ({
|
||||
title: formatVariableName(variable),
|
||||
value: variable,
|
||||
@@ -83,7 +85,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
|
||||
}
|
||||
|
||||
if (!selectedVariable) {
|
||||
throw new Error(`Shared variable ${name} doesn't exist`);
|
||||
throw new Error(`Account-wide variable ${name} doesn't exist`);
|
||||
}
|
||||
|
||||
let explicitSelect = false;
|
||||
|
||||
+35
-19
@@ -6,9 +6,10 @@ import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASMultiEnvironmentFlag,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableScopeFlag,
|
||||
EASVariableVisibilityFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
@@ -37,23 +38,32 @@ import {
|
||||
} from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type UpdateFlags = {
|
||||
interface RawUpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope?: EnvironmentVariableScope;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
visibility?: 'plaintext' | 'sensitive' | 'encrypted';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
type?: 'string' | 'file';
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
static override description =
|
||||
'update an environment variable on the current project or owner account';
|
||||
interface UpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
type?: 'string' | 'file';
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
static override hidden = true;
|
||||
export default class EnvUpdate extends EasCommand {
|
||||
static override description = 'update an environment variable on the current project or account';
|
||||
|
||||
static override flags = {
|
||||
'variable-name': Flags.string({
|
||||
@@ -74,7 +84,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
options: ['string', 'file'],
|
||||
}),
|
||||
...EASVariableVisibilityFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
@@ -95,7 +105,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvironmentVariableUpdate);
|
||||
const { args, flags } = await this.parse(EnvUpdate);
|
||||
const {
|
||||
name,
|
||||
value: rawValue,
|
||||
@@ -106,12 +116,12 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
environment: environments,
|
||||
type,
|
||||
visibility,
|
||||
} = this.validateFlags(flags, args);
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableUpdate, {
|
||||
} = await this.getContextAsync(EnvUpdate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -177,6 +187,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
scope,
|
||||
});
|
||||
|
||||
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
|
||||
@@ -194,8 +205,8 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
|
||||
Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`);
|
||||
}
|
||||
private validateFlags(
|
||||
flags: UpdateFlags,
|
||||
private sanitizeInputs(
|
||||
flags: RawUpdateFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): UpdateFlags {
|
||||
if (flags['non-interactive']) {
|
||||
@@ -209,6 +220,11 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const scope =
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
@@ -217,12 +233,13 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
);
|
||||
}
|
||||
return {
|
||||
'variable-environment': environment,
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
|
||||
return flags;
|
||||
return { ...flags, scope };
|
||||
}
|
||||
|
||||
private async promptForMissingFlagsAsync(
|
||||
@@ -328,15 +345,14 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
name,
|
||||
value,
|
||||
environment: environments,
|
||||
visibility: newVisibility,
|
||||
scope: rest.scope ?? EnvironmentVariableScope.Project,
|
||||
'non-interactive': nonInteractive,
|
||||
type: newType,
|
||||
fileName,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import openBrowserAsync from 'better-opn';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
|
||||
import { GetServerSideEnvironmentVariablesFn } from '../../commandUtils/context/ServerSideEnvironmentVariablesContextField';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EasEnvironmentFlagParameters,
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { diffFingerprint } from '../../fingerprint/cli';
|
||||
import { abridgedDiff } from '../../fingerprint/diff';
|
||||
import { Fingerprint, FingerprintDiffItem } from '../../fingerprint/types';
|
||||
import {
|
||||
getFingerprintInfoFromLocalProjectForPlatformsAsync,
|
||||
stringToAppPlatform,
|
||||
} from '../../fingerprint/utils';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
EnvironmentVariableEnvironment,
|
||||
FingerprintFragment,
|
||||
UpdateFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { FingerprintQuery } from '../../graphql/queries/FingerprintQuery';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
|
||||
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { Client } from '../../vcs/vcs';
|
||||
|
||||
enum FingerprintOriginType {
|
||||
Build = 'build',
|
||||
Update = 'update',
|
||||
Hash = 'hash',
|
||||
Project = 'project',
|
||||
}
|
||||
|
||||
type FingerprintOrigin = {
|
||||
type: FingerprintOriginType;
|
||||
build?: BuildFragment;
|
||||
update?: UpdateFragment;
|
||||
};
|
||||
|
||||
export default class FingerprintCompare extends EasCommand {
|
||||
static override description = 'compare fingerprints of the current project, builds, and updates';
|
||||
static override strict = false;
|
||||
|
||||
static override examples = [
|
||||
'$ eas fingerprint:compare \t # Compare fingerprints in interactive mode',
|
||||
'$ eas fingerprint:compare <FINGERPRINT-HASH> \t # Compare fingerprint against local directory',
|
||||
'$ eas fingerprint:compare <FINGERPRINT-HASH-1> <FINGERPRINT-HASH-2> \t # Compare provided fingerprints',
|
||||
'$ eas fingerprint:compare --build-id <BUILD-ID> \t # Compare fingerprint from build against local directory',
|
||||
'$ eas fingerprint:compare --build-id <BUILD-ID> --environment production \t # Compare fingerprint from build against local directory with the "production" environment',
|
||||
'$ eas fingerprint:compare --build-id <BUILD-ID-1> --build-id <BUILD-ID-2>\t # Compare fingerprint from a build against another build',
|
||||
'$ eas fingerprint:compare --build-id <BUILD-ID> --update-id <UPDATE-ID>\t # Compare fingerprint from build against fingerprint from update',
|
||||
'$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> \t # Compare fingerprint from update against provided fingerprint',
|
||||
];
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'hash1',
|
||||
description:
|
||||
"If provided alone, HASH1 is compared against the current project's fingerprint.",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'hash2',
|
||||
description: 'If two hashes are provided, HASH1 is compared against HASH2.',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
'build-id': Flags.string({
|
||||
aliases: ['buildId'],
|
||||
description: 'Compare the fingerprint with the build with the specified ID',
|
||||
multiple: true,
|
||||
}),
|
||||
'update-id': Flags.string({
|
||||
aliases: ['updateId'],
|
||||
description: 'Compare the fingerprint with the update with the specified ID',
|
||||
multiple: true,
|
||||
}),
|
||||
open: Flags.boolean({
|
||||
description: 'Open the fingerprint comparison in the browser',
|
||||
}),
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description:
|
||||
'If generating a fingerprint from the local directory, use the specified environment.',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
...this.ContextOptions.ServerSideEnvironmentVariables,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(FingerprintCompare);
|
||||
const { hash1, hash2 } = args;
|
||||
const {
|
||||
json,
|
||||
'non-interactive': nonInteractive,
|
||||
'build-id': buildIds,
|
||||
'update-id': updateIds,
|
||||
open,
|
||||
environment,
|
||||
} = flags;
|
||||
const [buildId1, buildId2] = buildIds ?? [];
|
||||
const [updateId1, updateId2] = updateIds ?? [];
|
||||
|
||||
const {
|
||||
projectId,
|
||||
privateProjectConfig: { projectDir },
|
||||
loggedIn: { graphqlClient },
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
} = await this.getContextAsync(FingerprintCompare, {
|
||||
nonInteractive,
|
||||
withServerSideEnvironment: environment ?? null,
|
||||
});
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const firstFingerprintInfo = await getFingerprintInfoAsync(
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
{
|
||||
nonInteractive,
|
||||
buildId: buildId1,
|
||||
updateId: updateId1,
|
||||
hash: hash1,
|
||||
}
|
||||
);
|
||||
const { fingerprint: firstFingerprint, origin: firstFingerprintOrigin } = firstFingerprintInfo;
|
||||
|
||||
const isFirstFingerprintSpecifiedByFlagOrArg = hash1 || buildId1 || updateId1;
|
||||
const isSecondFingerprintSpecifiedByFlagOrArg = hash2 || buildId2 || updateId2;
|
||||
const secondFingerprintInfo = await getFingerprintInfoAsync(
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
{
|
||||
nonInteractive,
|
||||
buildId: buildId2,
|
||||
updateId: updateId2,
|
||||
hash: hash2,
|
||||
useProjectFingerprint:
|
||||
isFirstFingerprintSpecifiedByFlagOrArg && !isSecondFingerprintSpecifiedByFlagOrArg,
|
||||
environmentForProjectFingerprint: environment,
|
||||
},
|
||||
firstFingerprintInfo
|
||||
);
|
||||
const { fingerprint: secondFingerprint, origin: secondFingerprintOrigin } =
|
||||
secondFingerprintInfo;
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ fingerprint1: firstFingerprint, fingerprint2: secondFingerprint });
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstFingerprint.hash === secondFingerprint.hash) {
|
||||
Log.log(
|
||||
`✅ ${capitalizeFirstLetter(
|
||||
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
|
||||
)} matches ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
Log.log(
|
||||
`🔄 ${capitalizeFirstLetter(
|
||||
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
|
||||
)} differs from ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
|
||||
);
|
||||
}
|
||||
|
||||
const fingerprintDiffs = diffFingerprint(projectDir, firstFingerprint, secondFingerprint);
|
||||
if (!fingerprintDiffs) {
|
||||
Log.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePathDiffs = fingerprintDiffs.filter(diff => {
|
||||
let sourceType;
|
||||
if (diff.op === 'added') {
|
||||
sourceType = diff.addedSource.type;
|
||||
} else if (diff.op === 'removed') {
|
||||
sourceType = diff.removedSource.type;
|
||||
} else if (diff.op === 'changed') {
|
||||
sourceType = diff.beforeSource.type;
|
||||
}
|
||||
return sourceType === 'dir' || sourceType === 'file';
|
||||
});
|
||||
if (filePathDiffs.length > 0) {
|
||||
Log.newLine();
|
||||
Log.log('📁 Paths with native dependencies:');
|
||||
}
|
||||
const fields = [];
|
||||
for (const diff of filePathDiffs) {
|
||||
const field = getDiffFilePathFields(diff);
|
||||
if (!field) {
|
||||
throw new Error(`Unsupported diff: ${JSON.stringify(diff)}`);
|
||||
}
|
||||
fields.push(field);
|
||||
}
|
||||
Log.log(
|
||||
formatFields(fields, {
|
||||
labelFormat: label => ` ${chalk.dim(label)}:`,
|
||||
})
|
||||
);
|
||||
|
||||
const contentDiffs = fingerprintDiffs.filter(diff => {
|
||||
let sourceType;
|
||||
if (diff.op === 'added') {
|
||||
sourceType = diff.addedSource.type;
|
||||
} else if (diff.op === 'removed') {
|
||||
sourceType = diff.removedSource.type;
|
||||
} else if (diff.op === 'changed') {
|
||||
sourceType = diff.beforeSource.type;
|
||||
}
|
||||
return sourceType === 'contents';
|
||||
});
|
||||
|
||||
for (const diff of contentDiffs) {
|
||||
printContentDiff(diff);
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const fingerprintCompareUrl = new URL(
|
||||
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/compare/${firstFingerprintInfo.fingerprint.hash}/${secondFingerprintInfo.fingerprint.hash}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
);
|
||||
|
||||
if (!open) {
|
||||
Log.newLine();
|
||||
Log.withInfo(
|
||||
`💡 Use the --open flag to view the comparison in the browser. ${learnMore(
|
||||
fingerprintCompareUrl.toString()
|
||||
)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
await openBrowserAsync(fingerprintCompareUrl.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async function getFingerprintInfoAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
vcsClient: Client,
|
||||
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn,
|
||||
{
|
||||
buildId,
|
||||
updateId,
|
||||
hash,
|
||||
useProjectFingerprint,
|
||||
environmentForProjectFingerprint,
|
||||
nonInteractive,
|
||||
}: {
|
||||
buildId?: string;
|
||||
updateId?: string;
|
||||
hash: string;
|
||||
useProjectFingerprint?: boolean;
|
||||
environmentForProjectFingerprint?: string;
|
||||
nonInteractive: boolean;
|
||||
},
|
||||
firstFingerprintInfo?: {
|
||||
fingerprint: Fingerprint;
|
||||
platforms?: AppPlatform[];
|
||||
origin: FingerprintOrigin;
|
||||
}
|
||||
): Promise<{ fingerprint: Fingerprint; origin: FingerprintOrigin }> {
|
||||
if (hash) {
|
||||
return await getFingerprintInfoFromHashAsync(graphqlClient, projectId, hash);
|
||||
} else if (updateId) {
|
||||
return await getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
nonInteractive,
|
||||
updateId
|
||||
);
|
||||
} else if (buildId) {
|
||||
return await getFingerprintInfoFromBuildIdAsync(graphqlClient, buildId);
|
||||
} else if (useProjectFingerprint) {
|
||||
if (!firstFingerprintInfo) {
|
||||
throw new Error(
|
||||
'First fingerprint must be provided in order to compare against the project.'
|
||||
);
|
||||
}
|
||||
return await getFingerprintInfoFromLocalProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
firstFingerprintInfo,
|
||||
environment: environmentForProjectFingerprint,
|
||||
});
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'Insufficent arguments provided for fingerprint comparison in non-interactive mode'
|
||||
);
|
||||
}
|
||||
return await getFingerprintInfoInteractiveAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
firstFingerprintInfo,
|
||||
environmentForProjectFingerprint,
|
||||
});
|
||||
}
|
||||
|
||||
async function getFingerprintInfoInteractiveAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
firstFingerprintInfo,
|
||||
environmentForProjectFingerprint,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
vcsClient: Client;
|
||||
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn;
|
||||
firstFingerprintInfo?: {
|
||||
fingerprint: Fingerprint;
|
||||
platforms?: AppPlatform[];
|
||||
origin: FingerprintOrigin;
|
||||
};
|
||||
environmentForProjectFingerprint?: string;
|
||||
}): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
|
||||
const prompt = firstFingerprintInfo
|
||||
? 'Select the second fingerprint to compare against'
|
||||
: 'Select a reference fingerprint for comparison';
|
||||
const originType = await selectAsync<FingerprintOriginType>(prompt, [
|
||||
...(firstFingerprintInfo
|
||||
? [{ title: 'Current project fingerprint', value: FingerprintOriginType.Project }]
|
||||
: []),
|
||||
{ title: 'Build fingerprint', value: FingerprintOriginType.Build },
|
||||
{ title: 'Update fingerprint', value: FingerprintOriginType.Update },
|
||||
{ title: 'Enter a fingerprint hash manually', value: FingerprintOriginType.Hash },
|
||||
]);
|
||||
if (originType === FingerprintOriginType.Project) {
|
||||
if (!firstFingerprintInfo) {
|
||||
throw new Error(
|
||||
'First fingerprint must be provided in order to compare against the project.'
|
||||
);
|
||||
}
|
||||
return await getFingerprintInfoFromLocalProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
firstFingerprintInfo,
|
||||
environment: environmentForProjectFingerprint,
|
||||
});
|
||||
} else if (originType === FingerprintOriginType.Build) {
|
||||
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
const buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
|
||||
filters: { hasFingerprint: true },
|
||||
});
|
||||
if (!buildId) {
|
||||
throw new Error('Must select build with fingerprint for comparison.');
|
||||
}
|
||||
return await getFingerprintInfoFromBuildIdAsync(graphqlClient, buildId);
|
||||
} else if (originType === FingerprintOriginType.Update) {
|
||||
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: 'On which branch would you like search for an update?',
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: updateBranch.name,
|
||||
}),
|
||||
paginatedQueryOptions: {
|
||||
json: false,
|
||||
nonInteractive: false,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedUpdateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName: selectedBranch.name,
|
||||
paginatedQueryOptions: {
|
||||
json: false,
|
||||
nonInteractive: false,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
const updateGroupId = selectedUpdateGroup[0].group;
|
||||
return await getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
false,
|
||||
updateGroupId
|
||||
);
|
||||
} else if (originType === FingerprintOriginType.Hash) {
|
||||
const { hash } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'hash',
|
||||
message: 'Provide the fingerprint hash',
|
||||
validate: (value: string) => !!value.trim(),
|
||||
hint: '0000000000000000000000000000000000000000',
|
||||
});
|
||||
return await getFingerprintInfoFromHashAsync(graphqlClient, projectId, hash);
|
||||
} else {
|
||||
throw new Error(`Unsupported fingerprint origin type: ${originType}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getFingerprintInfoFromLocalProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
firstFingerprintInfo,
|
||||
environment,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
vcsClient: Client;
|
||||
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn;
|
||||
firstFingerprintInfo: {
|
||||
fingerprint: Fingerprint;
|
||||
platforms?: AppPlatform[];
|
||||
origin: FingerprintOrigin;
|
||||
};
|
||||
environment?: string;
|
||||
}): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
|
||||
const firstFingerprintPlatforms = firstFingerprintInfo.platforms;
|
||||
if (!firstFingerprintPlatforms || firstFingerprintPlatforms.length === 0) {
|
||||
throw new Error(
|
||||
`Cannot compare the local directory against the provided fingerprint hash "${firstFingerprintInfo.fingerprint.hash}" because the associated platform could not be determined. Ensure the fingerprint is linked to a build or update to identify the platform.`
|
||||
);
|
||||
}
|
||||
|
||||
if (environment) {
|
||||
Log.log(`🔧 Using environment: ${environment}`);
|
||||
}
|
||||
const env = environment
|
||||
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
|
||||
: undefined;
|
||||
const fingerprint = await getFingerprintInfoFromLocalProjectForPlatformsAsync(
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
firstFingerprintPlatforms,
|
||||
{ env }
|
||||
);
|
||||
return { fingerprint, origin: { type: FingerprintOriginType.Project } };
|
||||
}
|
||||
|
||||
async function getFingerprintFromUpdateFragmentAsync(
|
||||
updateWithFingerprint: UpdateFragment
|
||||
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
|
||||
if (!updateWithFingerprint.fingerprint) {
|
||||
throw new Error(`Fingerprint for update ${updateWithFingerprint.id} was not computed.`);
|
||||
} else if (!updateWithFingerprint.fingerprint.debugInfoUrl) {
|
||||
throw new Error(`Fingerprint source for update ${updateWithFingerprint.id} was not computed.`);
|
||||
}
|
||||
|
||||
return {
|
||||
fingerprint: await getFingerprintFromFingerprintFragmentAsync(
|
||||
updateWithFingerprint.fingerprint
|
||||
),
|
||||
platforms: [stringToAppPlatform(updateWithFingerprint.platform)],
|
||||
origin: {
|
||||
type: FingerprintOriginType.Update,
|
||||
update: updateWithFingerprint,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getFingerprintInfoFromHashAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
hash: string
|
||||
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
|
||||
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
hash
|
||||
);
|
||||
const fingerprint = await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment);
|
||||
let platforms;
|
||||
const fingerprintBuilds = fingerprintFragment.builds?.edges.map(edge => edge.node) ?? [];
|
||||
const fingerprintUpdates = fingerprintFragment.updates?.edges.map(edge => edge.node) ?? [];
|
||||
if (fingerprintBuilds.length > 0) {
|
||||
platforms = [fingerprintBuilds[0].platform];
|
||||
} else if (fingerprintUpdates.length > 0) {
|
||||
platforms = [stringToAppPlatform(fingerprintUpdates[0].platform)];
|
||||
}
|
||||
return {
|
||||
fingerprint,
|
||||
platforms,
|
||||
origin: {
|
||||
type: FingerprintOriginType.Hash,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
nonInteractive: boolean,
|
||||
updateGroupIdOrUpdateId: string
|
||||
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
|
||||
// Some people may pass in update group id instead of update id, so add interactive support for that
|
||||
try {
|
||||
const maybeUpdateGroupId = updateGroupIdOrUpdateId;
|
||||
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
|
||||
groupId: maybeUpdateGroupId,
|
||||
});
|
||||
if (updateGroup.length === 1) {
|
||||
const update = updateGroup[0];
|
||||
return await getFingerprintFromUpdateFragmentAsync(update);
|
||||
}
|
||||
if (nonInteractive) {
|
||||
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const updateUrl =
|
||||
getExpoWebsiteBaseUrl() +
|
||||
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/updates/${maybeUpdateGroupId}`;
|
||||
throw new Error(
|
||||
`Please pass in your update ID from ${updateUrl} or use interactive mode to select the update ID.`
|
||||
);
|
||||
}
|
||||
const update = await selectAsync<UpdateFragment>(
|
||||
'Select a platform to compute the fingerprint from',
|
||||
updateGroup.map(update => ({
|
||||
title: update.platform,
|
||||
value: update,
|
||||
}))
|
||||
);
|
||||
return await getFingerprintFromUpdateFragmentAsync(update);
|
||||
} catch (error: any) {
|
||||
if (!error?.message.includes('Could not find any updates with group ID')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const updateId = updateGroupIdOrUpdateId;
|
||||
const updateWithFingerprint = await UpdateQuery.viewByUpdateAsync(graphqlClient, {
|
||||
updateId,
|
||||
});
|
||||
return await getFingerprintFromUpdateFragmentAsync(updateWithFingerprint);
|
||||
}
|
||||
|
||||
async function getFingerprintInfoFromBuildIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
buildId: string
|
||||
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
|
||||
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
|
||||
if (!buildWithFingerprint.fingerprint) {
|
||||
throw new Error(`Fingerprint for build ${buildId} was not computed.`);
|
||||
} else if (!buildWithFingerprint.fingerprint.debugInfoUrl) {
|
||||
throw new Error(`Fingerprint source for build ${buildId} was not computed.`);
|
||||
}
|
||||
return {
|
||||
fingerprint: await getFingerprintFromFingerprintFragmentAsync(buildWithFingerprint.fingerprint),
|
||||
platforms: [buildWithFingerprint.platform],
|
||||
origin: {
|
||||
type: FingerprintOriginType.Build,
|
||||
build: buildWithFingerprint,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getFingerprintFragmentFromHashAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
hash: string
|
||||
): Promise<FingerprintFragment> {
|
||||
const fingerprint = await FingerprintQuery.byHashAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
hash,
|
||||
});
|
||||
if (!fingerprint) {
|
||||
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
throw new Error(`Fingerprint with hash ${hash} was not uploaded for ${displayName}.`);
|
||||
}
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
async function getFingerprintFromFingerprintFragmentAsync(
|
||||
fingerprintFragment: Pick<FingerprintFragment, 'debugInfoUrl' | 'hash'>
|
||||
): Promise<Fingerprint> {
|
||||
const fingerprintDebugUrl = fingerprintFragment.debugInfoUrl;
|
||||
if (!fingerprintDebugUrl) {
|
||||
throw new Error(
|
||||
`The source for fingerprint hash ${fingerprintFragment.hash} was not computed.`
|
||||
);
|
||||
}
|
||||
const fingerprintResponse = await fetch(fingerprintDebugUrl);
|
||||
return (await fingerprintResponse.json()) as Fingerprint;
|
||||
}
|
||||
|
||||
function printContentDiff(diff: FingerprintDiffItem): void {
|
||||
if (diff.op === 'added') {
|
||||
const sourceType = diff.addedSource.type;
|
||||
if (sourceType === 'contents') {
|
||||
printContentSource({
|
||||
op: diff.op,
|
||||
sourceType,
|
||||
contentsId: diff.addedSource.id,
|
||||
contentsAfter: diff.addedSource.contents,
|
||||
});
|
||||
}
|
||||
} else if (diff.op === 'removed') {
|
||||
const sourceType = diff.removedSource.type;
|
||||
if (sourceType === 'contents') {
|
||||
printContentSource({
|
||||
op: diff.op,
|
||||
sourceType,
|
||||
contentsId: diff.removedSource.id,
|
||||
contentsBefore: diff.removedSource.contents,
|
||||
});
|
||||
}
|
||||
} else if (diff.op === 'changed') {
|
||||
const sourceType = diff.beforeSource.type;
|
||||
if (sourceType === 'contents') {
|
||||
if (diff.afterSource.type !== 'contents') {
|
||||
throw new Error(
|
||||
`Changed fingerprint source types must be the same, received ${diff.beforeSource.type}, ${diff.afterSource.type}`
|
||||
);
|
||||
}
|
||||
printContentSource({
|
||||
op: diff.op,
|
||||
sourceType: diff.beforeSource.type, // before and after source types should be the same
|
||||
contentsId: diff.beforeSource.id, // before and after content ids should be the same
|
||||
contentsBefore: diff.beforeSource.contents,
|
||||
contentsAfter: diff.afterSource.contents,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDiffFilePathFields(diff: FingerprintDiffItem): FormatFieldsItem | null {
|
||||
if (diff.op === 'added') {
|
||||
const sourceType = diff.addedSource.type;
|
||||
if (sourceType !== 'contents') {
|
||||
return getFilePathSourceFields({
|
||||
op: diff.op,
|
||||
sourceType,
|
||||
filePath: diff.addedSource.filePath,
|
||||
});
|
||||
}
|
||||
} else if (diff.op === 'removed') {
|
||||
const sourceType = diff.removedSource.type;
|
||||
if (sourceType !== 'contents') {
|
||||
return getFilePathSourceFields({
|
||||
op: diff.op,
|
||||
sourceType,
|
||||
filePath: diff.removedSource.filePath,
|
||||
});
|
||||
}
|
||||
} else if (diff.op === 'changed') {
|
||||
const sourceType = diff.beforeSource.type;
|
||||
if (sourceType !== 'contents') {
|
||||
return getFilePathSourceFields({
|
||||
op: diff.op,
|
||||
sourceType: diff.beforeSource.type, // before and after source types should be the same
|
||||
filePath: diff.beforeSource.filePath, // before and after filePaths should be the same
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFilePathSourceFields({
|
||||
op,
|
||||
sourceType,
|
||||
filePath,
|
||||
}: {
|
||||
op: 'added' | 'removed' | 'changed';
|
||||
sourceType: 'dir' | 'file';
|
||||
filePath: string;
|
||||
}): FormatFieldsItem {
|
||||
if (sourceType === 'dir') {
|
||||
if (op === 'added') {
|
||||
return { label: 'new directory', value: filePath };
|
||||
} else if (op === 'removed') {
|
||||
return { label: 'removed directory', value: filePath };
|
||||
} else if (op === 'changed') {
|
||||
return { label: 'modified directory', value: filePath };
|
||||
}
|
||||
} else if (sourceType === 'file') {
|
||||
if (op === 'added') {
|
||||
return { label: 'new file', value: filePath };
|
||||
} else if (op === 'removed') {
|
||||
return { label: 'removed file', value: filePath };
|
||||
} else if (op === 'changed') {
|
||||
return { label: 'modified file', value: filePath };
|
||||
}
|
||||
}
|
||||
throw new Error(`Unsupported source and op: ${sourceType}, ${op}`);
|
||||
}
|
||||
|
||||
const PRETTY_CONTENT_ID: Record<string, string> = {
|
||||
'expoAutolinkingConfig:ios': 'Expo autolinking config (iOS)',
|
||||
'expoAutolinkingConfig:android': 'Expo autolinking config (Android)',
|
||||
'packageJson:scripts': 'package.json scripts',
|
||||
expoConfig: 'Expo app config',
|
||||
'package:react-native': 'React Native package.json',
|
||||
'rncoreAutolinkingConfig:ios': 'React Native Community autolinking config (iOS)',
|
||||
'rncoreAutolinkingConfig:android': 'React Native Community autolinking config (Android)',
|
||||
};
|
||||
|
||||
function printContentSource({
|
||||
op,
|
||||
contentsBefore,
|
||||
contentsAfter,
|
||||
contentsId,
|
||||
}: {
|
||||
op: 'added' | 'removed' | 'changed';
|
||||
sourceType: 'contents';
|
||||
contentsBefore?: string | Buffer;
|
||||
contentsAfter?: string | Buffer;
|
||||
contentsId: string;
|
||||
}): void {
|
||||
Log.newLine();
|
||||
const prettyContentId = PRETTY_CONTENT_ID[contentsId] ?? contentsId;
|
||||
if (op === 'added') {
|
||||
Log.log(`${chalk.dim('📝 New content')}: ${prettyContentId}`);
|
||||
} else if (op === 'removed') {
|
||||
Log.log(`${chalk.dim('📝 Removed content')}: ${prettyContentId}`);
|
||||
} else if (op === 'changed') {
|
||||
Log.log(`${chalk.dim('📝 Modified content')}: ${prettyContentId}`);
|
||||
}
|
||||
printContentsDiff(contentsBefore ?? '', contentsAfter ?? '');
|
||||
}
|
||||
|
||||
function printContentsDiff(contents1: string | Buffer, contents2: string | Buffer): void {
|
||||
const stringifiedContents1 = Buffer.isBuffer(contents1) ? contents1.toString() : contents1;
|
||||
const stringifiedContents2 = Buffer.isBuffer(contents2) ? contents2.toString() : contents2;
|
||||
|
||||
const isStr1JSON = isJSON(stringifiedContents1);
|
||||
const isStr2JSON = isJSON(stringifiedContents2);
|
||||
|
||||
const prettifiedContents1 = isStr1JSON
|
||||
? JSON.stringify(JSON.parse(stringifiedContents1), null, 2)
|
||||
: stringifiedContents1;
|
||||
const prettifiedContents2 = isStr2JSON
|
||||
? JSON.stringify(JSON.parse(stringifiedContents2), null, 2)
|
||||
: stringifiedContents2;
|
||||
|
||||
abridgedDiff(prettifiedContents1, prettifiedContents2, 0);
|
||||
}
|
||||
|
||||
function prettyPrintFingerprint(fingerprint: Fingerprint, origin: FingerprintOrigin): string {
|
||||
if (origin.type === FingerprintOriginType.Project) {
|
||||
return `fingerprint ${fingerprint.hash} from local directory`;
|
||||
} else if (origin.type === FingerprintOriginType.Update) {
|
||||
return `fingerprint ${fingerprint.hash} from ${
|
||||
origin.update?.platform ? stringToAppPlatform(origin.update?.platform) : ''
|
||||
} ${origin.type}`;
|
||||
} else if (origin.type === FingerprintOriginType.Build) {
|
||||
return `fingerprint ${fingerprint.hash} from ${origin.build?.platform} ${origin.type}`;
|
||||
}
|
||||
return `fingerprint ${fingerprint.hash}`;
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectBuildToCompareAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
projectDisplayName: string,
|
||||
{
|
||||
filters,
|
||||
}: {
|
||||
filters?: {
|
||||
statuses?: BuildStatus[];
|
||||
platform?: RequestedPlatform;
|
||||
profile?: string;
|
||||
hasFingerprint?: boolean;
|
||||
};
|
||||
} = {}
|
||||
): Promise<string | null> {
|
||||
const spinner = ora().start('Fetching builds…');
|
||||
|
||||
let builds;
|
||||
try {
|
||||
builds = await fetchBuildsAsync({ graphqlClient, projectId, filters });
|
||||
spinner.stop();
|
||||
} catch (error) {
|
||||
spinner.fail(
|
||||
`Something went wrong and we couldn't fetch the builds for the project ${projectDisplayName}.`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
if (builds.length === 0) {
|
||||
Log.warn(`No fingerprints have been computed for builds of project ${projectDisplayName}.`);
|
||||
return null;
|
||||
} else {
|
||||
const build = await selectAsync<string>(
|
||||
'Which build do you want to compare?',
|
||||
builds.map(build => ({
|
||||
title: formatBuild(build),
|
||||
value: build.id,
|
||||
}))
|
||||
);
|
||||
|
||||
return build;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
appPlatformToPlatform,
|
||||
getFingerprintInfoFromLocalProjectForPlatformsAsync,
|
||||
stringToAppPlatform,
|
||||
} from '../../fingerprint/utils';
|
||||
import { AppPlatform, EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getProfilesAsync } from '../../utils/profiles';
|
||||
|
||||
export default class FingerprintGenerate extends EasCommand {
|
||||
static override description = 'generate fingerprints from the current project';
|
||||
static override strict = false;
|
||||
|
||||
static override examples = [
|
||||
'$ eas fingerprint:generate \t # Generate fingerprint in interactive mode',
|
||||
'$ eas fingerprint:generate --build-profile preview \t # Generate a fingerprint using the "preview" build profile',
|
||||
'$ eas fingerprint:generate --environment preview \t # Generate a fingerprint using the "preview" environment',
|
||||
'$ eas fingerprint:generate --json --non-interactive --platform android \t # Output fingerprint json to stdout',
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
...EasEnvironmentFlagParameters,
|
||||
exclusive: ['build-profile'],
|
||||
}),
|
||||
'build-profile': Flags.string({
|
||||
char: 'e',
|
||||
description: 'Name of the build profile from eas.json.',
|
||||
exclusive: ['environment'],
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
...this.ContextOptions.ServerSideEnvironmentVariables,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(FingerprintGenerate);
|
||||
const {
|
||||
json,
|
||||
'non-interactive': nonInteractive,
|
||||
platform: platformStringFlag,
|
||||
environment,
|
||||
'build-profile': buildProfileName,
|
||||
} = flags;
|
||||
|
||||
const {
|
||||
projectId,
|
||||
privateProjectConfig: { projectDir },
|
||||
loggedIn: { graphqlClient },
|
||||
vcsClient,
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
} = await this.getContextAsync(FingerprintGenerate, {
|
||||
nonInteractive,
|
||||
withServerSideEnvironment: environment ?? null,
|
||||
});
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
let platform: AppPlatform;
|
||||
if (platformStringFlag) {
|
||||
platform = stringToAppPlatform(platformStringFlag);
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Platform must be specified in non-interactive mode with the --p flag');
|
||||
}
|
||||
platform = await selectRequestedPlatformAsync();
|
||||
}
|
||||
|
||||
let env: Env | undefined = undefined;
|
||||
if (environment) {
|
||||
Log.log(`🔧 Using environment: ${environment}`);
|
||||
env = { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' };
|
||||
} else if (buildProfileName) {
|
||||
Log.log(`🔧 Using build profile: ${buildProfileName}`);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const buildProfile = (
|
||||
await getProfilesAsync({
|
||||
type: 'build',
|
||||
easJsonAccessor,
|
||||
platforms: [appPlatformToPlatform(platform)],
|
||||
profileName: buildProfileName ?? undefined,
|
||||
projectDir,
|
||||
})
|
||||
)[0];
|
||||
if (!buildProfile) {
|
||||
throw new Error(`Build profile ${buildProfile} not found for platform: ${platform}`);
|
||||
}
|
||||
const configResult = await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
});
|
||||
env = configResult.env;
|
||||
}
|
||||
|
||||
const fingerprint = await getFingerprintInfoFromLocalProjectForPlatformsAsync(
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
vcsClient,
|
||||
[platform],
|
||||
{ env }
|
||||
);
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(fingerprint);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.log(`✅ Fingerprint generated: ${fingerprint.hash}`);
|
||||
|
||||
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const fingerprintUrl = new URL(
|
||||
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprint.hash}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
);
|
||||
Log.log(`🔍 View the fingerprint at ${link(fingerprintUrl.toString())}`);
|
||||
Log.log(`💡 If you want to see the entire fingerprint output, pass in the --json flag.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function selectRequestedPlatformAsync(): Promise<AppPlatform> {
|
||||
const { requestedPlatform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Select platform',
|
||||
name: 'requestedPlatform',
|
||||
choices: [
|
||||
{ title: 'Android', value: AppPlatform.Android },
|
||||
{ title: 'iOS', value: AppPlatform.Ios },
|
||||
],
|
||||
});
|
||||
return requestedPlatform;
|
||||
}
|
||||
@@ -8,14 +8,13 @@ import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { AppPrivacy, Role } from '../../graphql/generated';
|
||||
import { Role } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { toAppPrivacy } from '../../project/projectUtils';
|
||||
import { Choice, confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
@@ -337,7 +336,6 @@ export default class ProjectInit extends EasCommand {
|
||||
createdProjectId = await AppMutation.createAppAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
projectName,
|
||||
privacy: toAppPrivacy(exp.privacy) ?? AppPrivacy.Public,
|
||||
});
|
||||
spinner.succeed(`Created ${chalk.bold(projectLink)}`);
|
||||
} catch (err) {
|
||||
|
||||
@@ -162,7 +162,10 @@ export default class Onboarding extends EasCommand {
|
||||
cloneMethod,
|
||||
});
|
||||
|
||||
const vcsClient = new GitClient(finalTargetProjectDirectory);
|
||||
const vcsClient = new GitClient({
|
||||
maybeCwdOverride: finalTargetProjectDirectory,
|
||||
requireCommit: false,
|
||||
});
|
||||
if (!app.githubRepository) {
|
||||
await fs.remove(path.join(finalTargetProjectDirectory, '.git'));
|
||||
await runCommandAsync({
|
||||
@@ -285,12 +288,12 @@ export default class Onboarding extends EasCommand {
|
||||
} else {
|
||||
Log.log('🚀 Now we are going to trigger your first build');
|
||||
Log.log();
|
||||
const { buildIds } = await runBuildAndSubmitAsync(
|
||||
const { buildIds } = await runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
finalTargetProjectDirectory,
|
||||
{
|
||||
projectDir: finalTargetProjectDirectory,
|
||||
flags: {
|
||||
nonInteractive: true,
|
||||
requestedPlatform:
|
||||
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
|
||||
@@ -307,8 +310,9 @@ export default class Onboarding extends EasCommand {
|
||||
repack: true,
|
||||
},
|
||||
actor,
|
||||
getDynamicProjectConfigFn
|
||||
);
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
getDynamicPrivateProjectConfigAsync: getDynamicProjectConfigFn,
|
||||
});
|
||||
const buildId = buildIds[0];
|
||||
Log.log();
|
||||
Log.log('🚀 You can now go back to the website to continue:');
|
||||
|
||||
@@ -25,6 +25,7 @@ import { promptAsync, selectAsync } from '../../prompts';
|
||||
export default class EnvironmentSecretCreate extends EasCommand {
|
||||
static override description =
|
||||
'create an environment secret on the current project or owner account';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
scope: Flags.enum({
|
||||
@@ -55,6 +56,9 @@ export default class EnvironmentSecretCreate extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('This command is deprecated. Use eas env:create instead.');
|
||||
Log.newLine();
|
||||
|
||||
let {
|
||||
flags: {
|
||||
name,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class EnvironmentSecretDelete extends EasCommand {
|
||||
static override description = 'delete an environment secret by ID';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
@@ -27,6 +28,9 @@ export default class EnvironmentSecretDelete extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('This command is deprecated. Use eas env:delete instead.');
|
||||
Log.newLine();
|
||||
|
||||
let {
|
||||
flags: { id, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentSecretDelete);
|
||||
|
||||
@@ -11,6 +11,7 @@ import formatFields from '../../utils/formatFields';
|
||||
|
||||
export default class EnvironmentSecretList extends EasCommand {
|
||||
static override description = 'list environment secrets available for your current app';
|
||||
static override hidden = true;
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
@@ -18,6 +19,9 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('This command is deprecated. Use eas env:list instead.');
|
||||
Log.newLine();
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -24,6 +24,7 @@ import intersection from '../../utils/expodash/intersection';
|
||||
|
||||
export default class EnvironmentSecretPush extends EasCommand {
|
||||
static override description = 'read environment secrets from env file and store on the server';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
scope: Flags.enum({
|
||||
@@ -47,6 +48,9 @@ export default class EnvironmentSecretPush extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('This command is deprecated. Use eas env:push instead.');
|
||||
Log.newLine();
|
||||
|
||||
const {
|
||||
flags: { scope, force, 'env-file': maybeEnvFilePath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentSecretPush);
|
||||
|
||||
@@ -18,8 +18,7 @@ import AndroidSubmitCommand from '../../submit/android/AndroidSubmitCommand';
|
||||
import { SubmissionContext, createSubmissionContextAsync } from '../../submit/context';
|
||||
import IosSubmitCommand from '../../submit/ios/IosSubmitCommand';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
|
||||
import NoVcsClient from '../../vcs/clients/noVcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
|
||||
/**
|
||||
* This command will be run on the EAS workers.
|
||||
@@ -65,10 +64,16 @@ export default class SubmitInternal extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(SubmitInternal, {
|
||||
nonInteractive: true,
|
||||
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
if (vcsClient instanceof GitClient) {
|
||||
// `build:internal` is run on EAS workers and the repo may have been changed
|
||||
// by pre-install hooks or other scripts. We don't want to require committing changes
|
||||
// to continue the build.
|
||||
vcsClient.requireCommit = false;
|
||||
}
|
||||
|
||||
const submissionProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
EasJsonAccessor.fromProjectPath(projectDir),
|
||||
flags.platform,
|
||||
|
||||
@@ -88,7 +88,10 @@ describe(UpdatePublish.name, () => {
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
@@ -111,7 +114,10 @@ describe(UpdatePublish.name, () => {
|
||||
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
|
||||
.mockResolvedValue({ branchId: 'branch123', branchName: 'branchFromChannel' });
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
@@ -146,7 +152,10 @@ describe(UpdatePublish.name, () => {
|
||||
|
||||
// Mock an existing branch, so we don't create a new one
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AppJSONConfig, ExpoConfig, PackageJSONConfig, getConfig } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
@@ -82,16 +81,23 @@ describe(UpdateRollBackToEmbedded.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with --non-interactive, --branch, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
it('creates a roll back to embedded with --non-interactive, --branch, --message, and --runtime-version', async () => {
|
||||
const flags = [
|
||||
'--non-interactive',
|
||||
'--branch=branch123',
|
||||
'--message=abc',
|
||||
'--runtime-version=exposdk:47.0.0',
|
||||
];
|
||||
|
||||
mockTestProject();
|
||||
const platforms = ['android', 'ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
@@ -104,19 +110,26 @@ describe(UpdateRollBackToEmbedded.name, () => {
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with --non-interactive, --channel, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
|
||||
it('creates a roll back to embedded with --non-interactive, --channel, --message, and --runtime-version', async () => {
|
||||
const flags = [
|
||||
'--non-interactive',
|
||||
'--channel=channel123',
|
||||
'--message=abc',
|
||||
'--runtime-version=exposdk:47.0.0',
|
||||
];
|
||||
|
||||
const { projectId } = mockTestProject();
|
||||
const platforms = ['android', 'ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
|
||||
|
||||
jest
|
||||
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
|
||||
.mockResolvedValue({ branchId: updateStub.branch.id, branchName: 'branchFromChannel' });
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
@@ -142,18 +155,25 @@ describe(UpdateRollBackToEmbedded.name, () => {
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
const flags = [
|
||||
'--non-interactive',
|
||||
'--branch=branch123',
|
||||
'--message=abc',
|
||||
'--runtime-version=exposdk:47.0.0',
|
||||
];
|
||||
|
||||
// Add configuration to the project that should not be included in the update
|
||||
mockTestProject();
|
||||
|
||||
const platforms = ['ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
|
||||
|
||||
// Mock an existing branch, so we don't create a new one
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export default class UpdateConfigure extends EasCommand {
|
||||
platform: flags['platform'],
|
||||
vcsClient,
|
||||
env: undefined,
|
||||
forceNativeConfigSync: true,
|
||||
});
|
||||
|
||||
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
|
||||
|
||||
@@ -6,37 +6,43 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateGroupMutation,
|
||||
UpdateMutationDeleteUpdateGroupArgs,
|
||||
BackgroundJobReceiptDataFragment,
|
||||
ScheduleUpdateGroupDeletionMutation,
|
||||
ScheduleUpdateGroupDeletionMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BackgroundJobReceiptNode } from '../../graphql/types/BackgroundJobReceipt';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
|
||||
|
||||
async function deleteUpdateGroupAsync(
|
||||
async function scheduleUpdateGroupDeletionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
group,
|
||||
}: {
|
||||
group: string;
|
||||
}
|
||||
): Promise<DeleteUpdateGroupMutation> {
|
||||
return await withErrorHandlingAsync(
|
||||
): Promise<BackgroundJobReceiptDataFragment> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateGroupMutation, UpdateMutationDeleteUpdateGroupArgs>(
|
||||
.mutation<ScheduleUpdateGroupDeletionMutation, ScheduleUpdateGroupDeletionMutationVariables>(
|
||||
gql`
|
||||
mutation DeleteUpdateGroup($group: ID!) {
|
||||
mutation ScheduleUpdateGroupDeletion($group: ID!) {
|
||||
update {
|
||||
deleteUpdateGroup(group: $group) {
|
||||
group
|
||||
scheduleUpdateGroupDeletion(group: $group) {
|
||||
id
|
||||
...BackgroundJobReceiptData
|
||||
}
|
||||
}
|
||||
}
|
||||
${BackgroundJobReceiptNode}
|
||||
`,
|
||||
{ group }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return result.update.scheduleUpdateGroupDeletion;
|
||||
}
|
||||
|
||||
export default class UpdateDelete extends EasCommand {
|
||||
@@ -90,7 +96,9 @@ export default class UpdateDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUpdateGroupAsync(graphqlClient, { group });
|
||||
const receipt = await scheduleUpdateGroupDeletionAsync(graphqlClient, { group });
|
||||
const successfulReceipt = await pollForBackgroundJobReceiptAsync(graphqlClient, receipt);
|
||||
Log.debug('Deletion result', { successfulReceipt });
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput({ group });
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { transformFingerprintSource } from '../../build/graphql';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
@@ -13,6 +14,7 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
FingerprintInfoGroup as GraphqlFingerprintInfoGroup,
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
UpdateInfoGroup,
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
UpdateRolloutInfoGroup,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
@@ -32,6 +35,7 @@ import {
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
filterCollectedAssetsByRequestedPlatforms,
|
||||
findCompatibleBuildsAsync,
|
||||
generateEasMetadataAsync,
|
||||
getBranchNameForCommandAsync,
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
|
||||
@@ -39,6 +43,7 @@ import {
|
||||
getRuntimeVersionInfoObjectsAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
isUploadedAssetCountAboveWarningThreshold,
|
||||
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync,
|
||||
platformDisplayNames,
|
||||
resolveInputDirectoryAsync,
|
||||
uploadAssetsAsync,
|
||||
@@ -235,6 +240,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
jsonFlag,
|
||||
});
|
||||
|
||||
const maybeServerEnv = environment
|
||||
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
|
||||
: {};
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
const bundleSpinner = ora().start('Exporting...');
|
||||
@@ -245,11 +254,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
exp,
|
||||
platformFlag: requestedPlatform,
|
||||
clearCache,
|
||||
extraEnv: {
|
||||
...(environment
|
||||
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
|
||||
: {}),
|
||||
},
|
||||
extraEnv: maybeServerEnv,
|
||||
});
|
||||
bundleSpinner.succeed('Exported bundle(s)');
|
||||
} catch (e) {
|
||||
@@ -391,34 +396,45 @@ export default class UpdatePublish extends EasCommand {
|
||||
...workflows,
|
||||
web: Workflow.UNKNOWN,
|
||||
},
|
||||
env: undefined,
|
||||
env: maybeServerEnv,
|
||||
});
|
||||
const runtimeToPlatformsAndFingerprintInfoMapping =
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
|
||||
runtimeVersionInfoObjects
|
||||
);
|
||||
|
||||
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
const { branch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchName,
|
||||
});
|
||||
|
||||
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping = await Promise.all(
|
||||
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
|
||||
return {
|
||||
...info,
|
||||
fingerprintSource: info.fingerprint
|
||||
? (
|
||||
await maybeUploadFingerprintAsync({
|
||||
hash: info.runtimeVersion,
|
||||
fingerprint: info.fingerprint,
|
||||
graphqlClient,
|
||||
})
|
||||
).fingerprintSource ?? null
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMappingFromExpoUpdates =
|
||||
await Promise.all(
|
||||
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
|
||||
return {
|
||||
...info,
|
||||
expoUpdatesRuntimeFingerprintSource: info.expoUpdatesRuntimeFingerprint
|
||||
? (
|
||||
await maybeUploadFingerprintAsync({
|
||||
hash: info.runtimeVersion,
|
||||
fingerprint: info.expoUpdatesRuntimeFingerprint,
|
||||
graphqlClient,
|
||||
})
|
||||
).fingerprintSource ?? null
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping =
|
||||
await maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync({
|
||||
projectDir,
|
||||
graphqlClient,
|
||||
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping:
|
||||
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMappingFromExpoUpdates,
|
||||
workflowsByPlatform: workflows,
|
||||
env: undefined,
|
||||
});
|
||||
|
||||
const runtimeVersionToRolloutInfoGroup =
|
||||
rolloutPercentage !== undefined
|
||||
@@ -436,7 +452,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups: PublishUpdateGroupInput[] =
|
||||
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(
|
||||
({ runtimeVersion, platforms, fingerprintSource }) => {
|
||||
({ runtimeVersion, platforms, fingerprintInfoGroup }) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
@@ -455,19 +471,30 @@ export default class UpdatePublish extends EasCommand {
|
||||
])
|
||||
)
|
||||
: null;
|
||||
const transformedFingerprintInfoGroup = Object.entries(fingerprintInfoGroup).reduce(
|
||||
(prev, [platform, fingerprintInfo]) => {
|
||||
return {
|
||||
...prev,
|
||||
[platform]: {
|
||||
...fingerprintInfo,
|
||||
fingerprintSource: transformFingerprintSource(fingerprintInfo.fingerprintSource),
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as GraphqlFingerprintInfoGroup
|
||||
);
|
||||
|
||||
return {
|
||||
branchId,
|
||||
branchId: branch.id,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
rolloutInfoGroup: localRolloutInfoGroup,
|
||||
runtimeFingerprintSource: fingerprintSource
|
||||
? transformFingerprintSource(fingerprintSource)
|
||||
: null,
|
||||
fingerprintInfoGroup: transformedFingerprintInfoGroup,
|
||||
runtimeVersion,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
environment: environment ?? null,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -535,95 +562,143 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
for (const runtime of uniqBy(
|
||||
runtimeToPlatformsAndFingerprintInfoMapping,
|
||||
version => version.runtimeVersion
|
||||
)) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime.runtimeVersion
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
const runtimeToCompatibleBuilds = await Promise.all(
|
||||
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
|
||||
findCompatibleBuildsAsync(graphqlClient, projectId, obj)
|
||||
)
|
||||
);
|
||||
|
||||
for (const runtime of uniqBy(
|
||||
runtimeToPlatformsAndFingerprintInfoMapping,
|
||||
version => version.runtimeVersion
|
||||
)) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime.runtimeVersion
|
||||
);
|
||||
if (newUpdatesForRuntimeVersion.length === 0) {
|
||||
throw new Error(
|
||||
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
|
||||
);
|
||||
if (newUpdatesForRuntimeVersion.length === 0) {
|
||||
throw new Error(
|
||||
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
|
||||
);
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
update => update.platform === 'android'
|
||||
);
|
||||
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
|
||||
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
update => update.platform === 'android'
|
||||
);
|
||||
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
|
||||
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
|
||||
|
||||
const projectName = exp.slug;
|
||||
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
|
||||
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
|
||||
const updateGroupLink = link(updateGroupUrl, { dim: false });
|
||||
const projectName = exp.slug;
|
||||
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
|
||||
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
|
||||
const updateGroupLink = link(updateGroupUrl, { dim: false });
|
||||
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime.runtimeVersion },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate
|
||||
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
...(newAndroidUpdate?.rolloutControlUpdate
|
||||
? [
|
||||
{
|
||||
label: 'Android Rollout',
|
||||
value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(newIosUpdate?.rolloutControlUpdate
|
||||
? [
|
||||
{
|
||||
label: 'iOS Rollout',
|
||||
value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Message', value: updateMessage ?? '' },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'EAS Dashboard', value: updateGroupLink },
|
||||
])
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime.runtimeVersion },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate ? [{ label: 'Android update ID', value: newAndroidUpdate.id }] : []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
...(newAndroidUpdate?.rolloutControlUpdate
|
||||
? [
|
||||
{
|
||||
label: 'Android Rollout',
|
||||
value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(newIosUpdate?.rolloutControlUpdate
|
||||
? [
|
||||
{
|
||||
label: 'iOS Rollout',
|
||||
value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Message', value: updateMessage ?? '' },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'EAS Dashboard', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
if (isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)) {
|
||||
Log.warn(
|
||||
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
|
||||
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
|
||||
learnMoreMessage: 'Consider optimizing your usage of assets',
|
||||
dim: false,
|
||||
})}.`
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
if (
|
||||
isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)
|
||||
) {
|
||||
Log.warn(
|
||||
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
|
||||
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
|
||||
learnMoreMessage: 'Consider optimizing your usage of assets',
|
||||
dim: false,
|
||||
})}.`
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
|
||||
const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
|
||||
({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
|
||||
)?.fingerprintInfoGroupWithCompatibleBuilds;
|
||||
if (fingerprintsWithoutCompatibleBuilds) {
|
||||
const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
|
||||
([_platform, fingerprintInfo]) => !fingerprintInfo.build
|
||||
);
|
||||
if (missingBuilds.length > 0) {
|
||||
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
Log.warn('No compatible builds found for the following fingerprints:');
|
||||
for (const [platform, fingerprintInfo] of missingBuilds) {
|
||||
const fingerprintUrl = new URL(
|
||||
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
);
|
||||
Log.warn(
|
||||
formatFields(
|
||||
[
|
||||
{
|
||||
label: `${this.prettyPlatform(platform)} fingerprint`,
|
||||
value: fingerprintInfo.fingerprintHash,
|
||||
},
|
||||
{ label: 'URL', value: fingerprintUrl.toString() },
|
||||
],
|
||||
{
|
||||
labelFormat: label => ` ${chalk.dim(label)}:`,
|
||||
}
|
||||
)
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private prettyPlatform(updatePlatform: string): string {
|
||||
switch (updatePlatform) {
|
||||
case 'android':
|
||||
return 'Android';
|
||||
case 'ios':
|
||||
return 'iOS';
|
||||
default:
|
||||
return updatePlatform;
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Platform as PublishPlatform } from '@expo/config';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -12,10 +11,12 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
PublishUpdateGroupInput,
|
||||
RuntimeFragment,
|
||||
StatuspageServiceName,
|
||||
UpdatePublishMutation,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { RuntimeQuery } from '../../graphql/queries/RuntimeQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
@@ -29,10 +30,8 @@ import {
|
||||
defaultPublishPlatforms,
|
||||
getBranchNameForCommandAsync,
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
|
||||
getRuntimeVersionInfoObjectsAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
|
||||
import {
|
||||
@@ -45,12 +44,13 @@ import {
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { Connection, QueryParams, selectPaginatedAsync } from '../../utils/relay';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
channel?: string;
|
||||
'runtime-version'?: string;
|
||||
message?: string;
|
||||
platform: string;
|
||||
'private-key-path'?: string;
|
||||
@@ -59,10 +59,10 @@ type RawUpdateFlags = {
|
||||
};
|
||||
|
||||
type UpdateFlags = {
|
||||
auto: boolean;
|
||||
platform: RequestedPlatform;
|
||||
branchName?: string;
|
||||
channelName?: string;
|
||||
runtimeVersion?: string;
|
||||
updateMessage?: string;
|
||||
privateKeyPath?: string;
|
||||
json: boolean;
|
||||
@@ -81,6 +81,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
description: 'Channel that the published rollback to embedded update should affect',
|
||||
required: false,
|
||||
}),
|
||||
'runtime-version': Flags.string({
|
||||
description: 'Runtime version that the rollback to embedded update should target',
|
||||
required: false,
|
||||
}),
|
||||
message: Flags.string({
|
||||
description: 'A short message describing the rollback to embedded update',
|
||||
required: false,
|
||||
@@ -95,11 +99,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
auto: Flags.boolean({
|
||||
description:
|
||||
'Use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
'private-key-path': Flags.string({
|
||||
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory. Only relevant if you are using code signing: https://docs.expo.dev/eas-update/code-signing/`,
|
||||
required: false,
|
||||
@@ -117,10 +116,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
const { flags: rawFlags } = await this.parse(UpdateRollBackToEmbedded);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
const {
|
||||
auto: autoFlag,
|
||||
platform: platformFlag,
|
||||
channelName: channelNameArg,
|
||||
updateMessage: updateMessageArg,
|
||||
runtimeVersion: runtimeVersionArg,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
@@ -171,14 +170,14 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
autoFlag: false,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
const updateMessage = await getUpdateMessageForCommandAsync(vcsClient, {
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
autoFlag: false,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
});
|
||||
@@ -186,28 +185,33 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
const realizedPlatforms: UpdatePublishPlatform[] =
|
||||
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
|
||||
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
const { branch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchName,
|
||||
});
|
||||
|
||||
const gitCommitHash = await vcsClient.getCommitHashAsync();
|
||||
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
|
||||
const selectedRuntime =
|
||||
runtimeVersionArg ??
|
||||
(
|
||||
await UpdateRollBackToEmbedded.selectRuntimeAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchName,
|
||||
})
|
||||
)?.version;
|
||||
if (!selectedRuntime) {
|
||||
Errors.error('Must select a runtime or provide the --runtimeVersion flag', { exit: 1 });
|
||||
}
|
||||
|
||||
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
|
||||
const runtimeVersionInfoObjects = await getRuntimeVersionInfoObjectsAsync({
|
||||
exp,
|
||||
platforms: realizedPlatforms,
|
||||
projectDir,
|
||||
workflows: {
|
||||
...workflows,
|
||||
web: Workflow.UNKNOWN,
|
||||
},
|
||||
env: undefined,
|
||||
});
|
||||
const runtimeToPlatformsAndFingerprintInfoMapping =
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
|
||||
runtimeVersionInfoObjects
|
||||
realizedPlatforms.map(platform => ({
|
||||
platform,
|
||||
runtimeVersionInfo: {
|
||||
runtimeVersion: selectedRuntime,
|
||||
expoUpdatesRuntimeFingerprint: null,
|
||||
expoUpdatesRuntimeFingerprintHash: null,
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
|
||||
@@ -215,10 +219,8 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
try {
|
||||
newUpdates = await this.publishRollbacksAsync({
|
||||
graphqlClient,
|
||||
isGitWorkingTreeDirty,
|
||||
gitCommitHash,
|
||||
updateMessage,
|
||||
branchId,
|
||||
branchId: branch.id,
|
||||
codeSigningInfo,
|
||||
runtimeToPlatformsAndFingerprintInfoMapping,
|
||||
realizedPlatforms,
|
||||
@@ -232,13 +234,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
for (const runtime of uniqBy(
|
||||
@@ -276,14 +271,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: updateMessage ?? '' },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'EAS Dashboard', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
@@ -294,8 +281,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
|
||||
private async publishRollbacksAsync({
|
||||
graphqlClient,
|
||||
isGitWorkingTreeDirty,
|
||||
gitCommitHash,
|
||||
updateMessage,
|
||||
branchId,
|
||||
codeSigningInfo,
|
||||
@@ -303,8 +288,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
realizedPlatforms,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
isGitWorkingTreeDirty: boolean | undefined;
|
||||
gitCommitHash: string | undefined;
|
||||
updateMessage: string | undefined;
|
||||
branchId: string;
|
||||
codeSigningInfo: CodeSigningInfo | undefined;
|
||||
@@ -329,8 +312,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
|
||||
runtimeVersion,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
@@ -381,22 +362,63 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
return newUpdates;
|
||||
}
|
||||
|
||||
private static async selectRuntimeAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchName,
|
||||
batchSize = 5,
|
||||
}: {
|
||||
appId: string;
|
||||
branchName: string;
|
||||
batchSize?: number;
|
||||
}
|
||||
): Promise<RuntimeFragment | null> {
|
||||
const queryAsync = async (queryParams: QueryParams): Promise<Connection<RuntimeFragment>> => {
|
||||
return await RuntimeQuery.getRuntimesOnBranchAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
first: queryParams.first,
|
||||
after: queryParams.after,
|
||||
last: queryParams.last,
|
||||
before: queryParams.before,
|
||||
});
|
||||
};
|
||||
const getTitleAsync = async (runtime: RuntimeFragment): Promise<string> => {
|
||||
return runtime.version;
|
||||
};
|
||||
return await selectPaginatedAsync({
|
||||
queryAsync,
|
||||
getTitleAsync,
|
||||
printedType: 'target runtime',
|
||||
pageSize: batchSize,
|
||||
});
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
|
||||
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
|
||||
const {
|
||||
branch: branchName,
|
||||
channel: channelName,
|
||||
message: updateMessage,
|
||||
'runtime-version': runtimeVersion,
|
||||
} = flags;
|
||||
if (nonInteractive && !(updateMessage && (branchName || channelName))) {
|
||||
Errors.error(
|
||||
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
|
||||
'--branch and --message, or --channel and --message are required in non-interactive mode',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
if (nonInteractive && !runtimeVersion) {
|
||||
Errors.error('--runtimeVersion is required in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
|
||||
return {
|
||||
auto,
|
||||
branchName,
|
||||
channelName,
|
||||
updateMessage,
|
||||
runtimeVersion,
|
||||
platform: flags.platform as RequestedPlatform,
|
||||
privateKeyPath: flags['private-key-path'],
|
||||
nonInteractive,
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import fg from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
import path from 'path';
|
||||
import tar from 'tar';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getBuildLogsUrl } from '../build/utils/url';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag } from '../commandUtils/flags';
|
||||
import {
|
||||
DistributionType,
|
||||
LocalBuildArchiveSourceType,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { FingerprintMutation } from '../graphql/mutations/FingerprintMutation';
|
||||
import { LocalBuildMutation } from '../graphql/mutations/LocalBuildMutation';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import * as xcode from '../run/ios/xcode';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { getTmpDirectory } from '../utils/paths';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
|
||||
export default class BuildUpload extends EasCommand {
|
||||
static override description = 'upload a local build and generate a sharable link';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
|
||||
char: 'p',
|
||||
options: [Platform.IOS, Platform.ANDROID],
|
||||
}),
|
||||
'build-path': Flags.string({
|
||||
description: 'Path for the local build',
|
||||
}),
|
||||
fingerprint: Flags.string({
|
||||
description: 'Fingerprint hash of the local build',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildUpload);
|
||||
const { 'build-path': buildPath, fingerprint: manualFingerprintHash } = flags;
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BuildUpload, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const platform = await this.selectPlatformAsync(flags.platform);
|
||||
const localBuildPath = await resolveLocalBuildPathAsync(platform, buildPath);
|
||||
|
||||
const {
|
||||
fingerprintHash: buildFingerprintHash,
|
||||
developmentClient,
|
||||
simulator,
|
||||
} = await extractAppMetadataAsync(localBuildPath, platform);
|
||||
|
||||
let fingerprint = manualFingerprintHash ?? buildFingerprintHash;
|
||||
if (fingerprint) {
|
||||
if (
|
||||
manualFingerprintHash &&
|
||||
buildFingerprintHash &&
|
||||
manualFingerprintHash !== buildFingerprintHash
|
||||
) {
|
||||
const selectedAnswer = await promptAsync({
|
||||
name: 'fingerprint',
|
||||
message: `The provided fingerprint hash ${manualFingerprintHash} does not match the fingerprint hash of the build ${buildFingerprintHash}. Which fingerprint do you want to use?`,
|
||||
type: 'select',
|
||||
choices: [
|
||||
{ title: manualFingerprintHash, value: manualFingerprintHash },
|
||||
{ title: buildFingerprintHash, value: buildFingerprintHash },
|
||||
],
|
||||
});
|
||||
fingerprint = String(selectedAnswer.fingerprint);
|
||||
}
|
||||
|
||||
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
|
||||
hash: fingerprint,
|
||||
});
|
||||
}
|
||||
|
||||
Log.log(`Using build ${localBuildPath}`);
|
||||
Log.log(`Fingerprint hash: ${fingerprint ?? 'Unknown'}`);
|
||||
|
||||
Log.log('Uploading your app archive to EAS');
|
||||
const bucketKey = await uploadAppArchiveAsync(graphqlClient, localBuildPath);
|
||||
|
||||
const build = await LocalBuildMutation.createLocalBuildAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
{ platform: toAppPlatform(platform), simulator },
|
||||
{ type: LocalBuildArchiveSourceType.Gcs, bucketKey },
|
||||
{ distribution: DistributionType.Internal, fingerprintHash: fingerprint, developmentClient }
|
||||
);
|
||||
|
||||
Log.withTick(`Here is a sharable link of your build: ${getBuildLogsUrl(build)}`);
|
||||
}
|
||||
|
||||
private async selectPlatformAsync(platform?: Platform): Promise<Platform> {
|
||||
if (platform) {
|
||||
return platform;
|
||||
}
|
||||
const { resolvedPlatform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Select platform',
|
||||
name: 'resolvedPlatform',
|
||||
choices: [
|
||||
{ title: 'Android', value: Platform.ANDROID },
|
||||
{ title: 'iOS', value: Platform.IOS },
|
||||
],
|
||||
});
|
||||
return resolvedPlatform;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveLocalBuildPathAsync(
|
||||
platform: Platform,
|
||||
inputBuildPath?: string
|
||||
): Promise<string> {
|
||||
const rootDir = process.cwd();
|
||||
let applicationArchivePatternOrPath: string[] = [];
|
||||
|
||||
if (inputBuildPath) {
|
||||
applicationArchivePatternOrPath.push(inputBuildPath);
|
||||
} else if (platform === Platform.ANDROID) {
|
||||
applicationArchivePatternOrPath.push('android/app/build/outputs/**/*.{apk,aab}');
|
||||
} else {
|
||||
const xcworkspacePath = await xcode.resolveXcodeProjectAsync(rootDir);
|
||||
const schemes = IOSConfig.BuildScheme.getRunnableSchemesFromXcodeproj(rootDir);
|
||||
if (xcworkspacePath && schemes.length > 0) {
|
||||
for (const scheme of schemes) {
|
||||
const buildSettings = await xcode.getXcodeBuildSettingsAsync(xcworkspacePath, scheme.name);
|
||||
applicationArchivePatternOrPath = applicationArchivePatternOrPath.concat(
|
||||
buildSettings.map(({ buildSettings }) => `${buildSettings.BUILD_DIR}/**/*.app`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let applicationArchives = await findArtifactsAsync({
|
||||
rootDir,
|
||||
patternOrPathArray: applicationArchivePatternOrPath,
|
||||
});
|
||||
|
||||
if (applicationArchives.length === 0 && !inputBuildPath) {
|
||||
Log.warn(`No application archives found at ${applicationArchivePatternOrPath}.`);
|
||||
const { path } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'path',
|
||||
message: 'Provide a path to the application archive:',
|
||||
validate: value => (value ? true : 'Path may not be empty.'),
|
||||
});
|
||||
applicationArchives = await findArtifactsAsync({
|
||||
rootDir,
|
||||
patternOrPathArray: [path],
|
||||
});
|
||||
}
|
||||
|
||||
if (applicationArchives.length === 1) {
|
||||
return applicationArchives[0];
|
||||
}
|
||||
|
||||
if (applicationArchives.length > 1) {
|
||||
const { path } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'path',
|
||||
message: 'Found multiple application archives. Select one:',
|
||||
choices: applicationArchives.map(archivePath => {
|
||||
return {
|
||||
title: archivePath,
|
||||
value: archivePath,
|
||||
};
|
||||
}),
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
throw new Error(`Found no application archives at ${inputBuildPath}.`);
|
||||
}
|
||||
|
||||
async function findArtifactsAsync({
|
||||
rootDir,
|
||||
patternOrPathArray,
|
||||
}: {
|
||||
rootDir: string;
|
||||
patternOrPathArray: string[];
|
||||
}): Promise<string[]> {
|
||||
const files = new Set<string>();
|
||||
for (const patternOrPath of patternOrPathArray) {
|
||||
if (path.isAbsolute(patternOrPath) && (await fs.pathExists(patternOrPath))) {
|
||||
files.add(patternOrPath);
|
||||
} else {
|
||||
const filesFound = await fg(patternOrPath, {
|
||||
cwd: rootDir,
|
||||
onlyFiles: false,
|
||||
});
|
||||
filesFound.forEach(file => files.add(file));
|
||||
}
|
||||
}
|
||||
|
||||
return [...files].map(filePath => {
|
||||
// User may provide an absolute path as input in which case
|
||||
// fg will return an absolute path.
|
||||
if (path.isAbsolute(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// User may also provide a relative path in which case
|
||||
// fg will return a path relative to rootDir.
|
||||
return path.join(rootDir, filePath);
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadAppArchiveAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
originalPath: string
|
||||
): Promise<string> {
|
||||
let filePath = originalPath;
|
||||
if ((await fs.stat(filePath)).isDirectory()) {
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
|
||||
const parentPath = path.dirname(originalPath);
|
||||
const folderName = path.basename(originalPath);
|
||||
await tar.create({ cwd: parentPath, file: tarPath, gzip: true }, [folderName]);
|
||||
filePath = tarPath;
|
||||
}
|
||||
const fileSize = (await fs.stat(filePath)).size;
|
||||
const bucketKey = await uploadFileAtPathToGCSAsync(
|
||||
graphqlClient,
|
||||
UploadSessionType.EasShareGcsAppArchive,
|
||||
filePath,
|
||||
createProgressTracker({
|
||||
total: fileSize,
|
||||
message: 'Uploading to EAS',
|
||||
completedMessage: 'Uploaded to EAS',
|
||||
})
|
||||
);
|
||||
return bucketKey;
|
||||
}
|
||||
|
||||
type AppMetadata = {
|
||||
fingerprintHash?: string;
|
||||
developmentClient: boolean;
|
||||
simulator: boolean;
|
||||
};
|
||||
|
||||
async function extractAppMetadataAsync(
|
||||
buildPath: string,
|
||||
platform: Platform
|
||||
): Promise<AppMetadata> {
|
||||
let developmentClient = false;
|
||||
let fingerprintHash: string | undefined;
|
||||
const simulator = platform === Platform.IOS;
|
||||
|
||||
const basePath = platform === Platform.ANDROID ? 'assets/' : buildPath;
|
||||
const fingerprintFilePath =
|
||||
platform === Platform.ANDROID ? 'fingerprint' : 'EXUpdates.bundle/fingerprint';
|
||||
const devMenuBundlePath =
|
||||
platform === Platform.ANDROID ? 'EXDevMenuApp.android.js' : 'EXDevMenu.bundle/';
|
||||
|
||||
const buildExtension = path.extname(buildPath);
|
||||
if (['.apk', '.aab'].includes(buildExtension)) {
|
||||
const zip = new StreamZip.async({ file: buildPath });
|
||||
try {
|
||||
developmentClient = Boolean(await zip.entry(path.join(basePath, devMenuBundlePath)));
|
||||
if (await zip.entry(path.join(basePath, fingerprintFilePath))) {
|
||||
fingerprintHash = (await zip.entryData(path.join(basePath, fingerprintFilePath))).toString(
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error(`Error reading ${buildExtension}: ${err}`);
|
||||
} finally {
|
||||
await zip.close();
|
||||
}
|
||||
} else if (buildExtension === '.app') {
|
||||
developmentClient = await fs.exists(path.join(basePath, devMenuBundlePath));
|
||||
|
||||
if (await fs.exists(path.join(basePath, fingerprintFilePath))) {
|
||||
fingerprintHash = await fs.readFile(path.join(basePath, fingerprintFilePath), 'utf8');
|
||||
}
|
||||
} else {
|
||||
// Use tar to list files in the archive
|
||||
try {
|
||||
let fingerprintHashPromise: Promise<string> | undefined;
|
||||
await tar.list({
|
||||
file: buildPath,
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
onentry: entry => {
|
||||
if (entry.path.endsWith(devMenuBundlePath)) {
|
||||
developmentClient = true;
|
||||
}
|
||||
if (entry.path.endsWith(fingerprintFilePath)) {
|
||||
fingerprintHashPromise = new Promise<string>(async (resolve, reject) => {
|
||||
try {
|
||||
let content = '';
|
||||
for await (const chunk of entry) {
|
||||
content += chunk.toString('utf8');
|
||||
}
|
||||
resolve(content);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
if (fingerprintHashPromise !== undefined) {
|
||||
fingerprintHash = await fingerprintHashPromise;
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error(`Error reading ${buildExtension}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
developmentClient,
|
||||
fingerprintHash,
|
||||
simulator,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs/promises';
|
||||
import fsExtra from 'fs-extra';
|
||||
import path from 'path';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
|
||||
const DEFAULT_WORKFLOW_NAME = 'workflow.yml';
|
||||
const HELLO_WORLD_TEMPLATE = `name: Hello World
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
|
||||
jobs:
|
||||
hello_world:
|
||||
steps:
|
||||
- uses: eas/checkout
|
||||
- run: echo "Hello, World"
|
||||
`;
|
||||
|
||||
export class WorkflowCreate extends EasCommand {
|
||||
static override description = 'create a new workflow configuration YAML file';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Name of the workflow file (must end with .yml or .yaml)',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { name: argFileName },
|
||||
flags,
|
||||
} = await this.parse(WorkflowCreate);
|
||||
|
||||
const { projectDir } = await this.getContextAsync(WorkflowCreate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
let fileName = argFileName;
|
||||
|
||||
if (!fileName) {
|
||||
const response = await prompts({
|
||||
type: 'text',
|
||||
name: 'fileName',
|
||||
message: 'What would you like to name your workflow file?',
|
||||
initial: DEFAULT_WORKFLOW_NAME,
|
||||
validate: value => {
|
||||
try {
|
||||
WorkflowFile.validateYamlExtension(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : 'Invalid file name';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.fileName) {
|
||||
Log.warn('Workflow creation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fileName = response.fileName;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ensureWorkflowsDirectoryExistsAsync({ projectDir });
|
||||
await this.createWorkflowFileAsync({ fileName, projectDir });
|
||||
} catch (error) {
|
||||
Log.error('Failed to create workflow file.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureWorkflowsDirectoryExistsAsync({
|
||||
projectDir,
|
||||
}: {
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await fs.access(path.join(projectDir, '.eas', 'workflows'));
|
||||
} catch {
|
||||
await fs.mkdir(path.join(projectDir, '.eas', 'workflows'), { recursive: true });
|
||||
Log.withTick(`Created directory ${chalk.bold(path.join(projectDir, '.eas', 'workflows'))}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async createWorkflowFileAsync({
|
||||
fileName,
|
||||
projectDir,
|
||||
}: {
|
||||
fileName: string;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
WorkflowFile.validateYamlExtension(fileName);
|
||||
|
||||
const filePath = path.join(projectDir, '.eas', 'workflows', fileName);
|
||||
|
||||
if (await fsExtra.pathExists(filePath)) {
|
||||
throw new Error(`Workflow file already exists: ${filePath}`);
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, HELLO_WORLD_TEMPLATE);
|
||||
Log.withTick(`Created ${chalk.bold(filePath)}`);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
import fs from 'node:fs';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { getWorkflowRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { WorkflowProjectSourceType } from '../../graphql/generated';
|
||||
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
|
||||
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
|
||||
import Log, { link } from '../../log';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { uploadAccountScopedEasJsonAsync } from '../../project/uploadAccountScopedEasJsonAsync';
|
||||
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
|
||||
export default class WorkflowRun extends EasCommand {
|
||||
static override description = 'Run an EAS workflow';
|
||||
|
||||
// TODO(@sjchmiela): Keep command hidden until workflows are live
|
||||
static override hidden = true;
|
||||
static override state = 'beta';
|
||||
|
||||
static override args = [{ name: 'file', description: 'Path to the workflow file to run' }];
|
||||
|
||||
static override flags = {
|
||||
@@ -34,8 +32,6 @@ export default class WorkflowRun extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(WorkflowRun);
|
||||
|
||||
Log.warn('Workflows are in beta and subject to breaking changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -48,7 +44,12 @@ export default class WorkflowRun extends EasCommand {
|
||||
|
||||
let yamlConfig: string;
|
||||
try {
|
||||
yamlConfig = await fs.promises.readFile(path.join(projectDir, args.file), 'utf8');
|
||||
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
|
||||
projectDir,
|
||||
filePath: args.file,
|
||||
});
|
||||
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
|
||||
yamlConfig = workflowFileContents.yamlConfig;
|
||||
} catch (err) {
|
||||
Log.error('Failed to read workflow file.');
|
||||
|
||||
@@ -61,8 +62,26 @@ export default class WorkflowRun extends EasCommand {
|
||||
} = await getDynamicPrivateProjectConfigAsync();
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
try {
|
||||
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
yamlConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof CombinedError) {
|
||||
WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
||||
error,
|
||||
accountName: account.name,
|
||||
projectName,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let projectArchiveBucketKey: string;
|
||||
let easJsonBucketKey: string;
|
||||
let packageJsonBucketKey: string;
|
||||
|
||||
try {
|
||||
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
|
||||
@@ -70,10 +89,17 @@ export default class WorkflowRun extends EasCommand {
|
||||
vcsClient,
|
||||
accountId: account.id,
|
||||
}));
|
||||
({ easJsonBucketKey } = await uploadAccountScopedEasJsonAsync({
|
||||
({ fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
projectDir,
|
||||
filePath: path.join(projectDir, 'eas.json'),
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
}));
|
||||
({ fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: path.join(projectDir, 'package.json'),
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
}));
|
||||
} catch (err) {
|
||||
Log.error('Failed to upload project sources.');
|
||||
@@ -95,6 +121,7 @@ export default class WorkflowRun extends EasCommand {
|
||||
type: WorkflowProjectSourceType.Gcs,
|
||||
projectArchiveBucketKey,
|
||||
easJsonBucketKey,
|
||||
packageJsonBucketKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -102,7 +129,7 @@ export default class WorkflowRun extends EasCommand {
|
||||
|
||||
Log.newLine();
|
||||
Log.succeed(
|
||||
`Workflow started successfully. ${link(
|
||||
`Workflow run started successfully. See logs: ${link(
|
||||
getWorkflowRunUrl(account.name, projectName, workflowRunId)
|
||||
)}`
|
||||
);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import * as YAML from 'yaml';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
|
||||
export class WorkflowValidate extends EasCommand {
|
||||
static override description = 'validate a workflow configuration yaml file';
|
||||
static override hidden = true;
|
||||
|
||||
static override args = [
|
||||
{
|
||||
@@ -18,81 +20,83 @@ export class WorkflowValidate extends EasCommand {
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { path: filePath },
|
||||
flags,
|
||||
} = await this.parse(WorkflowValidate);
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
projectDir,
|
||||
} = await this.getContextAsync(WorkflowValidate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const {
|
||||
projectId,
|
||||
exp: { slug: projectName },
|
||||
} = await getDynamicPrivateProjectConfigAsync();
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
const spinner = ora().start('Validating the workflow YAML file…');
|
||||
|
||||
try {
|
||||
await validateYAMLAsync(filePath);
|
||||
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
|
||||
projectDir,
|
||||
filePath,
|
||||
});
|
||||
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
|
||||
|
||||
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
|
||||
|
||||
// Check if the parsed result is empty or null
|
||||
if (
|
||||
parsedYaml === null ||
|
||||
parsedYaml === undefined ||
|
||||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
|
||||
) {
|
||||
throw new Error('YAML file is empty or contains only comments.');
|
||||
}
|
||||
|
||||
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
yamlConfig: workflowFileContents.yamlConfig,
|
||||
});
|
||||
|
||||
spinner.succeed('Workflow configuration YAML is valid.');
|
||||
} catch (error) {
|
||||
spinner.fail('Workflow configuration YAML is not valid.');
|
||||
|
||||
if (error instanceof YAML.YAMLParseError) {
|
||||
Log.error(`YAML syntax error: ${error.message}`);
|
||||
} else if (error instanceof CombinedError) {
|
||||
WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
||||
error,
|
||||
accountName: account.name,
|
||||
projectName,
|
||||
});
|
||||
|
||||
throw error;
|
||||
} else if (error instanceof Error) {
|
||||
Log.error(error.message);
|
||||
} else {
|
||||
Log.error(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkIfFileExistsAsync(filePath: string): Promise<void> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
throw new Error(`File does not exist: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfValidYAMLFileExtension(filePath: string): void {
|
||||
const fileExtension = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (fileExtension !== '.yml' && fileExtension !== '.yaml') {
|
||||
throw new Error('File must have a .yml or .yaml extension');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateYAMLAsync(filePath: string): Promise<void> {
|
||||
await checkIfFileExistsAsync(filePath);
|
||||
checkIfValidYAMLFileExtension(filePath);
|
||||
|
||||
try {
|
||||
const fileContents = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// First check if the file is empty or only contains whitespace
|
||||
if (!fileContents.trim()) {
|
||||
throw new Error('YAML file is empty or contains only comments.');
|
||||
}
|
||||
|
||||
const parsedYaml = YAML.parse(fileContents);
|
||||
|
||||
// Check if the parsed result is empty or null
|
||||
if (
|
||||
parsedYaml === null ||
|
||||
parsedYaml === undefined ||
|
||||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
|
||||
) {
|
||||
throw new Error('YAML file is empty or contains only comments.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === 'YAML file is empty or contains only comments.'
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof YAML.YAMLParseError || error instanceof Error) {
|
||||
throw new Error(`YAML parsing error: ${error.message}`);
|
||||
} else {
|
||||
throw new Error(`YAML parsing error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export interface AndroidCredentials {
|
||||
|
||||
interface Options {
|
||||
app: AppLookupParams;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider {
|
||||
@@ -37,7 +38,7 @@ export default class AndroidCredentialsProvider {
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<AndroidCredentials> {
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
|
||||
const setupBuildCredentialsAction = new SetUpBuildCredentials(this.options);
|
||||
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
|
||||
return this.toAndroidCredentials(buildCredentials);
|
||||
}
|
||||
|
||||
Loaded 100 of 196 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user