mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
193
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1bf434ec | ||
|
|
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 | ||
|
|
734a071345 | ||
|
|
26fd7a7daa | ||
|
|
df13e0433e | ||
|
|
fa72248d45 | ||
|
|
2b51db72db | ||
|
|
79696394be | ||
|
|
86c99efdcb | ||
|
|
ee500109f6 | ||
|
|
7eff738efe | ||
|
|
fdb167df34 | ||
|
|
027d3629a6 | ||
|
|
c30c1cd5cf | ||
|
|
a5d3ea8aba | ||
|
|
37f53e0976 | ||
|
|
97a26dce40 | ||
|
|
b0b14788dd | ||
|
|
b81c60e8a4 | ||
|
|
d47b80a125 | ||
|
|
3f2dcc7e09 | ||
|
|
b039da04e2 | ||
|
|
9e55fcbe7c | ||
|
|
281ae7bad6 | ||
|
|
433c47b221 | ||
|
|
5be97c50ae | ||
|
|
2bc0e968cf | ||
|
|
a426164e85 | ||
|
|
ac799bbf76 | ||
|
|
cf8e715cd5 | ||
|
|
71ca181895 | ||
|
|
7c9970d279 | ||
|
|
f40484ec4d | ||
|
|
746d64ac30 | ||
|
|
3782767f9a | ||
|
|
efb1c87fe3 | ||
|
|
c4075b9fd0 | ||
|
|
5bd3760b57 | ||
|
|
dd190869aa | ||
|
|
c7b2b8388c | ||
|
|
3e6cf78760 | ||
|
|
5478814cf6 | ||
|
|
6449ea7c16 | ||
|
|
88740682cd | ||
|
|
8c05a711fc | ||
|
|
d0f01cb58f | ||
|
|
a212662003 | ||
|
|
3139124daf | ||
|
|
8c4be1c556 | ||
|
|
3a7dfb0001 | ||
|
|
214cff043c | ||
|
|
0515c9a6fa | ||
|
|
367b763adf | ||
|
|
49dba20200 | ||
|
|
f0b14bf6a1 | ||
|
|
161acbb160 | ||
|
|
5aad520c23 | ||
|
|
d6ec3ae106 | ||
|
|
feb8d2d8f4 | ||
|
|
c07fd222d9 | ||
|
|
1873804c8b | ||
|
|
1cc4fd8880 | ||
|
|
f83d7e0be7 | ||
|
|
b225ba4bf3 | ||
|
|
4fb5972f18 | ||
|
|
463f73eec2 | ||
|
|
2dbaed5b2a | ||
|
|
20b76f72b0 | ||
|
|
56444432b9 | ||
|
|
02022d8d17 | ||
|
|
a7c77e8ab3 | ||
|
|
6489ddc6ad | ||
|
|
496e68935a | ||
|
|
a346af2869 | ||
|
|
7699573100 | ||
|
|
3f832765c2 | ||
|
|
af16cbaaea | ||
|
|
0d384d1722 | ||
|
|
58d2088bf7 | ||
|
|
c622014b64 | ||
|
|
81feffa0c5 | ||
|
|
9521cc45da | ||
|
|
1dfe24d48a | ||
|
|
99765c1802 | ||
|
|
de388cfd27 |
No files matched your search
+9
-12
@@ -1,23 +1,20 @@
|
||||
commentConfiguration:
|
||||
preamble: 'Subscribed to pull request'
|
||||
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']
|
||||
- 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']
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: tobyhs/codemention@v1.2.0
|
||||
- uses: tobyhs/codemention@v1.4.0
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
+240
@@ -8,13 +8,253 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- 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))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Fix tests for VisionOS type.
|
||||
- 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
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add EAS_SKIP_AUTO_FINGERPRINT to skip fingerprint computation on build ([#2675](https://github.com/expo/eas-cli/pull/2675) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Default build-logger-level to debug when EXPO_DEBUG is set. ([#2676](https://github.com/expo/eas-cli/pull/2676) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [13.1.1](https://github.com/expo/eas-cli/releases/tag/v13.1.1) - 2024-11-08
|
||||
|
||||
## [13.1.0](https://github.com/expo/eas-cli/releases/tag/v13.1.0) - 2024-11-07
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Compute fingerprint for builds with SDK 52 and higher ([#2663](https://github.com/expo/eas-cli/pull/2663) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [13.0.1](https://github.com/expo/eas-cli/releases/tag/v13.0.1) - 2024-11-05
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade EAS Metadata with new properties from App Store. ([#2671](https://github.com/expo/eas-cli/pull/2671) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [13.0.0](https://github.com/expo/eas-cli/releases/tag/v13.0.0) - 2024-11-05
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Resolve versioned expo config using `npx expo config` command instead of using fixed `@expo/config` version shipped with EAS CLI, if available. ([#2529](https://github.com/expo/eas-cli/pull/2529) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add `macos-sonoma-14.6-xcode-16.1` image and `sdk-52` image tag to `eas.schema.json`. ([#2666](https://github.com/expo/eas-cli/pull/2666) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Update `@expo` packages versions. ([#2530](https://github.com/expo/eas-cli/pull/2530) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [12.6.2](https://github.com/expo/eas-cli/releases/tag/v12.6.2) - 2024-10-29
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make email case insensitive during Apple authentication. ([#2659](https://github.com/expo/eas-cli/pull/2659) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [12.6.1](https://github.com/expo/eas-cli/releases/tag/v12.6.1) - 2024-10-25
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Create dynamic logged in context field and clean up erroneous SessionManager context field uses. ([#2648](https://github.com/expo/eas-cli/pull/2648) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [12.6.0](https://github.com/expo/eas-cli/releases/tag/v12.6.0) - 2024-10-21
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas channel:pause` and `eas channel:resume` commands to pause/resume update delivery to builds using specific update channels and display paused status in channel details output. ([#2614](https://github.com/expo/eas-cli/pull/2614) by [@fiberjw](https://github.com/fiberjw))
|
||||
- Add interactivity to eas update:edit command. ([#2638](https://github.com/expo/eas-cli/pull/2638) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [12.5.4](https://github.com/expo/eas-cli/releases/tag/v12.5.4) - 2024-10-19
|
||||
|
||||
## [12.5.3](https://github.com/expo/eas-cli/releases/tag/v12.5.3) - 2024-10-16
|
||||
|
||||
## [12.5.2](https://github.com/expo/eas-cli/releases/tag/v12.5.2) - 2024-10-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Disallow republishing an update that is being rolled-out. ([#2602](https://github.com/expo/eas-cli/pull/2602) by [@wschurman](https://github.com/wschurman))
|
||||
- Bump `@expo/apple-utils` to `2.0.2` resolving the Apple authentication error. ([#2641](https://github.com/expo/eas-cli/pull/2641) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Implement new `worker` deploy API flow. ([#2601](https://github.com/expo/eas-cli/pull/2601) by [@kitten](https://github.com/kitten)))
|
||||
- Unhide rollout-percentage flag on update publish command. ([#2608](https://github.com/expo/eas-cli/pull/2608) by [@wschurman](https://github.com/wschurman))
|
||||
- Let folks know about the new concurrency add-on ([#2611](https://github.com/expo/eas-cli/pull/2611) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [12.5.1](https://github.com/expo/eas-cli/releases/tag/v12.5.1) - 2024-09-27
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "12.5.1",
|
||||
"version": "14.5.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+565
-161
File diff suppressed because it is too large.
Load diff
Generated
+6250
-573
File diff suppressed because it is too large.
Load diff
@@ -1,35 +1,34 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "12.5.1",
|
||||
"version": "14.5.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "1.8.0",
|
||||
"@expo/apple-utils": "2.1.5",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "8.5.4",
|
||||
"@expo/config-plugins": "7.8.4",
|
||||
"@expo/config-types": "50.0.0",
|
||||
"@expo/eas-build-job": "1.0.136",
|
||||
"@expo/eas-json": "12.5.0",
|
||||
"@expo/env": "^0.3.0",
|
||||
"@expo/json-file": "8.2.37",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "1.0.165",
|
||||
"@expo/eas-json": "14.5.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.0.33",
|
||||
"@expo/package-manager": "1.1.2",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.20",
|
||||
"@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": "6.7.3",
|
||||
"@expo/prebuild-config": "8.0.17",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/steps": "1.0.136",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "1.0.166",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^1.26.2",
|
||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
||||
@@ -42,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",
|
||||
@@ -65,7 +65,7 @@
|
||||
"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",
|
||||
"nullthrows": "1.1.1",
|
||||
@@ -87,6 +87,7 @@
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "9.0.1",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.6.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -94,8 +95,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 +180,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": {
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-06/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
"$ref": "#/definitions/AppleConfig"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configVersion",
|
||||
"apple"
|
||||
],
|
||||
"definitions": {
|
||||
"apple": {
|
||||
"AppleKidsAge": {
|
||||
@@ -323,7 +337,8 @@
|
||||
"gambling",
|
||||
"unrestrictedWebAccess",
|
||||
"kidsAgeBand",
|
||||
"seventeenPlus"
|
||||
"ageRatingOverride",
|
||||
"koreaAgeRatingOverride"
|
||||
],
|
||||
"defaultSnippets": [
|
||||
{
|
||||
@@ -345,7 +360,9 @@
|
||||
"gambling": false,
|
||||
"unrestrictedWebAccess": false,
|
||||
"kidsAgeBand": null,
|
||||
"seventeenPlus": false
|
||||
"ageRatingOverride": "NONE",
|
||||
"koreaAgeRatingOverride": "NONE",
|
||||
"lootBox": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -367,7 +384,9 @@
|
||||
"unrestrictedWebAccess": false,
|
||||
"gambling": false,
|
||||
"kidsAgeBand": null,
|
||||
"seventeenPlus": false
|
||||
"ageRatingOverride": "${13|NONE,SEVENTEEN_PLUS,UNRATED|}",
|
||||
"koreaAgeRatingOverride": "${14|NONE,FIFTEEN_PLUS,NINETEEN_PLUS|}",
|
||||
"lootBox": false
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -442,8 +461,24 @@
|
||||
},
|
||||
"seventeenPlus": {
|
||||
"type": "boolean",
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "This property is deprecated, use `ageRatingOverride` instead.",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
},
|
||||
"ageRatingOverride": {
|
||||
"$ref": "#/definitions/apple/AppleAgeRatingOverride",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
},
|
||||
"koreaAgeRatingOverride": {
|
||||
"$ref": "#/definitions/apple/AppleKoreaAgeRatingOverride",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
},
|
||||
"lootBox": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain purchasable loot boxes? These virtual containers provide players with in-game items, including functional cards or cosmetic modifications, based on random chance."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -565,6 +600,20 @@
|
||||
"maxLength": 4000
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAgeRatingOverride": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"SEVENTEEN_PLUS",
|
||||
"UNRATED"
|
||||
]
|
||||
},
|
||||
"AppleKoreaAgeRatingOverride": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"FIFTEEN_PLUS",
|
||||
"NINETEEN_PLUS"
|
||||
]
|
||||
}
|
||||
},
|
||||
"AppleConfig": {
|
||||
@@ -775,19 +824,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
"$ref": "#/definitions/AppleConfig"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configVersion",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import { Command, Config } from '@oclif/core';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -115,6 +115,10 @@ export async function prepareJobAsync(
|
||||
},
|
||||
}),
|
||||
loggerLevel: ctx.loggerLevel,
|
||||
// Technically, these are unused, but let's include them here for type consistency.
|
||||
// See: https://github.com/expo/eas-build/pull/454
|
||||
appId: ctx.projectId,
|
||||
initiatingUserId: ctx.user.id,
|
||||
};
|
||||
|
||||
return sanitizeBuildJob(job) as Android.Job;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -66,6 +66,7 @@ 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';
|
||||
@@ -174,8 +175,9 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
}
|
||||
assert(projectArchive);
|
||||
|
||||
const runtimeMetadata = await createAndMaybeUploadFingerprintAsync(ctx);
|
||||
const metadata = await collectMetadataAsync(ctx, runtimeMetadata);
|
||||
const runtimeAndFingerprintMetadata =
|
||||
await computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync(ctx);
|
||||
const metadata = await collectMetadataAsync(ctx, runtimeAndFingerprintMetadata);
|
||||
const buildParams = resolveBuildParamsInput(ctx, metadata);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
@@ -486,8 +488,8 @@ async function handleSingleBuildProgressAsync(
|
||||
spinner.succeed('Build finished');
|
||||
return { refetch: false };
|
||||
case BuildStatus.New:
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
|
||||
formatAccountBillingUrl(accountName)
|
||||
)}.`;
|
||||
break;
|
||||
case BuildStatus.InQueue: {
|
||||
@@ -509,7 +511,7 @@ async function handleSingleBuildProgressAsync(
|
||||
Log.log('Start builds sooner in the priority queue.');
|
||||
Log.log(
|
||||
`Sign up for EAS Production or Enterprise at ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
formatAccountBillingUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
@@ -656,18 +658,39 @@ function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/settings/subscriptions`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
function formatAccountBillingUrl(accountName: string): string {
|
||||
return new URL(`/accounts/${accountName}/settings/billing`, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
|
||||
async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{
|
||||
runtimeVersion?: string | undefined;
|
||||
fingerprintHash?: string | undefined;
|
||||
fingerprintSource?: FingerprintSource | undefined;
|
||||
}> {
|
||||
const runtimeAndFingerprintMetadata =
|
||||
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
|
||||
if (!runtimeAndFingerprintMetadata?.fingerprintHash) {
|
||||
const fingerprint = await computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync(ctx);
|
||||
return {
|
||||
...runtimeAndFingerprintMetadata,
|
||||
...fingerprint,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...runtimeAndFingerprintMetadata,
|
||||
fingerprintHash: runtimeAndFingerprintMetadata.runtimeVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{
|
||||
runtimeVersion?: string;
|
||||
fingerprintSource?: FingerprintSource;
|
||||
fingerprintHash?: string;
|
||||
}> {
|
||||
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
|
||||
exp: ctx.exp,
|
||||
@@ -686,16 +709,54 @@ async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
return await maybeUploadFingerprintAsync({
|
||||
runtimeVersion: resolvedRuntimeVersion.runtimeVersion,
|
||||
fingerprint: resolvedRuntimeVersion.fingerprint,
|
||||
const uploadedFingerprint = await maybeUploadFingerprintAsync({
|
||||
hash: resolvedRuntimeVersion.runtimeVersion,
|
||||
fingerprint: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint,
|
||||
graphqlClient: ctx.graphqlClient,
|
||||
localBuildMode: ctx.localBuildOptions.localBuildMode,
|
||||
});
|
||||
return {
|
||||
runtimeVersion: uploadedFingerprint.hash,
|
||||
fingerprintSource: uploadedFingerprint.fingerprintSource,
|
||||
fingerprintHash: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprintHash ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
{ debug }: { debug?: boolean } = {}
|
||||
): Promise<{
|
||||
fingerprintHash?: string;
|
||||
fingerprintSource?: FingerprintSource;
|
||||
}> {
|
||||
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
|
||||
workflow: ctx.workflow,
|
||||
platforms: [ctx.platform],
|
||||
env: ctx.env,
|
||||
});
|
||||
if (!fingerprint) {
|
||||
return {};
|
||||
}
|
||||
const uploadedFingerprint = await maybeUploadFingerprintAsync({
|
||||
hash: fingerprint.hash,
|
||||
fingerprint: {
|
||||
fingerprintSources: fingerprint.sources,
|
||||
isDebugFingerprintSource: debug ?? false,
|
||||
},
|
||||
graphqlClient: ctx.graphqlClient,
|
||||
localBuildMode: ctx.localBuildOptions.localBuildMode,
|
||||
});
|
||||
return {
|
||||
fingerprintHash: uploadedFingerprint.hash,
|
||||
fingerprintSource: uploadedFingerprint.fingerprintSource,
|
||||
};
|
||||
}
|
||||
@@ -54,9 +54,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
|
||||
@@ -81,21 +82,44 @@ async function resolveEnvVarsAsync({
|
||||
.map(({ name, value }) => [name, value])
|
||||
) as Record<string, string>;
|
||||
|
||||
const envVarsWithSource: Record<string, 'build profile' | 'EAS server'> = {
|
||||
...Object.fromEntries(Object.keys(serverEnvVars).map(key => [key, 'EAS server'])),
|
||||
...(buildProfile.env
|
||||
? Object.fromEntries(Object.keys(buildProfile.env).map(key => [key, 'build profile']))
|
||||
: null),
|
||||
};
|
||||
Log.log(
|
||||
`Loaded "env" configuration for the "${buildProfileName}" profile and "${environment.toLowerCase()}" environment: ${
|
||||
Object.keys(envVarsWithSource).length > 0
|
||||
? Object.keys(envVarsWithSource)
|
||||
.map(key => `${key} (source: ${envVarsWithSource[key]})`)
|
||||
.join('\n')
|
||||
: 'no environment variables specified'
|
||||
}\n${learnMore('https://docs.expo.dev/build-reference/variables/')}`
|
||||
);
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
|
||||
serverEnvVars
|
||||
).join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildProfile.env && Object.keys(buildProfile.env).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables loaded from the "${buildProfileName}" build profile "env" configuration: ${
|
||||
buildProfile.env && Object.keys(buildProfile.env).join(', ')
|
||||
}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
buildProfile.env &&
|
||||
Object.keys(buildProfile.env).length > 0 &&
|
||||
Object.keys(serverEnvVars).length > 0
|
||||
) {
|
||||
const overlappingKeys = Object.keys(serverEnvVars).filter(
|
||||
key => buildProfile.env && Object.keys(buildProfile.env).includes(key)
|
||||
);
|
||||
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: ${overlappingKeys.join(
|
||||
', '
|
||||
)}. The values from the build profile configuration will be used.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
|
||||
return { ...serverEnvVars, ...buildProfile.env };
|
||||
} catch (e) {
|
||||
@@ -107,3 +131,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',
|
||||
|
||||
@@ -110,6 +110,10 @@ export async function prepareJobAsync(
|
||||
},
|
||||
}),
|
||||
loggerLevel: ctx.loggerLevel,
|
||||
// Technically, these are unused, but let's include them here for type consistency.
|
||||
// See: https://github.com/expo/eas-build/pull/454
|
||||
appId: ctx.projectId,
|
||||
initiatingUserId: ctx.user.id,
|
||||
};
|
||||
return sanitizeBuildJob(job) as Ios.Job;
|
||||
}
|
||||
|
||||
@@ -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.135';
|
||||
const PLUGIN_PACKAGE_VERSION = '1.0.163';
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,9 @@ import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
runtimeMetadata: {
|
||||
runtimeAndFingerprintMetadata: {
|
||||
runtimeVersion?: string | undefined;
|
||||
fingerprintHash?: string | undefined;
|
||||
fingerprintSource?: FingerprintSource | undefined;
|
||||
}
|
||||
): Promise<Metadata> {
|
||||
@@ -30,8 +31,9 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: runtimeMetadata?.runtimeVersion,
|
||||
fingerprintSource: runtimeMetadata?.fingerprintSource,
|
||||
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion,
|
||||
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
|
||||
fingerprintSource: runtimeAndFingerprintMetadata?.fingerprintSource,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
|
||||
...channelObject,
|
||||
distribution,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
AppVersionSource,
|
||||
@@ -381,7 +381,7 @@ async function prepareAndStartBuildAsync({
|
||||
vcsClient,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel: flags.buildLoggerLevel,
|
||||
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
|
||||
freezeCredentials: flags.freezeCredentials,
|
||||
repack: flags.repack,
|
||||
env,
|
||||
|
||||
@@ -50,3 +50,25 @@ export function getUpdateGroupUrl(
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getWorkflowRunUrl(
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
workflowRunId: string
|
||||
): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/workflows/${workflowRunId}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/github`,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ export const testChannelObject: UpdateChannelObject = {
|
||||
branchMapping:
|
||||
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
|
||||
updateBranches: [testUpdateBranch1, testUpdateBranch2],
|
||||
isPaused: false,
|
||||
__typename: 'UpdateChannel',
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -110,7 +110,11 @@ export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
|
||||
}
|
||||
): Promise<void> {
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName });
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
renderChannelHeaderContent({
|
||||
channelName: channel.name,
|
||||
channelId: channel.id,
|
||||
isPaused: channel.isPaused,
|
||||
});
|
||||
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
|
||||
@@ -167,7 +171,11 @@ function renderPageOfChannels(
|
||||
printJsonOnlyOutput({ currentPage });
|
||||
} else {
|
||||
for (const channel of currentPage) {
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
renderChannelHeaderContent({
|
||||
channelName: channel.name,
|
||||
channelId: channel.id,
|
||||
isPaused: channel.isPaused,
|
||||
});
|
||||
Log.addNewLineIfNone();
|
||||
logChannelDetails(channel);
|
||||
|
||||
@@ -196,9 +204,11 @@ function renderPageOfBranchesOnChannel(
|
||||
function renderChannelHeaderContent({
|
||||
channelName,
|
||||
channelId,
|
||||
isPaused,
|
||||
}: {
|
||||
channelName: string;
|
||||
channelId: string;
|
||||
isPaused: boolean;
|
||||
}): void {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
@@ -206,6 +216,7 @@ function renderChannelHeaderContent({
|
||||
formatFields([
|
||||
{ label: 'Name', value: channelName },
|
||||
{ label: 'ID', value: channelId },
|
||||
{ label: 'Status', value: isPaused ? 'Paused' : 'Active' },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
@@ -292,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
|
||||
@@ -308,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, {
|
||||
@@ -316,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, {
|
||||
@@ -324,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;
|
||||
}
|
||||
@@ -347,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;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import AnalyticsContextField from './context/AnalyticsContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
import DynamicLoggedInContextField from './context/DynamicLoggedInContextField';
|
||||
import {
|
||||
DynamicPrivateProjectConfigContextField,
|
||||
DynamicPublicProjectConfigContextField,
|
||||
@@ -15,6 +16,8 @@ import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
|
||||
import { OptionalPrivateProjectConfigContextField } from './context/OptionalPrivateProjectConfigContextField';
|
||||
import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField';
|
||||
import ProjectDirContextField from './context/ProjectDirContextField';
|
||||
import { ProjectIdContextField } from './context/ProjectIdContextField';
|
||||
import { ServerSideEnvironmentVariablesContextField } from './context/ServerSideEnvironmentVariablesContextField';
|
||||
import SessionManagementContextField from './context/SessionManagementContextField';
|
||||
import VcsClientContextField from './context/VcsClientContextField';
|
||||
import { EasCommandError } from './errors';
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
import Log from '../log';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import { Client } from '../vcs/vcs';
|
||||
@@ -43,8 +47,26 @@ export type ContextOutput<
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
type GetContextType<Type> = {
|
||||
[Property in keyof Type]: any;
|
||||
};
|
||||
|
||||
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
|
||||
|
||||
interface BaseGetContextAsyncArgs {
|
||||
nonInteractive: boolean;
|
||||
vcsClientOverride?: Client;
|
||||
}
|
||||
|
||||
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
|
||||
extends BaseGetContextAsyncArgs {
|
||||
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
|
||||
}
|
||||
|
||||
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
|
||||
withServerSideEnvironment?: never;
|
||||
}
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
protected static readonly ContextOptions = {
|
||||
/**
|
||||
@@ -61,8 +83,16 @@ export default abstract class EasCommand extends Command {
|
||||
MaybeLoggedIn: {
|
||||
maybeLoggedIn: new MaybeLoggedInContextField(),
|
||||
},
|
||||
/**
|
||||
* Specify this context if the logged-in requirement is only necessary in a particular execution of the command.
|
||||
*/
|
||||
DynamicLoggedIn: {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
getDynamicLoggedInAsync: new DynamicLoggedInContextField(),
|
||||
},
|
||||
/**
|
||||
* Specify this context requirement if the command needs to mutate the user session.
|
||||
* @deprecated Should not be used outside of session management commands, which currently only includes `login` and `logout`.
|
||||
*/
|
||||
SessionManagment: {
|
||||
sessionManager: new SessionManagementContextField(),
|
||||
@@ -72,7 +102,7 @@ export default abstract class EasCommand extends Command {
|
||||
* run within a project directory, null otherwise.
|
||||
*/
|
||||
OptionalProjectConfig: {
|
||||
privateProjectConfig: new OptionalPrivateProjectConfigContextField(),
|
||||
optionalPrivateProjectConfig: new OptionalPrivateProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Require this command to be run in a project directory. Return the project directory in the context.
|
||||
@@ -107,6 +137,16 @@ export default abstract class EasCommand extends Command {
|
||||
Vcs: {
|
||||
vcsClient: new VcsClientContextField(),
|
||||
},
|
||||
ServerSideEnvironmentVariables: {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
getServerSideEnvironmentVariablesAsync: new ServerSideEnvironmentVariablesContextField(),
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server. Returns the project ID evaluated from the app config.
|
||||
*/
|
||||
ProjectId: {
|
||||
projectId: new ProjectIdContextField(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -141,7 +181,19 @@ export default abstract class EasCommand extends Command {
|
||||
} = object,
|
||||
>(
|
||||
commandClass: { contextDefinition: ContextInput<C> },
|
||||
{ nonInteractive, vcsClientOverride }: { nonInteractive: boolean; vcsClientOverride?: Client }
|
||||
{
|
||||
nonInteractive,
|
||||
vcsClientOverride,
|
||||
// if specified and not null, the env vars from the selected environment will be fetched from the server
|
||||
// to resolve dynamic config (if dynamic config context is used) and enable getServerSideEnvironmentVariablesAsync function (if server side environment variables context is used)
|
||||
withServerSideEnvironment,
|
||||
}: C extends
|
||||
| GetContextType<typeof EasCommand.ContextOptions.ProjectConfig>
|
||||
| GetContextType<typeof EasCommand.ContextOptions.DynamicProjectConfig>
|
||||
| GetContextType<typeof EasCommand.ContextOptions.OptionalProjectConfig>
|
||||
| GetContextType<typeof EasCommand.ContextOptions.ServerSideEnvironmentVariables>
|
||||
? GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
|
||||
: GetContextAsyncArgsWithoutServerSideEnvironmentArgument
|
||||
): Promise<ContextOutput<C>> {
|
||||
const contextDefinition = commandClass.contextDefinition;
|
||||
|
||||
@@ -155,6 +207,7 @@ export default abstract class EasCommand extends Command {
|
||||
sessionManager: this.sessionManager,
|
||||
analytics: this.analytics,
|
||||
vcsClientOverride,
|
||||
withServerSideEnvironment,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@ export async function fetchBuildsAsync({
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
filters?: { statuses?: BuildStatus[]; platform?: RequestedPlatform; profile?: string };
|
||||
filters?: {
|
||||
statuses?: BuildStatus[];
|
||||
platform?: RequestedPlatform;
|
||||
profile?: string;
|
||||
hasFingerprint?: boolean;
|
||||
};
|
||||
}): Promise<BuildFragment[]> {
|
||||
let builds: BuildFragment[];
|
||||
const queryFilters: InputMaybe<BuildFilter> = {};
|
||||
@@ -48,6 +53,9 @@ export async function fetchBuildsAsync({
|
||||
if (filters?.profile) {
|
||||
queryFilters['buildProfile'] = filters.profile;
|
||||
}
|
||||
if (filters?.hasFingerprint) {
|
||||
queryFilters['hasFingerprint'] = filters.hasFingerprint;
|
||||
}
|
||||
if (!filters?.statuses) {
|
||||
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import { Client } from '../../vcs/vcs';
|
||||
|
||||
@@ -7,6 +8,10 @@ export interface ContextOptions {
|
||||
analytics: Analytics;
|
||||
nonInteractive: boolean;
|
||||
vcsClientOverride?: Client;
|
||||
/**
|
||||
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
|
||||
*/
|
||||
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
|
||||
}
|
||||
|
||||
export default abstract class ContextField<T> {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
|
||||
export type DynamicLoggedInContextFn = () => Promise<{
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
authenticationInfo: LoggedInAuthenticationInfo;
|
||||
}>;
|
||||
|
||||
export default class DynamicLoggedInContextField extends ContextField<DynamicLoggedInContextFn> {
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
}: ContextOptions): Promise<DynamicLoggedInContextFn> {
|
||||
return async () => {
|
||||
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
|
||||
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
|
||||
return {
|
||||
actor,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
authenticationInfo,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import {
|
||||
ExpoConfigOptions,
|
||||
getPrivateExpoConfig,
|
||||
getPublicExpoConfig,
|
||||
getPrivateExpoConfigAsync,
|
||||
getPublicExpoConfigAsync,
|
||||
} from '../../project/expoConfig';
|
||||
|
||||
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
|
||||
@@ -19,15 +21,34 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const expBefore = getPublicExpoConfig(projectDir, options);
|
||||
const expBefore = await getPublicExpoConfigAsync(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
env: options?.env,
|
||||
});
|
||||
const exp = getPublicExpoConfig(projectDir, options);
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
options = {
|
||||
...options,
|
||||
env: {
|
||||
...options?.env,
|
||||
...serverSideEnvironmentVariables,
|
||||
},
|
||||
};
|
||||
}
|
||||
const exp = await getPublicExpoConfigAsync(projectDir, options);
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
@@ -41,15 +62,34 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const expBefore = getPrivateExpoConfig(projectDir, options);
|
||||
const expBefore = await getPrivateExpoConfigAsync(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
env: options?.env,
|
||||
});
|
||||
const exp = getPrivateExpoConfig(projectDir, options);
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
options = {
|
||||
...options,
|
||||
env: {
|
||||
...options?.env,
|
||||
...serverSideEnvironmentVariables,
|
||||
},
|
||||
};
|
||||
}
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, options);
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
|
||||
import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
|
||||
export default class LoggedInContextField extends ContextField<{
|
||||
type LoggedInContextType = {
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
|
||||
actor: Actor;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
authenticationInfo: LoggedInAuthenticationInfo;
|
||||
};
|
||||
|
||||
export default class LoggedInContextField extends ContextField<LoggedInContextType> {
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
}: ContextOptions): Promise<LoggedInContextType> {
|
||||
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
@@ -25,6 +28,7 @@ export default class LoggedInContextField extends ContextField<{
|
||||
actor,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
authenticationInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import { Actor } from '../../user/User';
|
||||
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../gating/FeatureGating';
|
||||
|
||||
export default class MaybeLoggedInContextField extends ContextField<{
|
||||
type MaybeLoggedInContextType = {
|
||||
actor: Actor | null;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
|
||||
actor: Actor | null;
|
||||
featureGating: FeatureGating;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}> {
|
||||
authenticationInfo: {
|
||||
accessToken: string | null;
|
||||
sessionSecret: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export default class MaybeLoggedInContextField extends ContextField<MaybeLoggedInContextType> {
|
||||
async getValueAsync({ sessionManager }: ContextOptions): Promise<MaybeLoggedInContextType> {
|
||||
const authenticationInfo = {
|
||||
accessToken: sessionManager.getAccessToken(),
|
||||
sessionSecret: sessionManager.getSessionSecret(),
|
||||
@@ -27,6 +29,7 @@ export default class MaybeLoggedInContextField extends ContextField<{
|
||||
actor: actor ?? null,
|
||||
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
authenticationInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
+24
-5
@@ -1,10 +1,12 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
|
||||
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { getPrivateExpoConfig } from '../../project/expoConfig';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
export class OptionalPrivateProjectConfigContextField extends ContextField<
|
||||
| {
|
||||
@@ -14,7 +16,11 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<
|
||||
| {
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
@@ -35,11 +41,24 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const expBefore = getPrivateExpoConfig(projectDir);
|
||||
const expBefore = await getPrivateExpoConfigAsync(projectDir);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
});
|
||||
const exp = getPrivateExpoConfig(projectDir);
|
||||
let serverSideEnvVars: Record<string, string> | undefined;
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
serverSideEnvVars = serverSideEnvironmentVariables;
|
||||
}
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
|
||||
return {
|
||||
exp,
|
||||
projectDir,
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { getPrivateExpoConfig } from '../../project/expoConfig';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
export class PrivateProjectConfigContextField extends ContextField<{
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}> {
|
||||
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<{
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
}> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
const expBefore = getPrivateExpoConfig(projectDir);
|
||||
const expBefore = await getPrivateExpoConfigAsync(projectDir);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
});
|
||||
const exp = getPrivateExpoConfig(projectDir);
|
||||
let serverSideEnvVars: Record<string, string> | undefined;
|
||||
if (withServerSideEnvironment) {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
serverSideEnvVars = serverSideEnvironmentVariables;
|
||||
}
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
|
||||
|
||||
return {
|
||||
projectId,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
export class ProjectIdContextField extends ContextField<string> {
|
||||
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<string> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
const expBefore = await getPrivateExpoConfigAsync(projectDir);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
});
|
||||
return projectId;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
|
||||
import { getPublicExpoConfigAsync } from '../../project/expoConfig';
|
||||
|
||||
type GetServerSideEnvironmentVariablesFn = (
|
||||
maybeEnv?: Record<string, string>
|
||||
) => Promise<Record<string, string>>;
|
||||
|
||||
export class ServerSideEnvironmentVariablesContextField extends ContextField<GetServerSideEnvironmentVariablesFn> {
|
||||
async getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
withServerSideEnvironment,
|
||||
}: ContextOptions): Promise<GetServerSideEnvironmentVariablesFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (maybeEnv?: Record<string, string>) => {
|
||||
if (!withServerSideEnvironment) {
|
||||
throw new Error(
|
||||
'withServerSideEnvironment parameter is required to evaluate ServerSideEnvironmentVariablesContextField'
|
||||
);
|
||||
}
|
||||
const exp = await getPublicExpoConfigAsync(projectDir, { env: maybeEnv });
|
||||
const projectId = await getProjectIdAsync(sessionManager, exp, {
|
||||
nonInteractive,
|
||||
env: maybeEnv,
|
||||
});
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
|
||||
environment: withServerSideEnvironment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
});
|
||||
return serverSideEnvironmentVariables;
|
||||
};
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -6,6 +6,7 @@ import { Role } from '../../../../graphql/generated';
|
||||
import { AppQuery } from '../../../../graphql/queries/AppQuery';
|
||||
import { learnMore } from '../../../../log';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { isExpoInstalled } from '../../../../project/projectUtils';
|
||||
import SessionManager from '../../../../user/SessionManager';
|
||||
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from '../getProjectIdAsync';
|
||||
@@ -21,6 +22,7 @@ jest.mock('../../../../ora', () => ({
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../../../../project/projectUtils');
|
||||
|
||||
describe(getProjectIdAsync, () => {
|
||||
let sessionManager: SessionManager;
|
||||
@@ -71,6 +73,7 @@ describe(getProjectIdAsync, () => {
|
||||
);
|
||||
|
||||
jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
|
||||
jest.mocked(isExpoInstalled).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('gets the project ID from app config if exists', async () => {
|
||||
@@ -222,7 +225,7 @@ describe(getProjectIdAsync, () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'success',
|
||||
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
|
||||
config: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } },
|
||||
});
|
||||
jest
|
||||
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
|
||||
|
||||
+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,5 +1,4 @@
|
||||
import { getProjectConfigDescription } from '@expo/config';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { ExpoConfig, getProjectConfigDescription } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -8,9 +7,11 @@ import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { createOrModifyExpoConfigAsync, getPrivateExpoConfig } from '../../../project/expoConfig';
|
||||
import {
|
||||
createOrModifyExpoConfigAsync,
|
||||
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';
|
||||
|
||||
@@ -26,7 +27,7 @@ export async function saveProjectIdToAppConfigAsync(
|
||||
options: { env?: Env } = {}
|
||||
): Promise<void> {
|
||||
// NOTE(cedric): we disable plugins to avoid writing plugin-generated content to `expo.extra`
|
||||
const exp = getPrivateExpoConfig(projectDir, { skipPlugins: true, ...options });
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true, ...options });
|
||||
const result = await createOrModifyExpoConfigAsync(
|
||||
projectDir,
|
||||
{
|
||||
@@ -195,7 +196,6 @@ export async function validateOrSetProjectIdAsync({
|
||||
{
|
||||
accountName: getAccountNameForEASProjectSync(exp, actor),
|
||||
projectName: exp.slug,
|
||||
privacy: toAppPrivacy(exp.privacy),
|
||||
},
|
||||
{
|
||||
nonInteractive: options.nonInteractive,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { ExpoGraphqlClient } from './createGraphqlClient';
|
||||
import { EnvironmentVariableEnvironment } from '../../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
|
||||
const cachedServerSideEnvironmentVariables: Record<
|
||||
EnvironmentVariableEnvironment,
|
||||
Record<string, string> | null
|
||||
> = {
|
||||
[EnvironmentVariableEnvironment.Development]: null,
|
||||
[EnvironmentVariableEnvironment.Preview]: null,
|
||||
[EnvironmentVariableEnvironment.Production]: null,
|
||||
};
|
||||
|
||||
export async function loadServerSideEnvironmentVariablesAsync({
|
||||
environment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
}: {
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
projectId: string;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}): Promise<Record<string, string>> {
|
||||
// don't load environment variables if they were already loaded while executing a command
|
||||
const cachedEnvVarsForEnvironment = cachedServerSideEnvironmentVariables[environment];
|
||||
if (cachedEnvVarsForEnvironment) {
|
||||
return cachedEnvVarsForEnvironment;
|
||||
}
|
||||
|
||||
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: projectId,
|
||||
environment,
|
||||
}
|
||||
);
|
||||
const serverEnvVars = Object.fromEntries(
|
||||
environmentVariables
|
||||
.filter(({ name, value }) => name && value)
|
||||
.map(({ name, value }) => [name, value])
|
||||
) as Record<string, string>;
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
|
||||
serverEnvVars
|
||||
).join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
|
||||
cachedServerSideEnvironmentVariables[environment] = serverEnvVars;
|
||||
|
||||
return serverEnvVars;
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} 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
|
||||
@@ -26,15 +22,24 @@ export const EasNonInteractiveAndJsonFlags = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const EasEnvironmentFlagParameters = {
|
||||
description: "Environment variable's environment",
|
||||
parse: upperCaseAsync,
|
||||
options: mapToLowercase([
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
EnvironmentVariableEnvironment.Preview,
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
]),
|
||||
};
|
||||
|
||||
export const EASEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
|
||||
};
|
||||
|
||||
export const EASMultiEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
description: "Environment variable's environment",
|
||||
parse: upperCaseAsync,
|
||||
options: mapToLowercase([
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
EnvironmentVariableEnvironment.Preview,
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
]),
|
||||
...EasEnvironmentFlagParameters,
|
||||
multiple: true,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -47,23 +52,19 @@ export const EASVariableFormatFlag = {
|
||||
};
|
||||
|
||||
export const EASVariableVisibilityFlag = {
|
||||
visibility: Flags.enum<EnvironmentVariableVisibility>({
|
||||
visibility: Flags.enum<'plaintext' | 'sensitive' | 'secret'>({
|
||||
description: 'Visibility of the variable',
|
||||
options: mapToLowercase([
|
||||
EnvironmentVariableVisibility.Secret,
|
||||
EnvironmentVariableVisibility.Sensitive,
|
||||
EnvironmentVariableVisibility.Public,
|
||||
]),
|
||||
parse: upperCaseAsync,
|
||||
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',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -78,3 +79,14 @@ export const EasJsonOnlyFlag = {
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution.',
|
||||
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
|
||||
parse: upperCaseAsync,
|
||||
required: false,
|
||||
default: null,
|
||||
}),
|
||||
};
|
||||
@@ -12,16 +12,14 @@ export default class AccountView extends EasCommand {
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.MaybeLoggedIn,
|
||||
...this.ContextOptions.SessionManagment,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
maybeLoggedIn: { actor },
|
||||
sessionManager,
|
||||
maybeLoggedIn: { actor, authenticationInfo },
|
||||
} = await this.getContextAsync(AccountView, { nonInteractive: true });
|
||||
if (actor) {
|
||||
const loggedInAs = sessionManager.getAccessToken()
|
||||
const loggedInAs = authenticationInfo.accessToken
|
||||
? `${getActorDisplayName(actor)} (authenticated using EXPO_TOKEN)`
|
||||
: getActorDisplayName(actor);
|
||||
Log.log(chalk.green(loggedInAs));
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class BranchCreate extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
@@ -36,7 +36,7 @@ export default class BranchCreate extends EasCommand {
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(BranchCreate);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BranchCreate, {
|
||||
@@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class BranchDelete extends EasCommand {
|
||||
static override description = 'delete a branch';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class BranchDelete extends EasCommand {
|
||||
}
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BranchDelete, { nonInteractive });
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
@@ -13,14 +13,14 @@ export default class BranchList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BranchList);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BranchList, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
|
||||
@@ -63,7 +63,7 @@ export default class BranchRename extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ export default class BranchRename extends EasCommand {
|
||||
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(BranchRename);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BranchRename, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class BranchView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class BranchView extends EasCommand {
|
||||
} = await this.parse(BranchView);
|
||||
const { 'non-interactive': nonInteractive } = flags;
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BranchView, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -110,7 +110,7 @@ export default class BuildCancel extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
@@ -128,7 +128,7 @@ export default class BuildCancel extends EasCommand {
|
||||
}
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BuildCancel, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -38,6 +38,7 @@ export default class BuildConfigure extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildConfigure, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
Log.log(
|
||||
|
||||
@@ -94,7 +94,7 @@ export default class BuildDelete extends EasCommand {
|
||||
}),
|
||||
};
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
@@ -112,7 +112,7 @@ export default class BuildDelete extends EasCommand {
|
||||
}
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BuildDelete, { nonInteractive });
|
||||
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
@@ -142,6 +142,7 @@ export default class Build extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(Build, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
@@ -79,6 +79,7 @@ export default class BuildInspect extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildInspect, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const outputDirectory = path.resolve(process.cwd(), flags.output);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
|
||||
/**
|
||||
* This command will be run on the EAS Build workers, when building
|
||||
@@ -63,7 +64,8 @@ export default class BuildInternal extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildInternal, {
|
||||
nonInteractive: true,
|
||||
vcsClientOverride: new GitNoCommitClient(),
|
||||
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
@@ -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,
|
||||
@@ -72,7 +75,7 @@ export default class BuildList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
@@ -99,7 +102,7 @@ export default class BuildList extends EasCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BuildList, {
|
||||
nonInteractive,
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -120,6 +120,7 @@ export default class BuildResign extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildResign, {
|
||||
nonInteractive: flags.nonInteractive,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const maybeBuild = flags.maybeBuildId
|
||||
|
||||
@@ -79,8 +79,7 @@ export default class Run extends EasCommand {
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
|
||||
@@ -90,7 +89,7 @@ export default class Run extends EasCommand {
|
||||
const queryOptions = getPaginatedQueryOptions(flags);
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
} = await this.getContextAsync(Run, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ export default class BuildVersionGetView extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildVersionGetView, {
|
||||
nonInteractive: true,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
if (!flags.platform && flags['non-interactive']) {
|
||||
|
||||
@@ -54,6 +54,7 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildVersionSetView, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const platform = await selectPlatformAsync(flags.platform);
|
||||
|
||||
@@ -73,6 +73,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(BuildVersionSyncView, {
|
||||
nonInteractive: true,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
@@ -119,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;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default class BuildView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export default class BuildView extends EasCommand {
|
||||
flags,
|
||||
} = await this.parse(BuildView);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BuildView, {
|
||||
nonInteractive: true,
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelCreate);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelCreate, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class ChannelDelete extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class ChannelDelete extends EasCommand {
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelDelete);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelDelete, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelEdit);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelEdit, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -18,7 +18,7 @@ export default class ChannelList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class ChannelList extends EasCommand {
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelList, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
PauseUpdateChannelMutation,
|
||||
PauseUpdateChannelMutationVariables,
|
||||
UpdateChannelBasicInfoFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import { UpdateChannelBasicInfoFragmentNode } from '../../graphql/types/UpdateChannelBasicInfo';
|
||||
import Log from '../../log';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export async function pauseUpdateChannelAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ channelId }: PauseUpdateChannelMutationVariables
|
||||
): Promise<UpdateChannelBasicInfoFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<PauseUpdateChannelMutation, PauseUpdateChannelMutationVariables>(
|
||||
gql`
|
||||
mutation PauseUpdateChannel($channelId: ID!) {
|
||||
updateChannel {
|
||||
pauseUpdateChannel(channelId: $channelId) {
|
||||
id
|
||||
...UpdateChannelBasicInfoFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(UpdateChannelBasicInfoFragmentNode)}
|
||||
`,
|
||||
{ channelId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const channel = data.updateChannel.pauseUpdateChannel;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not find a channel with id: ${channelId}`);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
export default class ChannelPause extends EasCommand {
|
||||
static override description = 'pause a channel to stop it from sending updates';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the channel to edit',
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
description: 'Name of the branch to point to',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args,
|
||||
flags: { json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelPause);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelPause, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const existingChannel = args.name
|
||||
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: args.name,
|
||||
})
|
||||
: await selectChannelOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel to edit',
|
||||
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
|
||||
});
|
||||
|
||||
const channel = await pauseUpdateChannelAsync(graphqlClient, {
|
||||
channelId: existingChannel.id,
|
||||
});
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(channel);
|
||||
} else {
|
||||
Log.withTick(chalk`Channel {bold ${channel.name}} is now paused.\n`);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk`Users with builds on channel {bold ${channel.name}} will no longer receive updates.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
ResumeUpdateChannelMutation,
|
||||
ResumeUpdateChannelMutationVariables,
|
||||
UpdateChannelBasicInfoFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import { UpdateChannelBasicInfoFragmentNode } from '../../graphql/types/UpdateChannelBasicInfo';
|
||||
import Log from '../../log';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export async function resumeUpdateChannelAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ channelId }: ResumeUpdateChannelMutationVariables
|
||||
): Promise<UpdateChannelBasicInfoFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<ResumeUpdateChannelMutation, ResumeUpdateChannelMutationVariables>(
|
||||
gql`
|
||||
mutation ResumeUpdateChannel($channelId: ID!) {
|
||||
updateChannel {
|
||||
resumeUpdateChannel(channelId: $channelId) {
|
||||
id
|
||||
...UpdateChannelBasicInfoFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(UpdateChannelBasicInfoFragmentNode)}
|
||||
`,
|
||||
{ channelId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const channel = data.updateChannel.resumeUpdateChannel;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not find a channel with id: ${channelId}`);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
export default class ChannelResume extends EasCommand {
|
||||
static override description = 'resume a channel to start sending updates';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the channel to edit',
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
description: 'Name of the branch to point to',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args,
|
||||
flags: { json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelResume);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelResume, {
|
||||
nonInteractive,
|
||||
});
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const existingChannel = args.name
|
||||
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: args.name,
|
||||
})
|
||||
: await selectChannelOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel to edit',
|
||||
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
|
||||
});
|
||||
|
||||
const channel = await resumeUpdateChannelAsync(graphqlClient, {
|
||||
channelId: existingChannel.id,
|
||||
});
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(channel);
|
||||
} else {
|
||||
Log.withTick(chalk`Channel {bold ${channel.name}} is now active.\n`);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`Users with builds on channel {bold ${channel.name}} will now receive updates.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelRollout, {
|
||||
nonInteractive: argsAndFlags.nonInteractive,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
if (argsAndFlags.json) {
|
||||
enableJsonOutput();
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class ChannelView extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class ChannelView extends EasCommand {
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(ChannelView, {
|
||||
nonInteractive,
|
||||
|
||||
@@ -6,7 +6,6 @@ import chalk from 'chalk';
|
||||
|
||||
import { evaluateConfigWithEnvVarsAsync } from '../build/evaluateConfigWithEnvVarsAsync';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { createGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
@@ -35,7 +34,7 @@ export default class Config extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.SessionManagment,
|
||||
...this.ContextOptions.DynamicLoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -48,9 +47,10 @@ export default class Config extends EasCommand {
|
||||
profile: maybeProfile,
|
||||
'non-interactive': nonInteractive,
|
||||
} = flags;
|
||||
const { getDynamicPublicProjectConfigAsync, projectDir, sessionManager } =
|
||||
const { getDynamicPublicProjectConfigAsync, projectDir, getDynamicLoggedInAsync } =
|
||||
await this.getContextAsync(Config, {
|
||||
nonInteractive,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
@@ -88,10 +88,7 @@ export default class Config extends EasCommand {
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
}
|
||||
} else {
|
||||
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
|
||||
nonInteractive,
|
||||
});
|
||||
const graphqlClient = createGraphqlClient(authenticationInfo);
|
||||
const { graphqlClient } = await getDynamicLoggedInAsync();
|
||||
const { exp: appConfig } = await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: profile,
|
||||
buildProfileName: profileName,
|
||||
|
||||
@@ -39,6 +39,7 @@ export default class InitializeBuildCredentials extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(InitializeBuildCredentials, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const platform = await selectPlatformAsync(flags.platform);
|
||||
|
||||
@@ -22,12 +22,13 @@ export default class Credentials extends EasCommand {
|
||||
const { flags } = await this.parse(Credentials);
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
privateProjectConfig,
|
||||
optionalPrivateProjectConfig: privateProjectConfig,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
analytics,
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(Credentials, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
await new SelectPlatform(
|
||||
|
||||
+5
-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,13 +70,14 @@ 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,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(WorkerAlias, {
|
||||
nonInteractive: true,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const { projectId } = await getDynamicPrivateProjectConfigAsync();
|
||||
+38
-18
@@ -43,16 +43,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 +65,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 +89,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,13 +111,13 @@ 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,
|
||||
loggedIn: { graphqlClient },
|
||||
projectDir,
|
||||
} = await this.getContextAsync(WorkerDeploy, flags);
|
||||
} = await this.getContextAsync(WorkerDeploy, { ...flags, withServerSideEnvironment: null });
|
||||
|
||||
const projectDist = await resolveExportedProjectAsync(flags, projectDir);
|
||||
const { projectId } = await getDynamicPrivateProjectConfigAsync();
|
||||
@@ -250,7 +250,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 +258,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,
|
||||
@@ -382,6 +399,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 +430,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 +455,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}}`);
|
||||
}
|
||||
@@ -15,9 +15,10 @@ export default class DeviceCreate extends EasCommand {
|
||||
// this command is interactive by design
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
privateProjectConfig,
|
||||
optionalPrivateProjectConfig: privateProjectConfig,
|
||||
} = await this.getContextAsync(DeviceCreate, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const ctx = await createContextAsync({
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class DeviceDelete extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class DeviceDelete extends EasCommand {
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
let { 'apple-team-id': appleTeamIdentifier, udid } = flags;
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(DeviceDelete, {
|
||||
nonInteractive: paginatedQueryOptions.nonInteractive,
|
||||
@@ -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;
|
||||
|
||||
@@ -21,7 +21,7 @@ export default class BuildList extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class BuildList extends EasCommand {
|
||||
const { flags } = await this.parse(BuildList);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BuildList, {
|
||||
nonInteractive: paginatedQueryOptions.nonInteractive,
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class DeviceRename extends EasCommand {
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class DeviceRename extends EasCommand {
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
let { 'apple-team-id': appleTeamIdentifier, udid, name } = flags;
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(DeviceRename, {
|
||||
nonInteractive: paginatedQueryOptions.nonInteractive,
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +11,7 @@ export default class DeviceView extends EasCommand {
|
||||
static override args = [{ name: 'UDID' }];
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
|
||||
throw new Error('Device UDID is missing');
|
||||
}
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(DeviceView, {
|
||||
nonInteractive: true,
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import {
|
||||
parseVisibility,
|
||||
promptVariableEnvironmentAsync,
|
||||
promptVariableNameAsync,
|
||||
promptVariableTypeAsync,
|
||||
promptVariableValueAsync,
|
||||
} from '../../../utils/prompts';
|
||||
import EnvCreate from '../create';
|
||||
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../utils/prompts');
|
||||
|
||||
describe(EnvCreate, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const variableId = 'testId';
|
||||
const testAccountId = 'test-account-id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest
|
||||
.mocked(parseVisibility)
|
||||
.mockImplementation(jest.requireActual('../../../utils/prompts').parseVisibility);
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.createForAppAsync)
|
||||
.mockImplementation(async (_client, input, _appId) => ({
|
||||
...input,
|
||||
id: variableId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
}));
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.createSharedVariableAsync)
|
||||
.mockImplementation(async (_client, input, _appId) => ({
|
||||
...input,
|
||||
id: variableId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
}));
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.updateAsync)
|
||||
.mockImplementation(async (_client, input) => ({
|
||||
...input,
|
||||
id: variableId,
|
||||
name: 'VarName',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
type: EnvironmentSecretType.String,
|
||||
}));
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockImplementation(async () => []);
|
||||
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockImplementation(async () => []);
|
||||
});
|
||||
|
||||
describe('in interactive mode', () => {
|
||||
it('creates a project variable', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
'--value',
|
||||
'VarValue',
|
||||
'--environment',
|
||||
'production',
|
||||
'--visibility',
|
||||
'secret',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
|
||||
it('updates an existing variable in the same environment', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
'--value',
|
||||
'VarValue',
|
||||
'--environment',
|
||||
'production',
|
||||
'--visibility',
|
||||
'secret',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
const otherVariableId = 'otherId';
|
||||
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: otherVariableId,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
},
|
||||
]);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'promptForOverwriteAsync').mockResolvedValue(true);
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.updateAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async (_client, input) => ({
|
||||
...input,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
}));
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
filterNames: ['VarName'],
|
||||
});
|
||||
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: otherVariableId,
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an account-wide variable', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
'--value',
|
||||
'VarValue',
|
||||
'--environment',
|
||||
'production',
|
||||
'--scope',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
privateProjectConfig: { projectId: testProjectId },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
isGlobal: true,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
'test-account-id'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if an account-wide variable already exists', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
'--value',
|
||||
'VarValue',
|
||||
'--environment',
|
||||
'production',
|
||||
'--scope',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
privateProjectConfig: { projectId: testProjectId },
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.sharedAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: 'otherId',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('updates if an account-wide variable already exists and --force flag is set', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
'--value',
|
||||
'VarValue',
|
||||
'--environment',
|
||||
'production',
|
||||
'--force',
|
||||
'--scope',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
const otherVariableId = 'otherId';
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
privateProjectConfig: { projectId: testProjectId },
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.sharedAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: otherVariableId,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
},
|
||||
]);
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: otherVariableId,
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an account-wide variable and links it', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'--name',
|
||||
'VarName',
|
||||
'--value',
|
||||
'VarValue',
|
||||
'--environment',
|
||||
'production',
|
||||
'--environment',
|
||||
'development',
|
||||
'--scope',
|
||||
'account',
|
||||
'--link',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
isGlobal: true,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
testAccountId
|
||||
);
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
'testId',
|
||||
testProjectId,
|
||||
EnvironmentVariableEnvironment.Production
|
||||
);
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
'testId',
|
||||
testProjectId,
|
||||
EnvironmentVariableEnvironment.Development
|
||||
);
|
||||
});
|
||||
|
||||
it('prompts for missing arguments', async () => {
|
||||
const command = new EnvCreate([], mockConfig);
|
||||
|
||||
jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
|
||||
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
|
||||
jest
|
||||
.mocked(promptVariableTypeAsync)
|
||||
.mockImplementation(async () => EnvironmentSecretType.String);
|
||||
|
||||
jest
|
||||
.mocked(promptVariableEnvironmentAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptVariableNameAsync).toHaveBeenCalled();
|
||||
expect(promptVariableValueAsync).toHaveBeenCalled();
|
||||
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvDelete from '../delete';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvDelete, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockContext = {
|
||||
privateProjectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('deletes a variable by name in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvDelete(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'production',
|
||||
'--non-interactive',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
|
||||
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
|
||||
});
|
||||
|
||||
it('prompts for variable selection when name is not provided', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvDelete([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
|
||||
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
|
||||
});
|
||||
|
||||
it('throws an error when variable name is not found', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvDelete(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow('Variable "NON_EXISTENT_VARIABLE" not found.');
|
||||
});
|
||||
|
||||
it('throws an error when multiple variables with the same name are found', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
{ id: '2', name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Multiple variables with name "TEST_VARIABLE" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.'
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels deletion when user does not confirm', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
const command = new EnvDelete(['--non-interactive'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../../utils/prompts';
|
||||
import EnvGet from '../get';
|
||||
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../utils/prompts');
|
||||
|
||||
describe(EnvGet, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockVariables: EnvironmentVariableFragment[] = [
|
||||
{
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
];
|
||||
|
||||
it('retrieves environment variables successfully', async () => {
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
|
||||
const command = new EnvGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
filterNames: ['TEST_VAR_1'],
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
});
|
||||
|
||||
it('handles errors during retrieval', async () => {
|
||||
const errorMessage =
|
||||
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag";
|
||||
|
||||
const command = new EnvGet([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
privateProjectConfig: { projectId: testProjectId },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(errorMessage);
|
||||
});
|
||||
|
||||
it('prompts for variable name and environment if the name is ambigous', async () => {
|
||||
jest
|
||||
.mocked(promptVariableEnvironmentAsync)
|
||||
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
|
||||
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
|
||||
|
||||
const command = new EnvGet([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
privateProjectConfig: { projectId: testProjectId },
|
||||
});
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).mockResolvedValueOnce([
|
||||
...mockVariables,
|
||||
{
|
||||
id: 'var2',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
|
||||
expect(promptVariableNameAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvLink from '../link';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvLink, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockContext = {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
};
|
||||
|
||||
const successMessage = (env: EnvironmentVariableEnvironment): string =>
|
||||
`Linked variable ${chalk.bold('TEST_VARIABLE')} to project ${chalk.bold(
|
||||
'@testuser/testpp'
|
||||
)} in ${env.toLocaleLowerCase()}.`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('links an account-wide variable to the current project in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
|
||||
mockVariables[0]
|
||||
);
|
||||
|
||||
const command = new EnvLink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.run();
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
filterNames: ['TEST_VARIABLE'],
|
||||
});
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Development)
|
||||
);
|
||||
});
|
||||
|
||||
it('links an account-wide variable to the current project to a specified environment', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
|
||||
mockVariables[0]
|
||||
);
|
||||
|
||||
const command = new EnvLink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.run();
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
filterNames: ['TEST_VARIABLE'],
|
||||
});
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Production
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Production)
|
||||
);
|
||||
});
|
||||
|
||||
it('uses --variable-environment to select the variable with ambigous name', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Preview],
|
||||
},
|
||||
{
|
||||
id: 'other-id',
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockImplementation(
|
||||
(_client, { environment }) => {
|
||||
return mockVariables.filter(v => v.environments.includes(environment));
|
||||
}
|
||||
);
|
||||
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
|
||||
mockVariables[0]
|
||||
);
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvLink(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--environment',
|
||||
'production',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
'other-id',
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Production
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Production)
|
||||
);
|
||||
});
|
||||
|
||||
it('prompts for variable selection when the name is ambigous', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Preview],
|
||||
},
|
||||
{
|
||||
id: 'other-id',
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
|
||||
mockVariables[0]
|
||||
);
|
||||
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvLink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(selectAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Preview
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Preview)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when variable name is not found', async () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
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(
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
|
||||
it('uses environments from prompt to both link and unlink environments', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Preview],
|
||||
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
|
||||
mockVariables[0]
|
||||
);
|
||||
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
});
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvLink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Production
|
||||
);
|
||||
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Preview
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when variable name is not found', async () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
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(
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import EnvList from '../list';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
const mockVariables: EnvironmentVariableFragment[] = [
|
||||
{
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
{
|
||||
id: 'var2',
|
||||
name: 'TEST_VAR_2',
|
||||
value: 'value2',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
];
|
||||
|
||||
describe(EnvList, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('lists project environment variables successfully', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('lists project environment variables in specified environments', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList(['--environment', 'production'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('lists project environment variables including sensitive values', async () => {
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList(['--include-sensitive'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
includeFileContent: false,
|
||||
}
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('lists account-wide environment variables successfully', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList(['--scope', 'account'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('lists account-wide environment variables including sensitive values', async () => {
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync)
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList(['--include-sensitive', '--scope', 'account'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvUnlink from '../unlink';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvUnlink, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockContext = {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
};
|
||||
|
||||
const successMessage = (env: EnvironmentVariableEnvironment): string =>
|
||||
`Unlinked variable ${chalk.bold('TEST_VARIABLE')} from project ${chalk.bold(
|
||||
'@testuser/testpp'
|
||||
)} in ${env.toLocaleLowerCase()}.`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('unlinks an account-wide variable from the current project in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(
|
||||
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
|
||||
).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvUnlink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.run();
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
filterNames: ['TEST_VARIABLE'],
|
||||
});
|
||||
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Development)
|
||||
);
|
||||
});
|
||||
|
||||
it('unlinks an account-wide variable from the current project in a specified environment', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
linkedEnvironments: [EnvironmentVariableEnvironment.Production],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(
|
||||
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
|
||||
).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvUnlink(
|
||||
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.run();
|
||||
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Production)
|
||||
);
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
filterNames: ['TEST_VARIABLE'],
|
||||
});
|
||||
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Production
|
||||
);
|
||||
});
|
||||
|
||||
it('prompts for variable selection when the name is ambigous', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Preview],
|
||||
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
|
||||
},
|
||||
{
|
||||
id: 'other-id',
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(
|
||||
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
|
||||
).mockResolvedValue(mockVariables[0]);
|
||||
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({
|
||||
environments: [],
|
||||
});
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvUnlink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(selectAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Preview
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
successMessage(EnvironmentVariableEnvironment.Preview)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when variable name is not found', async () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
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(
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
|
||||
it('uses environments from prompt to both link and unlink environments', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Preview],
|
||||
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
|
||||
mockVariables[0]
|
||||
);
|
||||
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
});
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvUnlink([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Production
|
||||
);
|
||||
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
variableId,
|
||||
projectId,
|
||||
EnvironmentVariableEnvironment.Preview
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when variable name is not found', async () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
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(
|
||||
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import EnvUpdate from '../update';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvUpdate, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockContext = {
|
||||
privateProjectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('updates a project variable by selected name in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--value',
|
||||
'new-value',
|
||||
'--name',
|
||||
'NEW_VARIABLE',
|
||||
'--environment',
|
||||
'production',
|
||||
'--visibility',
|
||||
'plaintext',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: variableId,
|
||||
name: 'NEW_VARIABLE',
|
||||
value: 'new-value',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
`Updated variable ${chalk.bold('TEST_VARIABLE')} on project @testuser/testpp.`
|
||||
);
|
||||
});
|
||||
|
||||
it('updates an account-wide variable by selected name in non-interactive mode', async () => {
|
||||
const mockVariables = [
|
||||
{
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--value',
|
||||
'new-value',
|
||||
'--name',
|
||||
'NEW_VARIABLE',
|
||||
'--environment',
|
||||
'production',
|
||||
'--scope',
|
||||
'account',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: variableId,
|
||||
name: 'NEW_VARIABLE',
|
||||
value: 'new-value',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
});
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
|
||||
);
|
||||
});
|
||||
|
||||
it('prompts for variable selection when selected name is not provided', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
(promptAsync as jest.Mock).mockResolvedValue({ name: mockVariables[0].name });
|
||||
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvUpdate([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.objectContaining({ id: variableId })
|
||||
);
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
`Updated variable ${chalk.bold('TEST_VARIABLE')} on project @testuser/testpp.`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when variable name is not found', async () => {
|
||||
const mockVariables: never[] = [];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
|
||||
const command = new EnvUpdate(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Variable with name NON_EXISTENT_VARIABLE does not exist on project @testuser/testpp.'
|
||||
);
|
||||
});
|
||||
});
|
||||
+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"`;
|
||||
+291
-138
@@ -1,14 +1,18 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASMultiEnvironmentFlag,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableScopeFlag,
|
||||
EASVariableVisibilityFlag,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
@@ -22,27 +26,50 @@ import {
|
||||
} from '../../project/projectUtils';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import {
|
||||
parseVisibility,
|
||||
promptVariableEnvironmentAsync,
|
||||
promptVariableNameAsync,
|
||||
promptVariableTypeAsync,
|
||||
promptVariableValueAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../utils/prompts';
|
||||
import { isEnvironment, performForEnvironmentsAsync } from '../../utils/variableUtils';
|
||||
|
||||
type CreateFlags = {
|
||||
interface RawCreateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
link?: boolean;
|
||||
force?: boolean;
|
||||
visibility?: EnvironmentVariableVisibility;
|
||||
scope?: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
type?: 'string' | 'file';
|
||||
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 = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to create the variable in. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
name: Flags.string({
|
||||
@@ -52,42 +79,51 @@ 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',
|
||||
default: false,
|
||||
}),
|
||||
type: Flags.enum<'string' | 'file'>({
|
||||
description: 'The type of variable',
|
||||
options: ['string', 'file'],
|
||||
}),
|
||||
...EASVariableVisibilityFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.Analytics,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(EnvironmentVariableCreate);
|
||||
const { args, flags } = await this.parse(EnvCreate);
|
||||
|
||||
let {
|
||||
const validatedFlags = this.sanitizeFlags(flags);
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
scope,
|
||||
'non-interactive': nonInteractive,
|
||||
environment,
|
||||
environment: environments,
|
||||
visibility,
|
||||
link,
|
||||
force,
|
||||
} = this.validateFlags(flags);
|
||||
type,
|
||||
fileName,
|
||||
} = await this.promptForMissingFlagsAsync(validatedFlags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableCreate, {
|
||||
} = await this.getContextAsync(EnvCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -96,90 +132,77 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
|
||||
]);
|
||||
|
||||
if (!name) {
|
||||
name = await promptVariableNameAsync(nonInteractive);
|
||||
}
|
||||
|
||||
let overwrite = false;
|
||||
visibility = visibility ?? EnvironmentVariableVisibility.Public;
|
||||
|
||||
if (!value) {
|
||||
value = await promptVariableValueAsync({
|
||||
nonInteractive,
|
||||
hidden: visibility !== EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
}
|
||||
|
||||
if (scope === EnvironmentVariableScope.Project) {
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
}
|
||||
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
filterNames: [name],
|
||||
});
|
||||
const existingVariable = existingVariables.find(variable => variable.name === name);
|
||||
|
||||
const existingVariable = existingVariables.find(
|
||||
variable => !environments || variable.environments?.some(env => environments?.includes(env))
|
||||
);
|
||||
|
||||
if (existingVariable) {
|
||||
if (existingVariable.scope === EnvironmentVariableScope.Project) {
|
||||
await this.promptForOverwriteAsync({
|
||||
nonInteractive,
|
||||
force,
|
||||
message: `Variable ${name} already exists on this project.`,
|
||||
suggestion: 'Do you want to overwrite it?',
|
||||
});
|
||||
overwrite = true;
|
||||
}
|
||||
if (existingVariable.scope === EnvironmentVariableScope.Shared) {
|
||||
if (!nonInteractive) {
|
||||
const confirmation = await confirmAsync({
|
||||
message: `Shared variable ${name} already exists on this project. Do you want to unlink it first?`,
|
||||
});
|
||||
await this.promptForOverwriteAsync({
|
||||
nonInteractive,
|
||||
force,
|
||||
message: `Account-wide variable with ${name} name already exists on this account.`,
|
||||
suggestion: 'Do you want to unlink it first?',
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
Log.log('Aborting');
|
||||
throw new Error(`Shared variable ${name} already exists on this project.`);
|
||||
}
|
||||
} else if (!force) {
|
||||
throw new Error(
|
||||
`Shared variable ${name} already exists on this project. Use --force to overwrite it.`
|
||||
);
|
||||
}
|
||||
|
||||
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
existingVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
Log.withTick(
|
||||
`Unlinking shared variable ${chalk.bold(name)} on project ${chalk.bold(
|
||||
`Unlinking account-wide variable ${chalk.bold(name)} on project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
} else {
|
||||
if (!nonInteractive) {
|
||||
const confirmation = await confirmAsync({
|
||||
message: `Variable ${name} already exists on this project. Do you want to overwrite it?`,
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
Log.log('Aborting');
|
||||
throw new Error(
|
||||
`Variable ${name} already exists on this project. Use --force to overwrite it.`
|
||||
);
|
||||
}
|
||||
} else if (!force) {
|
||||
throw new Error(
|
||||
`Variable ${name} already exists on this project. Use --force to overwrite it.`
|
||||
await performForEnvironmentsAsync(environments, async environment => {
|
||||
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
existingVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
}
|
||||
overwrite = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const variable = await EnvironmentVariableMutation.createForAppAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
environment,
|
||||
visibility,
|
||||
overwrite,
|
||||
},
|
||||
projectId
|
||||
);
|
||||
const variable =
|
||||
overwrite && existingVariable
|
||||
? await EnvironmentVariableMutation.updateAsync(graphqlClient, {
|
||||
id: existingVariable.id,
|
||||
name,
|
||||
value,
|
||||
visibility,
|
||||
environments,
|
||||
type,
|
||||
fileName,
|
||||
})
|
||||
: await EnvironmentVariableMutation.createForAppAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
environments,
|
||||
visibility,
|
||||
type: type ?? EnvironmentSecretType.String,
|
||||
fileName,
|
||||
},
|
||||
projectId
|
||||
);
|
||||
|
||||
if (!variable) {
|
||||
throw new Error(
|
||||
`Could not create variable with name ${name} on project ${projectDisplayName}`
|
||||
@@ -190,41 +213,48 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
`Created a new variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
} else if (scope === EnvironmentVariableScope.Shared) {
|
||||
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
const existingVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
filterNames: [name],
|
||||
});
|
||||
const existingVariable = sharedVariables.find(variable => variable.name === name);
|
||||
|
||||
const existingVariable = existingVariables.find(
|
||||
variable => !environments || variable.environments?.some(env => environments?.includes(env))
|
||||
);
|
||||
|
||||
if (existingVariable) {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
|
||||
if (environment && !link) {
|
||||
const confirmation = await confirmAsync({
|
||||
message: `Unexpected argument: --environment can only be used with --link flag. Do you want to link the variable to the current project?`,
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
Log.log('Aborting');
|
||||
throw new Error('Unexpected argument: --environment can only be used with --link flag.');
|
||||
if (force) {
|
||||
overwrite = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
`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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!environment && link) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
}
|
||||
|
||||
const variable = await EnvironmentVariableMutation.createSharedVariableAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
visibility,
|
||||
},
|
||||
ownerAccount.id
|
||||
);
|
||||
const variable =
|
||||
overwrite && existingVariable
|
||||
? await EnvironmentVariableMutation.updateAsync(graphqlClient, {
|
||||
id: existingVariable.id,
|
||||
name,
|
||||
value,
|
||||
visibility,
|
||||
environments,
|
||||
type,
|
||||
})
|
||||
: await EnvironmentVariableMutation.createSharedVariableAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
name,
|
||||
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
|
||||
);
|
||||
|
||||
if (!variable) {
|
||||
throw new Error(
|
||||
@@ -236,41 +266,164 @@ export default class EnvironmentVariableCreate extends EasCommand {
|
||||
`Created a new variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
|
||||
if (link && environment) {
|
||||
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
|
||||
)}.`
|
||||
);
|
||||
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
variable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
|
||||
await performForEnvironmentsAsync(environments, async environment => {
|
||||
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
variable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateFlags(flags: CreateFlags): CreateFlags {
|
||||
if (flags.scope !== EnvironmentVariableScope.Shared && flags.link) {
|
||||
private async promptForOverwriteAsync({
|
||||
nonInteractive,
|
||||
force,
|
||||
message,
|
||||
suggestion,
|
||||
}: {
|
||||
nonInteractive: boolean;
|
||||
force: boolean;
|
||||
message: string;
|
||||
suggestion: string;
|
||||
}): Promise<void> {
|
||||
if (!nonInteractive) {
|
||||
const confirmation = await confirmAsync({
|
||||
message: `${message} ${suggestion}`,
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
Log.log('Aborting');
|
||||
throw new Error(`${message}`);
|
||||
}
|
||||
} else if (!force) {
|
||||
throw new Error(`${message} Use --force to overwrite it.`);
|
||||
}
|
||||
}
|
||||
|
||||
private async promptForMissingFlagsAsync(
|
||||
{
|
||||
name,
|
||||
value,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
...rest
|
||||
}: CreateFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): Promise<
|
||||
Required<
|
||||
Omit<CreateFlags, 'type' | 'visibility'> & {
|
||||
type: EnvironmentSecretType | undefined;
|
||||
visibility: EnvironmentVariableVisibility;
|
||||
}
|
||||
> & { fileName?: string }
|
||||
> {
|
||||
if (!name) {
|
||||
name = await promptVariableNameAsync(nonInteractive);
|
||||
}
|
||||
|
||||
let newType;
|
||||
let newVisibility = visibility ? parseVisibility(visibility) : undefined;
|
||||
|
||||
if (type === 'file') {
|
||||
newType = EnvironmentSecretType.FileBase64;
|
||||
} else if (type === 'string') {
|
||||
newType = EnvironmentSecretType.String;
|
||||
}
|
||||
|
||||
if (!type && !value && !nonInteractive) {
|
||||
newType = await promptVariableTypeAsync(nonInteractive);
|
||||
}
|
||||
|
||||
if (!newVisibility) {
|
||||
newVisibility = await promptVariableVisibilityAsync(nonInteractive);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
value = await promptVariableValueAsync({
|
||||
nonInteractive,
|
||||
hidden: newVisibility !== EnvironmentVariableVisibility.Public,
|
||||
filePath: newType === EnvironmentSecretType.FileBase64,
|
||||
});
|
||||
}
|
||||
|
||||
let environmentFilePath: string | undefined;
|
||||
let fileName: string | undefined;
|
||||
|
||||
if (newType === EnvironmentSecretType.FileBase64) {
|
||||
environmentFilePath = path.resolve(value);
|
||||
if (!(await fs.pathExists(environmentFilePath))) {
|
||||
throw new Error(`File "${value}" does not exist`);
|
||||
}
|
||||
fileName = path.basename(environmentFilePath);
|
||||
}
|
||||
|
||||
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
|
||||
|
||||
if (environment && !isEnvironment(environment.toUpperCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
let newEnvironments = environments
|
||||
? environments
|
||||
: environment
|
||||
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
|
||||
: undefined;
|
||||
|
||||
if (!newEnvironments) {
|
||||
newEnvironments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
|
||||
|
||||
if (!newEnvironments || newEnvironments.length === 0) {
|
||||
throw new Error('No environments selected');
|
||||
}
|
||||
}
|
||||
|
||||
newVisibility = newVisibility ?? EnvironmentVariableVisibility.Public;
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
environment: newEnvironments,
|
||||
visibility: newVisibility,
|
||||
link: rest.link ?? false,
|
||||
force: rest.force ?? false,
|
||||
'non-interactive': nonInteractive,
|
||||
type: newType,
|
||||
fileName,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+98
-49
@@ -1,96 +1,126 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableScopeFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type DeleteFlags = {
|
||||
name?: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
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 = {
|
||||
name: Flags.string({
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable to delete',
|
||||
}),
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentFlag,
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to delete',
|
||||
}),
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable to delete. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(EnvironmentVariableDelete);
|
||||
let { name, environment, 'non-interactive': nonInteractive, scope } = this.validateFlags(flags);
|
||||
const { args, flags } = await this.parse(EnvDelete);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
'variable-name': name,
|
||||
'variable-environment': environment,
|
||||
'non-interactive': nonInteractive,
|
||||
scope,
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableDelete, {
|
||||
} = await this.getContextAsync(EnvDelete, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (scope === EnvironmentVariableScope.Project) {
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
}
|
||||
}
|
||||
|
||||
const variables =
|
||||
scope === EnvironmentVariableScope.Project && environment
|
||||
scope === EnvironmentVariableScope.Project
|
||||
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
})
|
||||
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId });
|
||||
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
});
|
||||
|
||||
let selectedVariable;
|
||||
|
||||
if (!name) {
|
||||
({ name } = await promptAsync({
|
||||
({ variable: selectedVariable } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'name',
|
||||
name: 'variable',
|
||||
message: 'Pick the variable to be deleted:',
|
||||
choices: variables
|
||||
.filter(({ scope: variableScope }) => scope === variableScope)
|
||||
.map(variable => ({
|
||||
title: variable.name,
|
||||
value: variable.name,
|
||||
})),
|
||||
.map(variable => {
|
||||
return {
|
||||
title: formatVariableName(variable),
|
||||
value: variable,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
} else {
|
||||
const selectedVariables = variables.filter(
|
||||
variable =>
|
||||
variable.name === name && (!environment || variable.environments?.includes(environment))
|
||||
);
|
||||
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
`Environment variable wasn't selected. Run the command again and select existing variable or run it with ${chalk.bold(
|
||||
'--name VARIABLE_NAME'
|
||||
)} flag to fix the issue.`
|
||||
);
|
||||
if (selectedVariables.length !== 1) {
|
||||
if (selectedVariables.length === 0) {
|
||||
throw new Error(`Variable "${name}" not found.`);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Multiple variables with name "${name}" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
selectedVariable = selectedVariables[0];
|
||||
}
|
||||
|
||||
const selectedVariable = variables.find(variable => variable.name === name);
|
||||
|
||||
if (!selectedVariable) {
|
||||
throw new Error(`Variable "${name}" not found.`);
|
||||
}
|
||||
assert(selectedVariable, `Variable "${name}" not found.`);
|
||||
|
||||
if (!nonInteractive) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -106,7 +136,7 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of variable ${selectedVariable.name}.`);
|
||||
throw new Error(`Variable "${name}" not deleted.`);
|
||||
throw new Error(`Variable "${selectedVariable.name}" not deleted.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,17 +145,36 @@ export default class EnvironmentVariableDelete extends EasCommand {
|
||||
Log.withTick(`️Deleted variable ${selectedVariable.name}".`);
|
||||
}
|
||||
|
||||
private validateFlags(flags: DeleteFlags): DeleteFlags {
|
||||
private sanitizeInputs(
|
||||
flags: RawDeleteFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): DeleteFlags {
|
||||
if (flags['non-interactive']) {
|
||||
if (!flags.name) {
|
||||
if (!flags['variable-name']) {
|
||||
throw new Error(
|
||||
`Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold(
|
||||
'--name VARIABLE_NAME'
|
||||
'--variable-name VARIABLE_NAME'
|
||||
)} flag to fix the issue`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
const scope =
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return { ...flags, 'variable-environment': environment, scope };
|
||||
}
|
||||
|
||||
return { ...flags, scope };
|
||||
}
|
||||
}
|
||||
+105
-53
@@ -3,96 +3,152 @@ import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type ParsedFlags =
|
||||
| {
|
||||
nonInteractive: true;
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
command: string;
|
||||
}
|
||||
| {
|
||||
nonInteractive: false;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
command: string;
|
||||
};
|
||||
|
||||
interface RawFlags {
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
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.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EASEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'BASH_COMMAND',
|
||||
name: 'environment',
|
||||
required: true,
|
||||
description:
|
||||
"Environment to execute the command in. One of 'production', 'preview', or 'development'.",
|
||||
},
|
||||
{
|
||||
name: 'bash_command',
|
||||
required: true,
|
||||
description: 'bash command to execute with the environment variables from the environment',
|
||||
},
|
||||
];
|
||||
|
||||
private isNonInteractive: boolean = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags,
|
||||
args: { BASH_COMMAND: command },
|
||||
} = await this.parse(EnvExec);
|
||||
|
||||
const parsedFlags = this.sanitizeFlags(flags);
|
||||
const { flags, args } = await this.parse(EnvExec);
|
||||
const parsedFlags = this.sanitizeFlagsAndArgs(flags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvExec, {
|
||||
nonInteractive: parsedFlags.nonInteractive,
|
||||
});
|
||||
|
||||
this.isNonInteractive = parsedFlags.nonInteractive;
|
||||
|
||||
const environment =
|
||||
parsedFlags.environment ?? (await promptVariableEnvironmentAsync(parsedFlags.nonInteractive));
|
||||
parsedFlags.environment ??
|
||||
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
|
||||
const environmentVariables = await this.loadEnvironmentVariablesAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
environment,
|
||||
nonInteractive: parsedFlags.nonInteractive,
|
||||
});
|
||||
|
||||
await this.runCommandWithEnvVarsAsync({ command, environmentVariables });
|
||||
if (parsedFlags.nonInteractive) {
|
||||
await this.runCommandNonInteractiveWithEnvVarsAsync({
|
||||
command: parsedFlags.command,
|
||||
environmentVariables,
|
||||
});
|
||||
} else {
|
||||
await this.runCommandWithEnvVarsAsync({
|
||||
command: parsedFlags.command,
|
||||
environmentVariables,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeFlags(rawFlags: RawFlags): ParsedFlags {
|
||||
const environment = rawFlags.environment;
|
||||
if (rawFlags['non-interactive']) {
|
||||
if (!environment) {
|
||||
throw new Error(
|
||||
'You must specify an environment when running in non-interactive mode. Use the --environment flag.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
nonInteractive: true,
|
||||
environment,
|
||||
};
|
||||
private sanitizeFlagsAndArgs(
|
||||
rawFlags: RawFlags,
|
||||
{ bash_command, environment }: Record<string, string>
|
||||
): ParsedFlags {
|
||||
if (rawFlags['non-interactive'] && (!bash_command || !environment)) {
|
||||
throw new Error(
|
||||
"You must specify both environment and bash command when running in non-interactive mode. Run command as `eas env:exec ENVIRONMENT 'bash command'`."
|
||||
);
|
||||
}
|
||||
|
||||
environment = environment?.toUpperCase();
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
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: false,
|
||||
environment: rawFlags.environment,
|
||||
nonInteractive: rawFlags['non-interactive'],
|
||||
environment,
|
||||
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,
|
||||
@@ -101,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,
|
||||
@@ -137,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, {
|
||||
@@ -148,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) {
|
||||
|
||||
+107
-50
@@ -4,64 +4,84 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableFormatFlag,
|
||||
EASVariableScopeFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { formatVariable } from '../../utils/formatVariable';
|
||||
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
|
||||
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type GetFlags = {
|
||||
name?: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
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.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
name: Flags.string({
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable',
|
||||
}),
|
||||
...EASVariableFormatFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
...EASEnvironmentFlag,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(EnvironmentVariableGet);
|
||||
const { args, flags } = await this.parse(EnvGet);
|
||||
|
||||
let {
|
||||
environment,
|
||||
name,
|
||||
'variable-environment': environment,
|
||||
'variable-name': name,
|
||||
'non-interactive': nonInteractive,
|
||||
format,
|
||||
scope,
|
||||
} = this.validateFlags(flags);
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableGet, {
|
||||
} = await this.getContextAsync(EnvGet, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -69,16 +89,36 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
name = await promptVariableNameAsync(nonInteractive);
|
||||
}
|
||||
|
||||
if (!environment && scope === EnvironmentVariableScope.Project) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: false,
|
||||
});
|
||||
}
|
||||
const variable = await getVariableAsync(graphqlClient, scope, projectId, name, environment);
|
||||
|
||||
if (!variable) {
|
||||
const variables = await getVariablesAsync(graphqlClient, scope, projectId, name, environment);
|
||||
|
||||
if (variables.length === 0) {
|
||||
Log.error(`Variable with name "${name}" not found`);
|
||||
return;
|
||||
}
|
||||
if (!variable.value) {
|
||||
|
||||
let variable;
|
||||
|
||||
if (variables.length > 1) {
|
||||
const variableInEnvironment = variables.find(v => v.environments?.includes(environment!));
|
||||
if (!variableInEnvironment) {
|
||||
throw new Error(
|
||||
`Variable with name "${name}" not found in environment "${environment.toLocaleLowerCase()}"`
|
||||
);
|
||||
}
|
||||
|
||||
variable = variableInEnvironment;
|
||||
} else {
|
||||
variable = variables[0];
|
||||
}
|
||||
|
||||
if (variable.visibility === EnvironmentVariableVisibility.Secret) {
|
||||
throw new Error(
|
||||
`${chalk.bold(
|
||||
variable.name
|
||||
@@ -87,63 +127,80 @@ export default class EnvironmentVariableGet extends EasCommand {
|
||||
}
|
||||
|
||||
if (format === 'short') {
|
||||
Log.log(`${chalk.bold(variable.name)}=${variable.value}`);
|
||||
Log.log(`${chalk.bold(variable.name)}=${formatVariableValue(variable)}`);
|
||||
} else {
|
||||
Log.log(formatVariable(variable));
|
||||
}
|
||||
}
|
||||
|
||||
private validateFlags(flags: GetFlags): GetFlags {
|
||||
if (flags.environment && flags.scope === EnvironmentVariableScope.Shared) {
|
||||
throw new Error(`Unexpected argument: --environment can only be used with project variables`);
|
||||
}
|
||||
private sanitizeInputs(flags: RawGetFlags, { environment }: { environment?: string }): GetFlags {
|
||||
if (flags['non-interactive']) {
|
||||
if (!flags.name) {
|
||||
throw new Error('Variable name is required. Run the command with --name flag.');
|
||||
if (!flags['variable-name']) {
|
||||
throw new Error('Variable name is required. Run the command with --variable-name flag.');
|
||||
}
|
||||
if (!flags.scope) {
|
||||
throw new Error('Scope is required. Run the command with --scope flag.');
|
||||
}
|
||||
if (!flags.environment && flags.scope === EnvironmentVariableScope.Project) {
|
||||
if (!(flags['variable-environment'] ?? environment)) {
|
||||
throw new Error('Environment is required.');
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
if (environment && flags['variable-environment']) {
|
||||
throw new Error(
|
||||
"You can't use both --variable-environment flag when environment is passed as an argument. Run `eas env:get --help` for more information."
|
||||
);
|
||||
}
|
||||
|
||||
const scope =
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
return { ...flags, scope };
|
||||
}
|
||||
}
|
||||
|
||||
async function getVariableAsync(
|
||||
async function getVariablesAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
scope: string,
|
||||
projectId: string,
|
||||
name: string | undefined,
|
||||
environment: string | undefined
|
||||
): Promise<EnvironmentVariableFragment | null> {
|
||||
if (!environment && scope === EnvironmentVariableScope.Project) {
|
||||
throw new Error('Environment is required.');
|
||||
}
|
||||
environment: EnvironmentVariableEnvironment | undefined
|
||||
): Promise<EnvironmentVariableFragment[]> {
|
||||
if (!name) {
|
||||
throw new Error("Variable name is required. Run the command with '--name VARIABLE_NAME' flag.");
|
||||
throw new Error(
|
||||
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag."
|
||||
);
|
||||
}
|
||||
if (environment && scope === EnvironmentVariableScope.Project) {
|
||||
if (scope === EnvironmentVariableScope.Project) {
|
||||
const appVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
filterNames: [name],
|
||||
includeFileContent: true,
|
||||
});
|
||||
return appVariables[0];
|
||||
}
|
||||
|
||||
if (scope === EnvironmentVariableScope.Shared) {
|
||||
return appVariables;
|
||||
} else {
|
||||
const sharedVariables = await EnvironmentVariablesQuery.sharedWithSensitiveAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: projectId,
|
||||
filterNames: [name],
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
return sharedVariables[0];
|
||||
return sharedVariables;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
+153
-34
@@ -2,84 +2,203 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
EASMultiEnvironmentFlag,
|
||||
EASNonInteractiveFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvironmentVariableLink extends EasCommand {
|
||||
static override description = 'link a shared environment variable to the current project';
|
||||
type LinkFlags = {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'non-interactive': boolean;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
};
|
||||
|
||||
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 = {
|
||||
name: Flags.string({
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable',
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to link',
|
||||
}),
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvLink);
|
||||
|
||||
let {
|
||||
flags: { name, 'non-interactive': nonInteractive, environment },
|
||||
} = await this.parse(EnvironmentVariableLink);
|
||||
'variable-name': name,
|
||||
'variable-environment': currentEnvironment,
|
||||
'non-interactive': nonInteractive,
|
||||
environment: environments,
|
||||
} = this.validateInputs(flags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableLink, {
|
||||
} = await this.getContextAsync(EnvLink, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment: currentEnvironment,
|
||||
filterNames: name ? [name] : undefined,
|
||||
});
|
||||
|
||||
if (!name) {
|
||||
name = await selectAsync(
|
||||
'Select shared variable',
|
||||
let selectedVariable = variables[0];
|
||||
|
||||
if (variables.length > 1) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'Multiple variables found, run command with --variable-name and --variable-environment arguments.'
|
||||
);
|
||||
}
|
||||
selectedVariable = await selectAsync(
|
||||
'Select account-wide variable',
|
||||
variables.map(variable => ({
|
||||
title: variable.name,
|
||||
value: variable.name,
|
||||
title: formatVariableName(variable),
|
||||
value: variable,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const selectedVariable = variables.find(variable => variable.name === name);
|
||||
|
||||
if (!selectedVariable) {
|
||||
throw new Error(`Shared variable ${name} doesn't exist`);
|
||||
throw new Error(`Account-wide variable ${name} doesn't exist`);
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
let explicitSelect = false;
|
||||
if (!nonInteractive && !environments) {
|
||||
const selectedEnvironments =
|
||||
(selectedVariable.linkedEnvironments ?? []).length > 0
|
||||
? selectedVariable.linkedEnvironments
|
||||
: selectedVariable.environments;
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
selectedEnvironments: selectedEnvironments ?? [],
|
||||
});
|
||||
explicitSelect = true;
|
||||
}
|
||||
|
||||
const linkedVariable = await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
if (!linkedVariable) {
|
||||
throw new Error(
|
||||
`Could not link variable with name ${selectedVariable.name} to project with id ${projectId}`
|
||||
if (!environments) {
|
||||
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId
|
||||
);
|
||||
Log.withTick(
|
||||
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`Linked variable ${chalk.bold(linkedVariable.name)} to project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
|
||||
try {
|
||||
if (
|
||||
selectedVariable.linkedEnvironments?.includes(environment) ===
|
||||
environments.includes(environment)
|
||||
) {
|
||||
if (!explicitSelect && environments.includes(environment)) {
|
||||
Log.withTick(
|
||||
`Variable ${chalk.bold(
|
||||
selectedVariable.name
|
||||
)} is already linked to ${environment.toLocaleLowerCase()}.`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (environments.includes(environment)) {
|
||||
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
Log.withTick(
|
||||
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} in ${environment.toLocaleLowerCase()}.`
|
||||
);
|
||||
} else if (explicitSelect) {
|
||||
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
Log.withTick(
|
||||
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} in ${environment.toLocaleLowerCase()}.`
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Log.warn(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
private validateInputs(flags: LinkFlags, { environment }: { environment?: string }): LinkFlags {
|
||||
environment = environment?.toUpperCase();
|
||||
|
||||
if (environment && !isEnvironment(environment)) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const environments = flags.environment
|
||||
? flags.environment
|
||||
: environment
|
||||
? [environment as EnvironmentVariableEnvironment]
|
||||
: undefined;
|
||||
|
||||
if (flags['non-interactive']) {
|
||||
if (!flags['variable-name']) {
|
||||
throw new Error(
|
||||
`Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold(
|
||||
'--variable-name VARIABLE_NAME'
|
||||
)} flag to fix the issue`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...flags,
|
||||
environment: environments,
|
||||
};
|
||||
}
|
||||
}
|
||||
+157
-73
@@ -4,27 +4,92 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASMultiEnvironmentFlag,
|
||||
EASVariableFormatFlag,
|
||||
EASVariableScopeFlag,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
} from '../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
EnvironmentVariableWithFileContent,
|
||||
EnvironmentVariablesQuery,
|
||||
} from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { formatVariable } from '../../utils/formatVariable';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import {
|
||||
formatVariable,
|
||||
formatVariableValue,
|
||||
isEnvironment,
|
||||
performForEnvironmentsAsync,
|
||||
} from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvironmentValueList extends EasCommand {
|
||||
static override description = 'list environment variables for the current project';
|
||||
async function getVariablesForScopeAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
scope,
|
||||
includingSensitive,
|
||||
includeFileContent,
|
||||
environment,
|
||||
projectId,
|
||||
}: {
|
||||
scope: EnvironmentVariableScope;
|
||||
includingSensitive: boolean;
|
||||
includeFileContent: boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
projectId: string;
|
||||
}
|
||||
): Promise<EnvironmentVariableWithFileContent[]> {
|
||||
if (scope === EnvironmentVariableScope.Project) {
|
||||
if (includingSensitive) {
|
||||
return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
includeFileContent,
|
||||
});
|
||||
}
|
||||
return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
includeFileContent,
|
||||
});
|
||||
}
|
||||
|
||||
static override hidden = true;
|
||||
return includingSensitive
|
||||
? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
includeFileContent,
|
||||
})
|
||||
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
includeFileContent,
|
||||
});
|
||||
}
|
||||
|
||||
interface RawListFlags {
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive'?: boolean;
|
||||
}
|
||||
|
||||
interface ListFlags {
|
||||
scope: EnvironmentVariableScope;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
export default class EnvList extends EasCommand {
|
||||
static override description = 'list environment variables for the current project or account';
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -33,86 +98,105 @@ export default class EnvironmentValueList extends EasCommand {
|
||||
description: 'Display sensitive values in the output',
|
||||
default: false,
|
||||
}),
|
||||
'include-file-content': Flags.boolean({
|
||||
description: 'Display files content in the output',
|
||||
default: false,
|
||||
}),
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASVariableFormatFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to list the variables from. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvList);
|
||||
|
||||
let {
|
||||
flags: { environment, format, scope, 'include-sensitive': includeSensitive },
|
||||
} = await this.parse(EnvironmentValueList);
|
||||
format,
|
||||
environment: environments,
|
||||
scope,
|
||||
'include-sensitive': includeSensitive,
|
||||
'include-file-content': includeFileContent,
|
||||
'non-interactive': nonInteractive,
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentValueList, {
|
||||
} = await this.getContextAsync(EnvList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
if (scope === EnvironmentVariableScope.Project && !environment) {
|
||||
environment = await promptVariableEnvironmentAsync(false);
|
||||
if (!environments) {
|
||||
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
|
||||
}
|
||||
|
||||
const variables = await this.getVariablesForScopeAsync(graphqlClient, {
|
||||
scope,
|
||||
includingSensitive: includeSensitive,
|
||||
environment,
|
||||
projectId,
|
||||
});
|
||||
await performForEnvironmentsAsync(environments, async environment => {
|
||||
const variables = await getVariablesForScopeAsync(graphqlClient, {
|
||||
scope,
|
||||
includingSensitive: includeSensitive,
|
||||
includeFileContent,
|
||||
environment,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (format === 'short') {
|
||||
for (const variable of variables) {
|
||||
// TODO: Add Learn more link
|
||||
Log.addNewLineIfNone();
|
||||
if (environment) {
|
||||
Log.log(chalk.bold(`Environment: ${environment.toLocaleLowerCase()}`));
|
||||
}
|
||||
|
||||
if (variables.length === 0) {
|
||||
Log.log('No variables found for this environment.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === 'short') {
|
||||
for (const variable of variables) {
|
||||
Log.log(`${chalk.bold(variable.name)}=${formatVariableValue(variable)}`);
|
||||
}
|
||||
} else {
|
||||
if (scope === EnvironmentVariableScope.Shared) {
|
||||
Log.log(chalk.bold('Account-wide variables for this account:'));
|
||||
} else {
|
||||
Log.log(chalk.bold(`Variables for this project:`));
|
||||
}
|
||||
Log.log(
|
||||
`${chalk.bold(variable.name)}=${
|
||||
variable.value ??
|
||||
"***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)"
|
||||
}`
|
||||
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (scope === EnvironmentVariableScope.Shared) {
|
||||
Log.log(chalk.bold('Shared variables for this account:'));
|
||||
} else {
|
||||
Log.log(chalk.bold(`Variables for this project for environment ${environment}:`));
|
||||
}
|
||||
Log.log(
|
||||
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getVariablesForScopeAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
scope,
|
||||
includingSensitive,
|
||||
environment,
|
||||
projectId,
|
||||
}: {
|
||||
scope: EnvironmentVariableScope;
|
||||
includingSensitive: boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
projectId: string;
|
||||
}
|
||||
): Promise<EnvironmentVariableFragment[]> {
|
||||
if (scope === EnvironmentVariableScope.Project && environment) {
|
||||
if (includingSensitive) {
|
||||
return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
});
|
||||
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'.");
|
||||
}
|
||||
|
||||
return includingSensitive
|
||||
? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
})
|
||||
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId });
|
||||
const environments = flags.environment
|
||||
? flags.environment
|
||||
: environment
|
||||
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...flags,
|
||||
'non-interactive': flags['non-interactive'] ?? false,
|
||||
environment: environments,
|
||||
scope:
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project,
|
||||
};
|
||||
}
|
||||
}
|
||||
+88
-32
@@ -1,27 +1,42 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import dotenv from 'dotenv';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableFragment } from '../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import { EnvironmentSecretType, EnvironmentVariableVisibility } from '../../graphql/generated';
|
||||
import {
|
||||
EnvironmentVariableWithFileContent,
|
||||
EnvironmentVariablesQuery,
|
||||
} from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
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.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EASEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
...EASEnvironmentFlag,
|
||||
path: Flags.string({
|
||||
description: 'Path to the result `.env` file',
|
||||
default: '.env.local',
|
||||
@@ -30,16 +45,25 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { environment, path: targetPath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvironmentVariablePull);
|
||||
args: { environment: argEnvironment },
|
||||
flags: { environment: flagEnvironment, path: targetPath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvPull);
|
||||
|
||||
let environment = flagEnvironment?.toUpperCase() ?? argEnvironment?.toUpperCase();
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
environment = await promptVariableEnvironmentAsync({ nonInteractive });
|
||||
}
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariablePull, {
|
||||
projectDir,
|
||||
} = await this.getContextAsync(EnvPull, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -50,6 +74,7 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
{
|
||||
appId: projectId,
|
||||
environment,
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -63,32 +88,63 @@ export default class EnvironmentVariablePull extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const filePrefix = `# Environment: ${environment}\n\n`;
|
||||
let currentEnvLocal: Record<string, string> = {};
|
||||
|
||||
const envFileContent = environmentVariables
|
||||
.map((variable: EnvironmentVariableFragment) => {
|
||||
if (variable.value === null) {
|
||||
return `# ${variable.name}=***** (secret variables are not available for reading)`;
|
||||
if (await fs.exists(targetPath)) {
|
||||
currentEnvLocal = dotenv.parse(await fs.readFile(targetPath, 'utf8'));
|
||||
}
|
||||
|
||||
const filePrefix = `# Environment: ${environment.toLocaleLowerCase()}\n\n`;
|
||||
|
||||
const isFileVariablePresent = environmentVariables.some(v => {
|
||||
return v.type === EnvironmentSecretType.FileBase64 && v.valueWithFileContent;
|
||||
});
|
||||
|
||||
const envDir = path.join(projectDir, '.eas', '.env');
|
||||
if (isFileVariablePresent) {
|
||||
await fs.mkdir(envDir, { recursive: true });
|
||||
}
|
||||
|
||||
const skippedSecretVariables: string[] = [];
|
||||
const overridenSecretVariables: string[] = [];
|
||||
|
||||
const envFileContentLines = await Promise.all(
|
||||
environmentVariables.map(async (variable: EnvironmentVariableWithFileContent) => {
|
||||
if (variable.visibility === EnvironmentVariableVisibility.Secret) {
|
||||
if (currentEnvLocal[variable.name]) {
|
||||
overridenSecretVariables.push(variable.name);
|
||||
return `${variable.name}=${currentEnvLocal[variable.name]}`;
|
||||
}
|
||||
skippedSecretVariables.push(variable.name);
|
||||
return `# ${variable.name}=***** (secret)`;
|
||||
}
|
||||
if (variable.type === EnvironmentSecretType.FileBase64 && variable.valueWithFileContent) {
|
||||
const filePath = path.join(envDir, variable.name);
|
||||
await fs.writeFile(filePath, variable.valueWithFileContent, 'base64');
|
||||
return `${variable.name}=${filePath}`;
|
||||
}
|
||||
return `${variable.name}=${variable.value}`;
|
||||
})
|
||||
.join('\n');
|
||||
await fs.writeFile(targetPath, filePrefix + envFileContent);
|
||||
|
||||
const secretEnvVariables = environmentVariables.filter(
|
||||
(variable: EnvironmentVariableFragment) => variable.value === null
|
||||
);
|
||||
if (secretEnvVariables.length > 0) {
|
||||
Log.warn(
|
||||
`The eas env:pull command tried to pull environment variables with "secret" visibility. The variables with "secret" visibility are not available for reading, therefore thet were marked as "*****" in the generated .env file. Provide values for these manually in ${targetPath} if needed. Skipped variables: ${secretEnvVariables
|
||||
.map(v => v.name)
|
||||
.join('\n')}`
|
||||
);
|
||||
Log.warn();
|
||||
}
|
||||
|
||||
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')}.`);
|
||||
}
|
||||
|
||||
if (skippedSecretVariables.length > 0) {
|
||||
Log.addNewLineIfNone();
|
||||
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(
|
||||
', '
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
-114
@@ -1,9 +1,10 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag } from '../../commandUtils/flags';
|
||||
import { EASMultiEnvironmentFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
@@ -17,145 +18,168 @@ import {
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type PushFlags = {
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
path: string;
|
||||
};
|
||||
|
||||
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.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EASEnvironmentFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
path: Flags.string({
|
||||
description: 'Path to the input `.env` file',
|
||||
default: '.env.local',
|
||||
}),
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to push variables to. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(EnvironmentVariablePush);
|
||||
const { args, flags } = await this.parse(EnvPush);
|
||||
|
||||
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
|
||||
|
||||
const { environment, path: envPath } = this.validateFlags(flags);
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariablePush, {
|
||||
} = await this.getContextAsync(EnvPush, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
if (!environments) {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive: false,
|
||||
multiple: true,
|
||||
});
|
||||
}
|
||||
|
||||
const updateVariables: Record<string, EnvironmentVariablePushInput> =
|
||||
await this.parseEnvFileAsync(envPath, environment);
|
||||
await this.parseEnvFileAsync(envPath, environments);
|
||||
|
||||
const variableNames = Object.keys(updateVariables);
|
||||
|
||||
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
filterNames: variableNames,
|
||||
});
|
||||
for (const environment of environments) {
|
||||
const displayedEnvironment = environment.toLocaleLowerCase();
|
||||
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
filterNames: variableNames,
|
||||
});
|
||||
|
||||
const existingDifferentVariables: EnvironmentVariableFragment[] = [];
|
||||
// Remove variables that are the same as the ones in the environment
|
||||
existingVariables.forEach(variable => {
|
||||
const existingVariableUpdate = updateVariables[variable.name];
|
||||
if (existingVariableUpdate && existingVariableUpdate.value !== variable.value) {
|
||||
existingDifferentVariables.push(variable);
|
||||
} else {
|
||||
delete updateVariables[variable.name];
|
||||
}
|
||||
});
|
||||
const existingDifferentVariables: EnvironmentVariableFragment[] = [];
|
||||
// Remove variables that are the same as the ones in the environment
|
||||
existingVariables.forEach(existingVariable => {
|
||||
const existingVariableUpdate = updateVariables[existingVariable.name];
|
||||
|
||||
const existingDifferentSharedVariables = existingDifferentVariables.filter(
|
||||
variable => variable.scope === EnvironmentVariableScope.Shared
|
||||
);
|
||||
if (existingVariableUpdate) {
|
||||
const hasMoreEnvironments = existingVariableUpdate.environments.some(
|
||||
newEnv => !existingVariable.environments?.includes(newEnv)
|
||||
);
|
||||
|
||||
if (existingDifferentSharedVariables.length > 0) {
|
||||
const existingDifferentSharedVariablesNames = existingDifferentSharedVariables.map(
|
||||
variable => variable.name
|
||||
if (existingVariableUpdate.value !== existingVariable.value || hasMoreEnvironments) {
|
||||
existingDifferentVariables.push(existingVariable);
|
||||
} else {
|
||||
delete updateVariables[existingVariable.name];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const existingDifferentSharedVariables = existingDifferentVariables.filter(
|
||||
variable => variable.scope === EnvironmentVariableScope.Shared
|
||||
);
|
||||
Log.error('Shared 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');
|
||||
}
|
||||
|
||||
if (existingDifferentVariables.length > 0) {
|
||||
Log.warn('Some variables already exist in the environment.');
|
||||
const variableNames = existingDifferentVariables.map(variable => variable.name);
|
||||
|
||||
const confirmationMessage =
|
||||
variableNames.length > 1
|
||||
? `The ${variableNames.join(
|
||||
', '
|
||||
)} environment variables already exist in ${environment} environment. Do you want to override them all?`
|
||||
: `The ${variableNames[0]} environment variable already exists in ${environment} environment. Do you want to override it?`;
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: confirmationMessage,
|
||||
});
|
||||
|
||||
let variablesToOverwrite: string[] = [];
|
||||
|
||||
if (!confirm && existingDifferentVariables.length === 1) {
|
||||
throw new Error('No new variables to push.');
|
||||
}
|
||||
|
||||
if (confirm) {
|
||||
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
|
||||
} else {
|
||||
const promptResult = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'variablesToOverwrite',
|
||||
message: 'Select variables to overwrite:',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: existingDifferentVariables.map(variable => ({
|
||||
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
|
||||
variable.value ?? '(secret)'
|
||||
})`,
|
||||
value: variable.name,
|
||||
})),
|
||||
if (existingDifferentSharedVariables.length > 0) {
|
||||
const existingDifferentSharedVariablesNames = existingDifferentSharedVariables.map(
|
||||
variable => variable.name
|
||||
);
|
||||
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}`);
|
||||
});
|
||||
variablesToOverwrite = promptResult.variablesToOverwrite;
|
||||
throw new Error('Account-wide variables cannot be overwritten by eas env:push command');
|
||||
}
|
||||
|
||||
for (const existingVariable of existingVariables) {
|
||||
const name = existingVariable.name;
|
||||
if (variablesToOverwrite.includes(name)) {
|
||||
updateVariables[name]['overwrite'] = true;
|
||||
if (existingDifferentVariables.length > 0) {
|
||||
Log.warn(`Some variables already exist in the ${displayedEnvironment} environment.`);
|
||||
const variableNames = existingDifferentVariables.map(variable => variable.name);
|
||||
|
||||
const confirmationMessage =
|
||||
variableNames.length > 1
|
||||
? `The ${variableNames.join(
|
||||
', '
|
||||
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
|
||||
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: confirmationMessage,
|
||||
});
|
||||
|
||||
let variablesToOverwrite: string[] = [];
|
||||
|
||||
if (!confirm && existingDifferentVariables.length === 0) {
|
||||
throw new Error('No new variables to push.');
|
||||
}
|
||||
|
||||
if (confirm) {
|
||||
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
|
||||
} else {
|
||||
delete updateVariables[name];
|
||||
const promptResult = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'variablesToOverwrite',
|
||||
message: 'Select variables to overwrite:',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: existingDifferentVariables.map(variable => ({
|
||||
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
|
||||
variable.value ?? '(secret)'
|
||||
})`,
|
||||
value: variable.name,
|
||||
})),
|
||||
});
|
||||
variablesToOverwrite = promptResult.variablesToOverwrite;
|
||||
}
|
||||
|
||||
for (const existingVariable of existingVariables) {
|
||||
const name = existingVariable.name;
|
||||
if (variablesToOverwrite.includes(name)) {
|
||||
updateVariables[name]['overwrite'] = true;
|
||||
} else {
|
||||
delete updateVariables[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any of the sensitive variables already exist in the environment. Prompt the user to overwrite them.
|
||||
const existingSensitiveVariables = existingVariables.filter(
|
||||
variable => variable.value === null
|
||||
);
|
||||
|
||||
if (existingSensitiveVariables.length > 0) {
|
||||
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
|
||||
variable => `- ${variable.name}`
|
||||
// Check if any of the sensitive variables already exist in the environment. Prompt the user to overwrite them.
|
||||
const existingSensitiveVariables = existingVariables.filter(
|
||||
variable => variable.visibility !== EnvironmentVariableVisibility.Public
|
||||
);
|
||||
const confirm = await confirmAsync({
|
||||
message: `You are about to overwrite sensitive variables.\n${existingSensitiveVariablesNames.join(
|
||||
'\n'
|
||||
)}\n Do you want to continue?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting...');
|
||||
|
||||
if (existingSensitiveVariables.length > 0) {
|
||||
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
|
||||
variable => `- ${variable.name}`
|
||||
);
|
||||
const confirm = await confirmAsync({
|
||||
message: `You are about to overwrite sensitive variables.\n${existingSensitiveVariablesNames.join(
|
||||
'\n'
|
||||
)}\n Do you want to continue?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,13 +195,30 @@ export default class EnvironmentVariablePush extends EasCommand {
|
||||
variablesToPush,
|
||||
projectId
|
||||
);
|
||||
Log.log(`Uploaded env file to ${environments.join(', ').toLocaleLowerCase()}.`);
|
||||
}
|
||||
|
||||
Log.log(`Uploaded env file to ${environment} environment.`);
|
||||
parseFlagsAndArgs(
|
||||
flags: { path: string; environment: EnvironmentVariableEnvironment[] | undefined },
|
||||
{ environment }: Record<string, string>
|
||||
): { environment?: EnvironmentVariableEnvironment[]; path: string } {
|
||||
if (environment && !isEnvironment(environment.toUpperCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const environments =
|
||||
flags.environment ??
|
||||
(environment ? [environment.toUpperCase() as EnvironmentVariableEnvironment] : undefined);
|
||||
|
||||
return {
|
||||
...flags,
|
||||
environment: environments,
|
||||
};
|
||||
}
|
||||
|
||||
private async parseEnvFileAsync(
|
||||
envPath: string,
|
||||
environment: string
|
||||
environments: EnvironmentVariableEnvironment[]
|
||||
): Promise<Record<string, EnvironmentVariablePushInput>> {
|
||||
if (!(await fs.exists(envPath))) {
|
||||
throw new Error(`File ${envPath} does not exist.`);
|
||||
@@ -186,11 +227,25 @@ export default class EnvironmentVariablePush extends EasCommand {
|
||||
|
||||
const variables: Record<string, string> = dotenv.parse(await fs.readFile(envPath, 'utf8'));
|
||||
|
||||
const hasFileVariables = Object.values(variables).some(value =>
|
||||
value.includes(path.join('.eas', '.env'))
|
||||
);
|
||||
|
||||
if (hasFileVariables) {
|
||||
Log.warn('File variables are not supported in this command.');
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(variables)) {
|
||||
// Skip file variables
|
||||
const fileVariablePath = path.join('.eas', '.env', name);
|
||||
if (value.endsWith(fileVariablePath)) {
|
||||
Log.warn(`Skipping file variable ${name}`);
|
||||
continue;
|
||||
}
|
||||
pushInput[name] = {
|
||||
name,
|
||||
value,
|
||||
environment,
|
||||
environments,
|
||||
visibility: name.startsWith('EXPO_SENSITIVE')
|
||||
? EnvironmentVariableVisibility.Sensitive
|
||||
: EnvironmentVariableVisibility.Public,
|
||||
@@ -199,12 +254,4 @@ export default class EnvironmentVariablePush extends EasCommand {
|
||||
|
||||
return pushInput;
|
||||
}
|
||||
|
||||
private validateFlags(flags: PushFlags): Required<PushFlags> {
|
||||
if (!flags.environment) {
|
||||
throw new Error('Please provide an environment to push the env file to.');
|
||||
}
|
||||
|
||||
return { ...flags, environment: flags.environment };
|
||||
}
|
||||
}
|
||||
+143
-47
@@ -2,95 +2,191 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import { EASMultiEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvironmentVariableUnlink extends EasCommand {
|
||||
static override description = 'unlink a shared environment variable to the current project';
|
||||
type UnlinkFlags = {
|
||||
'variable-name'?: string;
|
||||
'non-interactive': boolean;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
};
|
||||
|
||||
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 = {
|
||||
name: Flags.string({
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable',
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to unlink the variable from. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvUnlink);
|
||||
|
||||
let {
|
||||
flags: { name, 'non-interactive': nonInteractive, environment },
|
||||
} = await this.parse(EnvironmentVariableUnlink);
|
||||
'variable-name': name,
|
||||
'non-interactive': nonInteractive,
|
||||
environment: unlinkEnvironments,
|
||||
} = this.validateInputs(flags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableUnlink, {
|
||||
} = await this.getContextAsync(EnvUnlink, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
}
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
filterNames: name ? [name] : undefined,
|
||||
});
|
||||
const linkedVariables = appVariables.filter(
|
||||
({ scope }) => scope === EnvironmentVariableScope.Shared
|
||||
);
|
||||
|
||||
if (linkedVariables.length === 0) {
|
||||
throw new Error(`There are no linked shared env variables for project ${projectDisplayName}`);
|
||||
}
|
||||
let selectedVariable = variables[0];
|
||||
|
||||
if (!name) {
|
||||
name = await selectAsync(
|
||||
'Select shared variable to unlink',
|
||||
linkedVariables.map(variable => ({
|
||||
title: variable.name,
|
||||
value: variable.name,
|
||||
if (variables.length > 1) {
|
||||
if (nonInteractive) {
|
||||
throw new Error("Multiple variables found, please select one using '--variable-name'");
|
||||
}
|
||||
selectedVariable = await selectAsync(
|
||||
'Select account-wide variable',
|
||||
variables.map(variable => ({
|
||||
title: formatVariableName(variable),
|
||||
value: variable,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const selectedVariable = linkedVariables.find(variable => variable.name === name);
|
||||
|
||||
if (!selectedVariable) {
|
||||
throw new Error(`Shared variable ${name} doesn't exist`);
|
||||
throw new Error(`Account-wide variable ${name} doesn't exist`);
|
||||
}
|
||||
|
||||
const unlinkedVariable = await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
let explicitSelect = false;
|
||||
|
||||
if (!unlinkedVariable) {
|
||||
throw new Error(
|
||||
`Could not unlink variable with name ${selectedVariable.name} from project ${projectDisplayName}`
|
||||
if (!nonInteractive && !unlinkEnvironments) {
|
||||
const selectedEnvironments =
|
||||
(selectedVariable.linkedEnvironments ?? []).length > 0
|
||||
? selectedVariable.linkedEnvironments
|
||||
: selectedVariable.environments;
|
||||
const environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
selectedEnvironments: selectedEnvironments ?? [],
|
||||
});
|
||||
explicitSelect = true;
|
||||
unlinkEnvironments = Object.values(EnvironmentVariableEnvironment).filter(
|
||||
env => !environments.includes(env)
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`Unlinked variable ${chalk.bold(unlinkedVariable.name)} from the project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)}.`
|
||||
);
|
||||
if (!unlinkEnvironments) {
|
||||
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId
|
||||
);
|
||||
Log.withTick(
|
||||
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
|
||||
if (
|
||||
selectedVariable.linkedEnvironments?.includes(environment) !==
|
||||
unlinkEnvironments.includes(environment)
|
||||
) {
|
||||
if (!explicitSelect && unlinkEnvironments.includes(environment)) {
|
||||
Log.withTick(
|
||||
`Variable ${chalk.bold(
|
||||
selectedVariable.name
|
||||
)} is already unlinked from ${environment.toLocaleLowerCase()}.`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (unlinkEnvironments.includes(environment)) {
|
||||
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
Log.withTick(
|
||||
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} in ${environment.toLocaleLowerCase()}.`
|
||||
);
|
||||
} else if (explicitSelect) {
|
||||
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
|
||||
graphqlClient,
|
||||
selectedVariable.id,
|
||||
projectId,
|
||||
environment
|
||||
);
|
||||
Log.withTick(
|
||||
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} in ${environment.toLocaleLowerCase()}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateInputs(
|
||||
flags: UnlinkFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): UnlinkFlags {
|
||||
if (flags['non-interactive']) {
|
||||
if (!flags['variable-name']) {
|
||||
throw new Error(
|
||||
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
environment: [environment],
|
||||
...flags,
|
||||
};
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
+291
-140
@@ -1,14 +1,25 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
EASMultiEnvironmentFlag,
|
||||
EASNonInteractiveFlag,
|
||||
EASVariableScopeFlag,
|
||||
EASVariableVisibilityFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableScope, EnvironmentVariableVisibility } from '../../graphql/generated';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
@@ -17,57 +28,100 @@ import {
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync, promptVariableValueAsync } from '../../utils/prompts';
|
||||
import {
|
||||
parseVisibility,
|
||||
promptVariableEnvironmentAsync,
|
||||
promptVariableNameAsync,
|
||||
promptVariableTypeAsync,
|
||||
promptVariableValueAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type UpdateFlags = {
|
||||
interface RawUpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope?: EnvironmentVariableScope;
|
||||
environment?: string;
|
||||
visibility?: EnvironmentVariableVisibility;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
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({
|
||||
description: 'Current name of the variable',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to update',
|
||||
}),
|
||||
name: Flags.string({
|
||||
description: 'Name of the variable',
|
||||
description: 'New name of the variable',
|
||||
}),
|
||||
value: Flags.string({
|
||||
description: 'Text value or the variable',
|
||||
description: 'New value or the variable',
|
||||
}),
|
||||
type: Flags.enum<'string' | 'file'>({
|
||||
description: 'The type of variable',
|
||||
options: ['string', 'file'],
|
||||
}),
|
||||
...EASVariableVisibilityFlag,
|
||||
...EASVariableScopeFlag,
|
||||
...EASEnvironmentFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable to update. One of 'production', 'preview', or 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.Analytics,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(EnvironmentVariableUpdate);
|
||||
let {
|
||||
const { args, flags } = await this.parse(EnvUpdate);
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
value: rawValue,
|
||||
scope,
|
||||
'variable-name': currentName,
|
||||
'variable-environment': currentEnvironment,
|
||||
'non-interactive': nonInteractive,
|
||||
environment,
|
||||
environment: environments,
|
||||
type,
|
||||
visibility,
|
||||
} = this.validateFlags(flags);
|
||||
} = this.sanitizeInputs(flags, args);
|
||||
|
||||
const {
|
||||
privateProjectConfig: { projectId },
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvironmentVariableUpdate, {
|
||||
} = await this.getContextAsync(EnvUpdate, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
@@ -76,132 +130,229 @@ export default class EnvironmentVariableUpdate extends EasCommand {
|
||||
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
|
||||
]);
|
||||
|
||||
let selectedVariable: EnvironmentVariableFragment;
|
||||
let existingVariables: EnvironmentVariableFragment[] = [];
|
||||
const suffix =
|
||||
scope === EnvironmentVariableScope.Project
|
||||
? `on project ${projectDisplayName}`
|
||||
: `on account ${ownerAccount.name}`;
|
||||
|
||||
if (scope === EnvironmentVariableScope.Project) {
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync(nonInteractive);
|
||||
}
|
||||
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment,
|
||||
environment: currentEnvironment,
|
||||
filterNames: currentName ? [currentName] : undefined,
|
||||
});
|
||||
if (!name) {
|
||||
name = await selectAsync(
|
||||
'Select variable',
|
||||
existingVariables.map(variable => ({
|
||||
title: variable.name,
|
||||
value: variable.name,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const existingVariable = existingVariables.find(variable => variable.name === name);
|
||||
if (!existingVariable) {
|
||||
throw new Error(
|
||||
`Variable with name ${name} does not exist on project ${projectDisplayName}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
value = await promptVariableValueAsync({
|
||||
nonInteractive,
|
||||
required: false,
|
||||
initial: existingVariable.value,
|
||||
});
|
||||
if (!value || value.length === 0) {
|
||||
value = '';
|
||||
}
|
||||
}
|
||||
|
||||
const variable = await EnvironmentVariableMutation.createForAppAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
environment,
|
||||
visibility,
|
||||
overwrite: true,
|
||||
},
|
||||
projectId
|
||||
);
|
||||
if (!variable) {
|
||||
throw new Error(
|
||||
`Could not update variable with name ${name} on project ${projectDisplayName}`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`Updated variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
|
||||
);
|
||||
} else if (scope === EnvironmentVariableScope.Shared) {
|
||||
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
});
|
||||
|
||||
if (!name) {
|
||||
name = await selectAsync(
|
||||
'Select variable',
|
||||
sharedVariables.map(variable => ({
|
||||
title: variable.name,
|
||||
value: variable.name,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const existingVariable = sharedVariables.find(variable => variable.name === name);
|
||||
if (!existingVariable) {
|
||||
throw new Error(
|
||||
"Variable with this name doesn't exist on this account. Use a different name."
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
value = await promptVariableValueAsync({
|
||||
nonInteractive,
|
||||
required: false,
|
||||
initial: existingVariable.value,
|
||||
});
|
||||
if (!value || value.length === 0) {
|
||||
value = '';
|
||||
}
|
||||
}
|
||||
|
||||
const variable = await EnvironmentVariableMutation.createSharedVariableAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
visibility,
|
||||
overwrite: true,
|
||||
},
|
||||
ownerAccount.id
|
||||
);
|
||||
|
||||
if (!variable) {
|
||||
throw new Error(
|
||||
`Could not update variable with name ${name} on account ${ownerAccount.name}`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`Updated shared variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (scope === EnvironmentVariableScope.Shared) {
|
||||
existingVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment: currentEnvironment,
|
||||
filterNames: currentName ? [currentName] : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (existingVariables.length === 0) {
|
||||
throw new Error(
|
||||
`Variable with name ${currentName} ${
|
||||
currentEnvironment ? `in environment ${currentEnvironment.toLocaleLowerCase()}` : ''
|
||||
} does not exist ${suffix}.`
|
||||
);
|
||||
} else if (existingVariables.length > 1) {
|
||||
selectedVariable = await selectAsync(
|
||||
'Select variable',
|
||||
existingVariables.map(variable => ({
|
||||
title: formatVariableName(variable),
|
||||
value: variable,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
selectedVariable = existingVariables[0];
|
||||
}
|
||||
|
||||
assert(selectedVariable, 'Variable must be selected');
|
||||
|
||||
const {
|
||||
name: newName,
|
||||
value: newValue,
|
||||
environment: newEnvironments,
|
||||
visibility: newVisibility,
|
||||
type: newType,
|
||||
fileName,
|
||||
} = await this.promptForMissingFlagsAsync(selectedVariable, {
|
||||
name,
|
||||
value: rawValue,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
scope,
|
||||
});
|
||||
|
||||
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
|
||||
id: selectedVariable.id,
|
||||
name: newName,
|
||||
value: newValue,
|
||||
environments: newEnvironments,
|
||||
type: newType,
|
||||
visibility: newVisibility,
|
||||
fileName: newValue ? fileName : undefined,
|
||||
});
|
||||
if (!variable) {
|
||||
throw new Error(`Could not update variable with name ${name} ${suffix}`);
|
||||
}
|
||||
|
||||
Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`);
|
||||
}
|
||||
private validateFlags(flags: UpdateFlags): UpdateFlags {
|
||||
private sanitizeInputs(
|
||||
flags: RawUpdateFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): UpdateFlags {
|
||||
if (flags['non-interactive']) {
|
||||
if (!flags.name) {
|
||||
if (!flags['variable-name']) {
|
||||
throw new Error(
|
||||
'Variable name is required in non-interactive mode. Run the command with --name flag.'
|
||||
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.scope === EnvironmentVariableScope.Project && !flags.environment) {
|
||||
throw new Error(
|
||||
'Environment is required when updating project-wide variable in non-interactive mode. Run the command with --environment flag.'
|
||||
);
|
||||
if (flags['type'] && !flags['value']) {
|
||||
throw new Error('Value is required when type is set. Run the command with --value flag.');
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
const scope =
|
||||
flags.scope === 'account'
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...flags, scope };
|
||||
}
|
||||
|
||||
private async promptForMissingFlagsAsync(
|
||||
selectedVariable: EnvironmentVariableFragment,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
...rest
|
||||
}: UpdateFlags
|
||||
): Promise<
|
||||
Omit<UpdateFlags, 'type' | 'visibility'> & {
|
||||
type?: EnvironmentSecretType;
|
||||
visibility?: EnvironmentVariableVisibility;
|
||||
fileName?: string;
|
||||
}
|
||||
> {
|
||||
let newType;
|
||||
let newVisibility: EnvironmentVariableVisibility | undefined;
|
||||
let fileName: string | undefined;
|
||||
|
||||
if (type === 'file') {
|
||||
newType = EnvironmentSecretType.FileBase64;
|
||||
} else if (type === 'string') {
|
||||
newType = EnvironmentSecretType.String;
|
||||
}
|
||||
|
||||
if (!nonInteractive) {
|
||||
if (!name) {
|
||||
name = await promptVariableNameAsync(nonInteractive, selectedVariable.name);
|
||||
|
||||
if (!name || name.length === 0) {
|
||||
name = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!type && !value && !nonInteractive) {
|
||||
newType = await promptVariableTypeAsync(nonInteractive, selectedVariable.type);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
value = await promptVariableValueAsync({
|
||||
nonInteractive,
|
||||
required: false,
|
||||
filePath: (newType ?? selectedVariable.type) === EnvironmentSecretType.FileBase64,
|
||||
initial:
|
||||
(newType ?? selectedVariable.type) === EnvironmentSecretType.FileBase64
|
||||
? undefined
|
||||
: selectedVariable.value,
|
||||
});
|
||||
|
||||
if (!value || value.length === 0 || value === selectedVariable.value) {
|
||||
value = undefined;
|
||||
newType = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let environmentFilePath: string | undefined;
|
||||
|
||||
if ((newType ?? selectedVariable.type) === EnvironmentSecretType.FileBase64 && value) {
|
||||
environmentFilePath = path.resolve(value);
|
||||
if (!(await fs.pathExists(environmentFilePath))) {
|
||||
throw new Error(`File "${value}" does not exist`);
|
||||
}
|
||||
fileName = path.basename(environmentFilePath);
|
||||
}
|
||||
|
||||
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
|
||||
|
||||
if (!environments || environments.length === 0) {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
selectedEnvironments: selectedVariable.environments ?? [],
|
||||
});
|
||||
|
||||
if (
|
||||
!environments ||
|
||||
environments.length === 0 ||
|
||||
environments === selectedVariable.environments
|
||||
) {
|
||||
environments = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!visibility) {
|
||||
newVisibility = await promptVariableVisibilityAsync(
|
||||
nonInteractive,
|
||||
selectedVariable.visibility
|
||||
);
|
||||
|
||||
if (!newVisibility || newVisibility === selectedVariable.visibility) {
|
||||
newVisibility = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (visibility) {
|
||||
newVisibility = parseVisibility(visibility);
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
name,
|
||||
value,
|
||||
environment: environments,
|
||||
visibility: newVisibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type: newType,
|
||||
fileName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { AppPlatform, BuildStatus } from '../../graphql/generated';
|
||||
import { FingerprintMutation } from '../../graphql/mutations/FingerprintMutation';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { Fingerprint, FingerprintDiffItem } from '../../utils/fingerprint';
|
||||
import { createFingerprintAsync, diffFingerprint } from '../../utils/fingerprintCli';
|
||||
import { abridgedDiff } from '../../utils/fingerprintDiff';
|
||||
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class FingerprintCompare extends EasCommand {
|
||||
static override description = 'compare fingerprints of the current project, builds and updates';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
'build-id': Flags.string({
|
||||
aliases: ['buildId'],
|
||||
description: 'Compare the fingerprint with the build with the specified ID',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.ProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(FingerprintCompare);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags;
|
||||
|
||||
const {
|
||||
projectId,
|
||||
privateProjectConfig: { projectDir },
|
||||
loggedIn: { graphqlClient },
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(FingerprintCompare, {
|
||||
nonInteractive,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
let buildId: string | null = buildIdFromArg;
|
||||
if (!buildId) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Build ID must be provided in non-interactive mode');
|
||||
}
|
||||
|
||||
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
|
||||
filters: { hasFingerprint: true },
|
||||
});
|
||||
if (!buildId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.log(`Comparing fingerprints of the current project and build ${buildId}…`);
|
||||
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
|
||||
const fingerprintDebugUrl = buildWithFingerprint.fingerprint?.debugInfoUrl;
|
||||
if (!fingerprintDebugUrl) {
|
||||
Log.error('A fingerprint for the build could not be found.');
|
||||
return;
|
||||
}
|
||||
const fingerprintResponse = await fetch(fingerprintDebugUrl);
|
||||
const fingerprint = (await fingerprintResponse.json()) as Fingerprint;
|
||||
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
|
||||
const buildPlatform = buildWithFingerprint.platform;
|
||||
const workflow = workflows[appPlatformToPlatform(buildPlatform)];
|
||||
|
||||
const projectFingerprint = await createFingerprintAsync(projectDir, {
|
||||
workflow,
|
||||
platforms: [appPlatformToString(buildPlatform)],
|
||||
debug: true,
|
||||
env: undefined,
|
||||
});
|
||||
if (!projectFingerprint) {
|
||||
Log.error('Project fingerprints can only be computed for projects with SDK 52 or higher');
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadedFingerprint = await maybeUploadFingerprintAsync({
|
||||
hash: fingerprint.hash,
|
||||
fingerprint: {
|
||||
fingerprintSources: fingerprint.sources,
|
||||
isDebugFingerprintSource: Log.isDebug,
|
||||
},
|
||||
graphqlClient,
|
||||
});
|
||||
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
|
||||
hash: uploadedFingerprint.hash,
|
||||
source: uploadedFingerprint.fingerprintSource,
|
||||
});
|
||||
|
||||
if (fingerprint.hash === projectFingerprint.hash) {
|
||||
Log.log(`✅ Project fingerprint matches build`);
|
||||
return;
|
||||
} else {
|
||||
Log.log(`🔄 Project fingerprint differs from build`);
|
||||
}
|
||||
|
||||
const fingerprintDiffs = diffFingerprint(projectDir, fingerprint, projectFingerprint);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 config',
|
||||
};
|
||||
|
||||
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 isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function appPlatformToPlatform(platform: AppPlatform): Platform {
|
||||
switch (platform) {
|
||||
case AppPlatform.Android:
|
||||
return Platform.ANDROID;
|
||||
case AppPlatform.Ios:
|
||||
return Platform.IOS;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
function appPlatformToString(platform: AppPlatform): string {
|
||||
switch (platform) {
|
||||
case AppPlatform.Android:
|
||||
return 'android';
|
||||
case AppPlatform.Ios:
|
||||
return 'ios';
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export default class MetadataPull extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(MetadataPull, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
// this command is interactive (all nonInteractive flags passed to utility functions are false)
|
||||
|
||||
Loaded 100 of 219 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user