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 | ||
|
|
073bdcd575 | ||
|
|
6979c8d67b | ||
|
|
0869bc511f | ||
|
|
693562d742 |
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
|
||||
|
||||
+365
@@ -12,6 +12,371 @@ 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
|
||||
|
||||
## [13.2.0](https://github.com/expo/eas-cli/releases/tag/v13.2.0) - 2024-11-13
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "13.2.2",
|
||||
"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.2",
|
||||
"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,24 +79,13 @@ async function resolveEnvVarsAsync({
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables 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 found for the "${environment.toLowerCase()}" environment on EAS servers.`
|
||||
);
|
||||
}
|
||||
|
||||
const encryptedEnvVars = environmentVariables.filter(({ name, value }) => name && !value);
|
||||
if (encryptedEnvVars.length > 0) {
|
||||
Log.warn(
|
||||
`Some environment variables defined in the "${environment.toLowerCase()}" environment on EAS servers are of "encrypted" type and cannot be read outside of the EAS servers (including EAS CLI): ${encryptedEnvVars
|
||||
.map(({ name }) => name)
|
||||
.join(
|
||||
', '
|
||||
)}. However, they will be available during the build process happening on the EAS servers. This can lead to potential configuration mismatches between the local development environment and the build environment if the encrypted environment variables are used to resolve the app config.`
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,10 +95,6 @@ async function resolveEnvVarsAsync({
|
||||
buildProfile.env && Object.keys(buildProfile.env).join(', ')
|
||||
}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables specified in the "${buildProfileName}" build profile "env" configuration.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -126,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.`
|
||||
);
|
||||
@@ -145,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
-11
@@ -42,22 +42,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables 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 readable environment variables found for the "${environment.toLowerCase()}" environment on EAS servers.`
|
||||
);
|
||||
}
|
||||
|
||||
const encryptedEnvVars = environmentVariables.filter(({ name, value }) => name && !value);
|
||||
if (encryptedEnvVars.length > 0) {
|
||||
Log.warn(
|
||||
`Some environment variables defined in the "${environment.toLowerCase()}" environment on EAS servers are of "encrypted" type and cannot be read outside of the EAS servers (including EAS CLI): ${encryptedEnvVars
|
||||
.map(({ name }) => name)
|
||||
.join(', ')}. `
|
||||
`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