Compare commits

..
130 changed files with 1077 additions and 9606 deletions

No files matched your search

+1 -5
View File
@@ -3,7 +3,7 @@ commentConfiguration:
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['khamilowicz', 'sjchmiela']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
@@ -18,7 +18,3 @@ rules:
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/worker/**']
mentions: ['kitten', 'kadikraman']
- patterns: ['packages/eas-cli/src/commands/deploy/**']
mentions: ['kitten', 'kadikraman']
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v2
- name: Check if CHANGELOG.md was updated
id: changelog-updated
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076
uses: tj-actions/changed-files@v41
with:
files: CHANGELOG.md
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
+13
View File
@@ -226,3 +226,16 @@ jobs:
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
open-pr-in-universe:
name: Open a PR with `@expo/eas-json` bump in universe
needs: publish-to-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Open a PR with `@expo/eas-json` version bump in universe
uses: benc-uk/workflow-dispatch@v1
with:
repo: expo/universe
ref: main
workflow: update-eas-json.yml
token: ${{ secrets.EXPO_BOT_PAT }}
+1 -4
View File
@@ -28,9 +28,6 @@ jobs:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
- run: |
git config --global user.email "[email protected]"
git config --global user.name "Test"
- run: yarn test
if: ${{ !matrix.coverage }}
env:
@@ -52,7 +49,7 @@ jobs:
notify-slack:
runs-on: ubuntu-latest
needs: [test]
needs: test
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
@@ -66,7 +66,7 @@ jobs:
destination_branch: 'main'
pr_title: 'Upgrade eas-build dependencies'
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
pr_reviewer: 'radoslawkrzemien,sjchmiela,khamilowicz'
pr_reviewer: 'szdziedzic,radoslawkrzemien,sjchmiela,khamilowicz'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
+3 -266
View File
@@ -8,276 +8,13 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- Onboarding: add cwd to spawned git process. ([#3015](https://github.com/expo/eas-cli/pull/3015) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
## [16.6.0](https://github.com/expo/eas-cli/releases/tag/v16.6.0) - 2025-05-14
### 🎉 New features
- Add support for uploading iOS internal distribution local builds. ([#3014](https://github.com/expo/eas-cli/pull/3014) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.5.0](https://github.com/expo/eas-cli/releases/tag/v16.5.0) - 2025-05-13
### 🎉 New features
- Add `--wait`/`--no-wait` flag to `eas workflow:run`. ([#3012](https://github.com/expo/eas-cli/pull/3012) by [@sjchmiela](https://github.com/sjchmiela))
- Add `--json` flag to `eas workflow:run`. ([#3013](https://github.com/expo/eas-cli/pull/3013) by [@sjchmiela](https://github.com/sjchmiela))
## [16.4.2](https://github.com/expo/eas-cli/releases/tag/v16.4.2) - 2025-05-10
### 🧹 Chores
- Update `eas-build-cache-provider` function names to use `buildCacheProvider` key. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.4.1](https://github.com/expo/eas-cli/releases/tag/v16.4.1) - 2025-05-07
### 🧹 Chores
- Allow non-standard build types in custom gradle command. ([#3003](https://github.com/expo/eas-cli/pull/3003) by [@khamilowicz](https://github.com/khamilowicz))
## [16.4.0](https://github.com/expo/eas-cli/releases/tag/v16.4.0) - 2025-05-02
### 🎉 New features
- Add `--verbose-logs` flag for `build` command ([#3000](https://github.com/expo/eas-cli/pull/3000) by [@khamilowicz](https://github.com/khamilowicz))
- Add `eas-build-cache-provider` package. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 🧹 Chores
- Read remote build cache provider types from `@expo/config`. ([#3005](https://github.com/expo/eas-cli/pull/3005) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.3.3](https://github.com/expo/eas-cli/releases/tag/v16.3.3) - 2025-04-24
### 🐛 Bug fixes
- Add `sdk-52` alias to the list or supported android images. ([#2989](https://github.com/expo/eas-cli/pull/2989) by [@kadikraman](https://github.com/kadikraman))
- Detect when expo metro config is used. ([#2996](https://github.com/expo/eas-cli/pull/2996) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
- upload assetmap.json when publishing update. ([#2994](https://github.com/expo/eas-cli/pull/2994) by [@quinlanj](https://github.com/quinlanj))
- Add error messages for CDN-level upload errors. ([#2998](https://github.com/expo/eas-cli/pull/2998) by [@kitten](https://github.com/kitten))
## [16.3.2](https://github.com/expo/eas-cli/releases/tag/v16.3.2) - 2025-04-17
### 🧹 Chores
- Update upload command to display build info. ([#2990](https://github.com/expo/eas-cli/pull/2990) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- change launch asset file extension. ([#2991](https://github.com/expo/eas-cli/pull/2991) by [@quinlanj](https://github.com/quinlanj))
## [16.3.1](https://github.com/expo/eas-cli/releases/tag/v16.3.1) - 2025-04-11
### 🧹 Chores
- Add `--no-dev-client` flag for `build:download` command. ([#2985](https://github.com/expo/eas-cli/pull/2985) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.3.0](https://github.com/expo/eas-cli/releases/tag/v16.3.0) - 2025-04-09
### 🎉 New features
- Add `eas upload` command. ([#2932](https://github.com/expo/eas-cli/pull/2932), [#2981](https://github.com/expo/eas-cli/pull/2981), [#2983](https://github.com/expo/eas-cli/pull/2983) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Add `eas build:download` command. ([#2982](https://github.com/expo/eas-cli/pull/2982) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.2.2](https://github.com/expo/eas-cli/releases/tag/v16.2.2) - 2025-04-08
## [16.2.1](https://github.com/expo/eas-cli/releases/tag/v16.2.1) - 2025-04-04
### 🧹 Chores
- Bump `@expo/apple-utils` to use async JWT API. ([#2973](https://github.com/expo/eas-cli/pull/2973) by [@EvanBacon](https://github.com/EvanBacon))
## [16.2.0](https://github.com/expo/eas-cli/releases/tag/v16.2.0) - 2025-04-03
### 🎉 New features
- Add environment flag to `eas fingerprint:compare`. ([#2954](https://github.com/expo/eas-cli/pull/2954) by [@quinlanj](https://github.com/quinlanj))
- Add build-profile flag to `eas fingerprint:generate`. ([#2966](https://github.com/expo/eas-cli/pull/2966) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Remove hidden flag from `eas fingerprint:generate`. ([#2965](https://github.com/expo/eas-cli/pull/2965) by [@quinlanj](https://github.com/quinlanj))
- Refactor `eas update` command to improve code readability. ([#2976](https://github.com/expo/eas-cli/pull/2976) by [@quinlanj](https://github.com/quinlanj))
- `eas update`: add warning if no build exists with fingerprint. ([#2977](https://github.com/expo/eas-cli/pull/2977) by [@quinlanj](https://github.com/quinlanj))
## [16.1.0](https://github.com/expo/eas-cli/releases/tag/v16.1.0) - 2025-03-26
### 🎉 New features
- Add environment flag to `eas fingerprint:generate`. ([#2951](https://github.com/expo/eas-cli/pull/2951) by [@quinlanj](https://github.com/quinlanj))
- Add `corepack` field to `eas.json`. ([#2964](https://github.com/expo/eas-cli/pull/2964) by [@szdziedzic](https://github.com/szdziedzic))
## [16.0.1](https://github.com/expo/eas-cli/releases/tag/v16.0.1) - 2025-03-20
### 🐛 Bug fixes
- Make `eas update:configure` re-apply configuration from app.json /app.config.js when run multiple times. ([#2957](https://github.com/expo/eas-cli/pull/2957) by [@brentvatne](https://github.com/brentvatne))
## [16.0.0](https://github.com/expo/eas-cli/releases/tag/v16.0.0) - 2025-03-19
### 🛠 Breaking changes
- Add support for `.easignore` when `requireCommit` is set to `true`. ([#2942](https://github.com/expo/eas-cli/pull/2942) by [@sjchmiela](https://github.com/sjchmiela))
- Up to 15.0.0, if `requireCommit` was `true`, `.easignore` was silently ignored.
- Versions 15.0.0-15.0.13 started using `.easignore` to skip files from being bundled into a tarball when `requireCommit` was `true`. This was an unexpected change in behavior.
- To clear this up, versions 15.0.13-15.0.15 were erroring if `.easignore` was present when `requireCommit` was `true`.
- `[email protected]` formalizes the 15.0.0-15.0.13 behavior by adhering to `.easignore` even when `requireCommit` is set to `true`.
- If you know what you're doing and you want to suppress a warning printed, you can do so by setting `EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING` environment variable to `true`.
### 🎉 New features
- Add requestId to ApiV2Error. ([#2941](https://github.com/expo/eas-cli/pull/2941) by [@wschurman](https://github.com/wschurman))
- Release `eas fingerprint:generate` in hidden mode ([#2937](https://github.com/expo/eas-cli/pull/2937) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Use correct logic to determine whether artifacts have expired in `eas build:run` command. ([#2931](https://github.com/expo/eas-cli/pull/2931) by [@szdziedzic](https://github.com/szdziedzic))
## [15.0.15](https://github.com/expo/eas-cli/releases/tag/v15.0.15) - 2025-03-12
### 🐛 Bug fixes
- Pass through the updates version to `setUpdatesConfigAsync`, which expects it in order to determine which field values to use. ([#2934](https://github.com/expo/eas-cli/pull/2934) by [@brentvatne](https://github.com/brentvatne)).
## [15.0.14](https://github.com/expo/eas-cli/releases/tag/v15.0.14) - 2025-03-06
### 🐛 Bug fixes
- In `EAS_NO_VCS=1`, use Git for repository root when `EAS_PROJECT_ROOT` is not set. ([#2901](https://github.com/expo/eas-cli/pull/2901) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Suggest using `eas build:dev` for matching configurations. ([#2929](https://github.com/expo/eas-cli/pull/2929) by [@szdziedzic](https://github.com/szdziedzic))
## [15.0.13](https://github.com/expo/eas-cli/releases/tag/v15.0.13) - 2025-03-04
### 🐛 Bug fixes
- Fixed `.git` being always unexpectedly removed if you had `requireCommit: true` and `.easignore` present. ([#2925](https://github.com/expo/eas-cli/pull/2925) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Fix `eas fingerprint:compare` URL generation and pretty prints. ([#2909](https://github.com/expo/eas-cli/pull/2909) by [@quinlanj](https://github.com/quinlanj))
- fix formatFields to handle empty array. ([#2914](https://github.com/expo/eas-cli/pull/2914) by [@quinlanj](https://github.com/quinlanj))
- fix git diff header in `fingerprint:compare`. ([#2915](https://github.com/expo/eas-cli/pull/2915) by [@quinlanj](https://github.com/quinlanj))
- amend fingerprint compare url. ([#2923](https://github.com/expo/eas-cli/pull/2923) by [@quinlanj](https://github.com/quinlanj))
- improve error message for `applicationIdSuffix`. ([#2924](https://github.com/expo/eas-cli/pull/2924) by [@kadikraman](https://github.com/kadikraman))
## [15.0.12](https://github.com/expo/eas-cli/releases/tag/v15.0.12) - 2025-02-22
### 🐛 Bug fixes
- Fix APNS key creation. ([#2916](https://github.com/expo/eas-cli/pull/2916) by [@EvanBacon](https://github.com/EvanBacon))
## [15.0.11](https://github.com/expo/eas-cli/releases/tag/v15.0.11) - 2025-02-21
### 🐛 Bug fixes
- Fix internal TestFlight group creation. ([#2906](https://github.com/expo/eas-cli/pull/2906) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Narrow amount of data queried for basic update channel operations. ([#2901](https://github.com/expo/eas-cli/pull/2901) by [@wschurman](https://github.com/wschurman))
- Fix `eas fingerprint:compare` description. ([#2908](https://github.com/expo/eas-cli/pull/2908) by [@quinlanj](https://github.com/quinlanj))
- Skip auto-creation of TestFlight group when there are already exisitng TestFlight groups and allow to opt out of the behavior by setting `EAS_NO_AUTO_TESTFLIGHT_SETUP` env var. ([#2856](https://github.com/expo/eas-cli/pull/2856) by [@szdziedzic](https://github.com/szdziedzic))
## [15.0.10](https://github.com/expo/eas-cli/releases/tag/v15.0.10) - 2025-02-11
### 🐛 Bug fixes
- Fix files deleted in working directory not being removed from the project archive when `requireCommit` is false. ([#2900](https://github.com/expo/eas-cli/pull/2900) by [@sjchmiela](https://github.com/sjchmiela))
## [15.0.9](https://github.com/expo/eas-cli/releases/tag/v15.0.9) - 2025-02-09
### 🐛 Bug fixes
- Fix files not being ignored when creating a tarball on Windows in Git repository in no `requireCommit` mode. ([#2894](https://github.com/expo/eas-cli/pull/2894) by [@sjchmiela](https://github.com/sjchmiela))
## [15.0.8](https://github.com/expo/eas-cli/releases/tag/v15.0.8) - 2025-02-09
## [15.0.7](https://github.com/expo/eas-cli/releases/tag/v15.0.7) - 2025-02-09
## [15.0.6](https://github.com/expo/eas-cli/releases/tag/v15.0.6) - 2025-02-07
## [15.0.5](https://github.com/expo/eas-cli/releases/tag/v15.0.5) - 2025-02-06
### 🐛 Bug fixes
- Do not copy files over onto a cloned Git repository when packing the project archive if `requireCommit` is true. ([#2885](https://github.com/expo/eas-cli/pull/2885) by [@sjchmiela](https://github.com/sjchmiela))
- Fix `EISDIR` error when archiving project with submodules ignored. ([#2884](https://github.com/expo/eas-cli/pull/2884) by [@sjchmiela](https://github.com/sjchmiela))
## [15.0.4](https://github.com/expo/eas-cli/releases/tag/v15.0.4) - 2025-02-05
### 🐛 Bug fixes
- Fixed `GitClient` not respecting `.easignore` file. ([#2873](https://github.com/expo/eas-cli/pull/2873) by [@sjchmiela](https://github.com/sjchmiela))
- Fix symlink support in `makeShallowCopyAsync`. ([#2874](https://github.com/expo/eas-cli/pull/2874) by [@sjchmiela](https://github.com/sjchmiela))
- Allow excluding `.git` directory from project archive by adding it to `.easignore`. ([#2879](https://github.com/expo/eas-cli/pull/2879) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Popup website in fingerprint:compare. ([#2859](https://github.com/expo/eas-cli/pull/2859) by [@quinlanj](https://github.com/quinlanj))
- Fix fingerprint:compare URL. ([#2861](https://github.com/expo/eas-cli/pull/2861) by [@quinlanj](https://github.com/quinlanj))
- Make less gql calls in fingerprint:compare. ([#2860](https://github.com/expo/eas-cli/pull/2860) by [@quinlanj](https://github.com/quinlanj))
- No longer require owner field for SDK >= 53 or canary. ([#2835](https://github.com/expo/eas-cli/pull/2835) by [@wschurman](https://github.com/wschurman))
- Add --open flag to fingerprint:compare. ([#2872](https://github.com/expo/eas-cli/pull/2872) by [@quinlanj](https://github.com/quinlanj))
## [15.0.3](https://github.com/expo/eas-cli/releases/tag/v15.0.3) - 2025-02-04
### 🐛 Bug fixes
- Fixed EAS server environment variables does not pass to `npx expo-updates runtimeversion:resolve` call. ([#2867](https://github.com/expo/eas-cli/pull/2867) by [@kudo](https://github.com/kudo))
## [15.0.2](https://github.com/expo/eas-cli/releases/tag/v15.0.2) - 2025-02-04
## [15.0.1](https://github.com/expo/eas-cli/releases/tag/v15.0.1) - 2025-02-04
## [15.0.0](https://github.com/expo/eas-cli/releases/tag/v15.0.0) - 2025-02-04
### 🛠 Breaking changes
- Use Git to archive projects containing a Git repository. (Previously, Git would only be used if `requireCommit` flag in `eas.json` was set to `true`.) ([#2841](https://github.com/expo/eas-cli/pull/2841) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- Print warning for `NoVcsClient` only once. ([#2863](https://github.com/expo/eas-cli/pull/2863) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Add update support for fingerprint:compare. ([#2850](https://github.com/expo/eas-cli/pull/2850) by [@quinlanj](https://github.com/quinlanj))
- Add update group id support for fingerprint:compare. ([#2851](https://github.com/expo/eas-cli/pull/2851) by [@quinlanj](https://github.com/quinlanj))
- Add better interactive support for fingerprint:compare. ([#2854](https://github.com/expo/eas-cli/pull/2854) by [@quinlanj](https://github.com/quinlanj))
## [14.7.1](https://github.com/expo/eas-cli/releases/tag/v14.7.1) - 2025-01-31
### 🐛 Bug fixes
- Account for `ios.config.usesNonExemptEncryption` in non-exempt encryption status prompt. ([#2852](https://github.com/expo/eas-cli/pull/2852) by [@EvanBacon](https://github.com/EvanBacon))
## [14.7.0](https://github.com/expo/eas-cli/releases/tag/v14.7.0) - 2025-01-30
### 🎉 New features
- Add `--submit` and `-s` as aliases for `--auto-submit` flag. ([#2846](https://github.com/expo/eas-cli/pull/2846) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Skip non-exempt check in non-interactive mode. ([#2849](https://github.com/expo/eas-cli/pull/2849) by [@EvanBacon](https://github.com/EvanBacon))
## [14.6.0](https://github.com/expo/eas-cli/releases/tag/v14.6.0) - 2025-01-30
### 🎉 New features
- Prompt to set non-exempt encryption status for the iOS app to support faster store submissions. ([#2843](https://github.com/expo/eas-cli/pull/2843) by [@EvanBacon](https://github.com/EvanBacon))
- Automatically create internal TestFlight group in EAS Submit command. ([#2839](https://github.com/expo/eas-cli/pull/2839) by [@evanbacon](https://github.com/evanbacon))
- Sanitize and generate names for EAS Submit to prevent failures due to invalid characters or taken names. ([#2842](https://github.com/expo/eas-cli/pull/2842) by [@evanbacon](https://github.com/evanbacon))
- Release `eas fingerprint:compare`. ([#2821](https://github.com/expo/eas-cli/pull/2821) by [@quinlanj](https://github.com/quinlanj))
### 🐛 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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.6.1",
"version": "14.5.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
@@ -1,22 +0,0 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
To use the EAS remote build provider plugin, install the `eas-build-cache-provider` package as a developer dependency:
```bash
npm install --save-dev eas-build-cache-provider
```
Then, update your **app.json** to include the `buildCacheProvider` property and its provider under `experiments`:
```json
{
"expo": {
...
"experiments": {
"buildCacheProvider": "eas"
}
}
}
```
@@ -1,58 +0,0 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "16.4.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/config": "11.0.10",
"@expo/spawn-async": "^1.7.2",
"chalk": "4.1.2",
"figures": "3.2.0",
"fs-extra": "11.2.0",
"getenv": "1.0.0",
"log-symbols": "4.1.0",
"semver": "7.5.2",
"terminal-link": "2.1.1",
"tslib": "2.4.1"
},
"devDependencies": {
"@tsconfig/node18": "18.2.4",
"@types/babel__code-frame": "7.0.3",
"@types/fs-extra": "11.0.4",
"@types/getenv": "^1.0.0",
"memfs": "3.4.13",
"rimraf": "3.0.2",
"typescript": "5.3.3"
},
"engines": {
"node": ">=18.0.0"
},
"homepage": "https://github.com/expo/eas-cli",
"license": "MIT",
"main": "build/index.js",
"types": "build/index.d.ts",
"repository": "expo/eas-cli",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
"watch": "yarn build --watch --preserveWatchOutput",
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "yarn rebuild",
"rebuild": "rimraf build && yarn build",
"test": "jest",
"clean": "rimraf build node_modules yarn-error.log"
},
"files": [
"/build"
],
"publishConfig": {
"access": "public"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
}
}
@@ -1,19 +0,0 @@
module.exports = {
extends: ['universe/shared/typescript-analysis'],
overrides: [
{
files: ['*.ts', '*.d.ts'],
parserOptions: {
project: './packages/eas-build-cache-provider/tsconfig.json',
},
rules: {
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
},
},
],
};
@@ -1,38 +0,0 @@
import { RunOptions, getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
export function isSpawnResultError(obj: any): obj is Error & SpawnResult {
return (
obj &&
'message' in obj &&
obj.status !== undefined &&
obj.stdout !== undefined &&
obj.stderr !== undefined
);
}
export function isDevClientBuild({
runOptions,
projectRoot,
}: {
runOptions: RunOptions;
projectRoot: string;
}): boolean {
if (!hasDirectDevClientDependency(projectRoot)) {
return false;
}
if ('variant' in runOptions && runOptions.variant !== undefined) {
return runOptions.variant === 'debug';
}
if ('configuration' in runOptions && runOptions.configuration !== undefined) {
return runOptions.configuration === 'Debug';
}
return true;
}
export function hasDirectDevClientDependency(projectRoot: string): boolean {
const { dependencies = {}, devDependencies = {} } = getPackageJson(projectRoot);
return !!dependencies['expo-dev-client'] || !!devDependencies['expo-dev-client'];
}
@@ -1,136 +0,0 @@
import {
BuildCacheProviderPlugin,
CalculateFingerprintHashProps,
ResolveBuildCacheProps,
UploadBuildCacheProps,
} from '@expo/config';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { isDevClientBuild, isSpawnResultError } from './helpers';
import Log from './log';
async function resolveBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: ResolveBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip checking for remote builds');
return null;
}
Log.log(
chalk`{whiteBright \u203A} {bold Searching builds with matching fingerprint on EAS servers}`
);
try {
const results = await spawnAsync(
'npx',
[
'eas-cli',
'build:download',
`--platform=${platform}`,
`--fingerprint=${fingerprintHash}`,
'--non-interactive',
isDevClientBuild({ runOptions, projectRoot }) ? '--dev-client' : '--no-dev-client',
'--json',
],
{
cwd: projectRoot,
}
);
Log.log(chalk`{whiteBright \u203A} {bold Successfully downloaded cached build}`);
// {
// "path": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
// }
const json = JSON.parse(results.stdout.trim());
return json?.path;
} catch (error) {
Log.debug('eas-cli error:', error);
// @TODO(2025-04-11): remove this in a future release
if (isSpawnResultError(error) && error.stderr.includes('command build:download not found')) {
Log.warn(
`To take advantage of EAS build cache provider, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: UploadBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip uploading builds');
return null;
}
try {
Log.log(chalk`{whiteBright \u203A} {bold Uploading build to EAS}`);
const results = await spawnAsync(
'npx',
[
'eas-cli',
'upload',
`--platform=${platform}`,
`--fingerprint=${fingerprintHash}`,
buildPath ? `--build-path=${buildPath}` : '',
'--non-interactive',
'--json',
],
{
cwd: projectRoot,
}
);
// {
// "url": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
// }
const json = JSON.parse(results.stdout.trim());
Log.log(chalk`{whiteBright \u203A} {bold Build successfully uploaded: ${json?.url}}`);
return json?.url;
} catch (error) {
Log.debug('eas-cli error:', error);
}
return null;
}
async function calculateEASFingerprintHashAsync({
projectRoot,
platform,
}: CalculateFingerprintHashProps): Promise<string | null> {
// prefer using `eas fingerprint:generate` because it automatically upload sources
try {
const results = await spawnAsync(
'npx',
['eas-cli', 'fingerprint:generate', `--platform=${platform}`, '--json', '--non-interactive'],
{
cwd: projectRoot,
}
);
// {
// "hash": "203f960b965e154b77dc31c6c42e5582e8d77196"
// }
const json = JSON.parse(results.stdout.trim());
return json?.hash;
} catch (error) {
Log.debug('eas-cli error:', error);
}
return null;
}
const EASBuildCacheProvider: BuildCacheProviderPlugin = {
resolveBuildCache: resolveBuildCacheAsync,
uploadBuildCache: uploadBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASBuildCacheProvider;
@@ -1,121 +0,0 @@
import chalk from 'chalk';
import figures from 'figures';
import { boolish } from 'getenv';
import logSymbols from 'log-symbols';
import terminalLink from 'terminal-link';
type Color = (...text: string[]) => string;
export default class Log {
public static readonly isDebug = boolish('EXPO_DEBUG', false);
public static log(...args: any[]): void {
Log.consoleLog(...args);
}
public static newLine(): void {
Log.consoleLog();
}
public static addNewLineIfNone(): void {
if (!Log.isLastLineNewLine) {
Log.newLine();
}
}
public static error(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.red));
}
public static warn(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
}
public static debug(...args: any[]): void {
if (Log.isDebug) {
Log.consoleLog(...args);
}
}
public static gray(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
}
public static warnDeprecatedFlag(flag: string, message: string): void {
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
}
public static fail(message: string): void {
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
}
public static succeed(message: string): void {
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
}
public static withTick(...args: any[]): void {
Log.consoleLog(chalk.green(figures.tick), ...args);
}
public static withInfo(...args: any[]): void {
Log.consoleLog(chalk.green(figures.info), ...args);
}
private static consoleLog(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
console.log(...args);
}
private static withTextColor(args: any[], chalkColor: Color): string[] {
return args.map(arg => chalkColor(arg));
}
private static isLastLineNewLine = false;
private static updateIsLastLineNewLine(args: any[]): void {
if (args.length === 0) {
Log.isLastLineNewLine = true;
} else {
const lastArg = args[args.length - 1];
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
Log.isLastLineNewLine = true;
} else {
Log.isLastLineNewLine = false;
}
}
}
}
/**
* Prints a link for given URL, using text if provided, otherwise text is just the URL.
* Format links as dim (unless disabled) and with an underline.
*
* @example https://expo.dev
*/
export function link(
url: string,
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
): string {
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
const output = terminalLink(text, url, {
fallback: () =>
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
});
return dim ? chalk.dim(output) : output;
}
/**
* Provide a consistent "Learn more" link experience.
* Format links as dim (unless disabled) with an underline.
*
* @example Learn more: https://expo.dev
*/
export function learnMore(
url: string,
{
learnMoreMessage: maybeLearnMoreMessage,
dim = true,
}: { learnMoreMessage?: string; dim?: boolean } = {}
): string {
return link(url, { text: maybeLearnMoreMessage ?? 'Learn more', dim });
}
@@ -1,4 +0,0 @@
{
"extends": "./tsconfig",
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}
@@ -1,18 +0,0 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "build",
"rootDir": "src",
"typeRoots": ["../../node_modules/@types", "../../ts-declarations", "./node_modules/@types"]
},
"include": ["src/**/*"]
}
+83 -212
View File
@@ -69,8 +69,6 @@ eas --help COMMAND
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:delete [BUILD_ID]`](#eas-builddelete-build_id)
* [`eas build:dev`](#eas-builddev)
* [`eas build:download`](#eas-builddownload)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:resign`](#eas-buildresign)
@@ -107,8 +105,6 @@ eas --help COMMAND
* [`eas env:pull [ENVIRONMENT]`](#eas-envpull-environment)
* [`eas env:push [ENVIRONMENT]`](#eas-envpush-environment)
* [`eas env:update [ENVIRONMENT]`](#eas-envupdate-environment)
* [`eas fingerprint:compare [HASH1] [HASH2]`](#eas-fingerprintcompare-hash1-hash2)
* [`eas fingerprint:generate`](#eas-fingerprintgenerate)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory)
@@ -162,7 +158,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -179,7 +175,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -196,7 +192,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -210,7 +206,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -262,7 +258,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -283,7 +279,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -303,7 +299,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -323,7 +319,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -346,7 +342,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -354,16 +350,16 @@ start a build
```
USAGE
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache] [-s |
--auto-submit-with-profile <value>] [-m <value>] [--build-logger-level trace|debug|info|warn|error|fatal]
[--freeze-credentials] [--verbose-logs] [--json --non-interactive]
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache]
[--auto-submit | --auto-submit-with-profile <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to
"production" if defined in eas.json.
-m, --message=<value> A short message describing the build
-p, --platform=(android|ios|all)
-s, --auto-submit Submit on build complete using the submit profile with the
--auto-submit Submit on build complete using the submit profile with the
same name as the build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with
provided name
@@ -377,14 +373,13 @@ FLAGS
--local Run build locally [experimental]
--non-interactive Run the command in non-interactive mode.
--output=<value> Output path for local build
--verbose-logs Use verbose logs for the build process
--[no-]wait Wait for build(s) to complete
DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -403,7 +398,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -420,7 +415,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -439,48 +434,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
run dev client simulator/emulator build with matching fingerprint or create a new one
```
USAGE
$ eas build:dev [-p ios|android] [-e <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. It must be a profile allowing to create
emulator/simulator internal distribution dev client builds. The "development-simulator"
build profile will be selected by default.
-p, --platform=(ios|android)
DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
download simulator/emulator builds for a given fingerprint hash
```
USAGE
$ eas build:download --fingerprint <value> [-p ios|android] [--dev-client] [--json --non-interactive]
FLAGS
-p, --platform=(ios|android)
--[no-]dev-client Filter only dev-client builds.
--fingerprint=<value> (required) Fingerprint hash of the build to download
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
download simulator/emulator builds for a given fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/download.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:inspect`
@@ -515,7 +469,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -567,7 +521,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -596,7 +550,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -622,7 +576,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -672,7 +626,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -691,7 +645,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -710,7 +664,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -727,7 +681,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -748,7 +702,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -770,7 +724,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -790,7 +744,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -812,7 +766,7 @@ DESCRIPTION
pause a channel to stop it from sending updates
```
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -834,7 +788,7 @@ DESCRIPTION
resume a channel to start sending updates
```
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -867,7 +821,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -890,7 +844,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -911,7 +865,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -928,7 +882,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -946,11 +900,11 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
deploy your Expo Router web build and API Routes
Deploy your Expo Router web build and API Routes.
```
USAGE
@@ -968,13 +922,13 @@ FLAGS
--prod Create a new production deployment.
DESCRIPTION
deploy your Expo Router web build and API Routes
Deploy your Expo Router web build and API Routes.
ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/deploy/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -999,7 +953,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/deploy/alias.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/deploy/alias.ts)_
## `eas deploy:promote`
@@ -1036,7 +990,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1056,7 +1010,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1077,7 +1031,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1098,7 +1052,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1112,7 +1066,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1126,7 +1080,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1155,7 +1109,7 @@ DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/create.ts)_
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1179,7 +1133,7 @@ DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/delete.ts)_
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1200,7 +1154,7 @@ DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/exec.ts)_
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1225,7 +1179,7 @@ DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/get.ts)_
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1250,7 +1204,7 @@ DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/list.ts)_
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1273,7 +1227,7 @@ DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/pull.ts)_
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1294,7 +1248,7 @@ DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/push.ts)_
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1325,83 +1279,7 @@ DESCRIPTION
update an environment variable on the current project or account
```
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
compare fingerprints of the current project, builds, and updates
```
USAGE
$ eas fingerprint:compare [HASH1] [HASH2] [--build-id <value>] [--update-id <value>] [--open] [--environment
development|preview|production] [--json --non-interactive]
ARGUMENTS
HASH1 If provided alone, HASH1 is compared against the current project's fingerprint.
HASH2 If two hashes are provided, HASH1 is compared against HASH2.
FLAGS
--build-id=<value>... Compare the fingerprint with the build with the specified ID
--environment=(development|preview|production) If generating a fingerprint from the local directory, use the
specified environment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--open Open the fingerprint comparison in the browser
--update-id=<value>... Compare the fingerprint with the update with the specified ID
DESCRIPTION
compare fingerprints of the current project, builds, and updates
EXAMPLES
$ eas fingerprint:compare # Compare fingerprints in interactive mode
$ eas fingerprint:compare <FINGERPRINT-HASH> # Compare fingerprint against local directory
$ eas fingerprint:compare <FINGERPRINT-HASH-1> <FINGERPRINT-HASH-2> # Compare provided fingerprints
$ eas fingerprint:compare --build-id <BUILD-ID> # Compare fingerprint from build against local directory
$ eas fingerprint:compare --build-id <BUILD-ID> --environment production # Compare fingerprint from build against local directory with the "production" environment
$ eas fingerprint:compare --build-id <BUILD-ID-1> --build-id <BUILD-ID-2> # Compare fingerprint from a build against another build
$ eas fingerprint:compare --build-id <BUILD-ID> --update-id <UPDATE-ID> # Compare fingerprint from build against fingerprint from update
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
generate fingerprints from the current project
```
USAGE
$ eas fingerprint:generate [-p android|ios] [--environment development|preview|production | -e <value>] [--json
--non-interactive]
FLAGS
-e, --build-profile=<value> Name of the build profile from eas.json.
-p, --platform=(android|ios)
--environment=(development|preview|production) Environment variable's environment
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
generate fingerprints from the current project
EXAMPLES
$ eas fingerprint:generate # Generate fingerprint in interactive mode
$ eas fingerprint:generate --build-profile preview # Generate a fingerprint using the "preview" build profile
$ eas fingerprint:generate --environment preview # Generate a fingerprint using the "preview" environment
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
```
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/env/update.ts)_
## `eas help [COMMAND]`
@@ -1509,7 +1387,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1526,7 +1404,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1543,7 +1421,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1573,7 +1451,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1587,7 +1465,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1610,7 +1488,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1628,7 +1506,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1659,7 +1537,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1701,7 +1579,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1721,7 +1599,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1742,7 +1620,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1766,7 +1644,7 @@ DESCRIPTION
edit all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1788,7 +1666,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1820,7 +1698,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1848,7 +1726,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1868,7 +1746,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1888,7 +1766,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1909,7 +1787,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1929,7 +1807,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1947,7 +1825,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1969,7 +1847,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1986,7 +1864,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2028,7 +1906,7 @@ ALIASES
## `eas deploy [options]`
deploy your Expo Router web build and API Routes
Deploy your Expo Router web build and API Routes.
```
USAGE
@@ -2046,7 +1924,7 @@ FLAGS
--prod Create a new production deployment.
DESCRIPTION
deploy your Expo Router web build and API Routes
Deploy your Expo Router web build and API Routes.
ALIASES
$ eas worker:deploy
@@ -2070,34 +1948,27 @@ DESCRIPTION
create a new workflow configuration YAML file
```
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/workflow/create.ts)_
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:run [FILE]`
run an EAS workflow
Run an EAS workflow
```
USAGE
$ eas workflow:run [FILE] [--non-interactive] [--wait] [--json]
$ eas workflow:run [FILE] [--non-interactive]
ARGUMENTS
FILE Path to the workflow file to run
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--[no-]wait Wait for workflow run to complete
DESCRIPTION
run an EAS workflow
FLAG DESCRIPTIONS
--[no-]wait Wait for workflow run to complete
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
Run an EAS workflow
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/workflow/run.ts)_
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:validate PATH`
@@ -2117,5 +1988,5 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v16.6.1/packages/eas-cli/src/commands/workflow/validate.ts)_
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v14.5.0/packages/eas-cli/src/commands/workflow/validate.ts)_
<!-- commandsstop -->
-3
View File
@@ -12,6 +12,3 @@ if (process.env.TMPDIR) {
(fs.realpath as any).native = jest.fn();
module.exports = fs;
// Ensure requiring node:fs returns the mock too. This is needed for expo/config and expo/json-file.
jest.mock('node:fs', () => fs);
+113 -2836
View File
File diff suppressed because it is too large. Load diff
+5 -8
View File
@@ -1,19 +1,19 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.6.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": "2.1.12",
"@expo/apple-utils": "2.1.5",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.173",
"@expo/eas-json": "16.4.0",
"@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",
@@ -28,7 +28,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.173",
"@expo/steps": "1.0.166",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -38,7 +38,6 @@
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"bplist-parser": "^0.3.0",
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"dateformat": "4.6.3",
@@ -69,7 +68,6 @@
"nanoid": "3.3.8",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"node-stream-zip": "1.15.0",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
@@ -80,7 +78,6 @@
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.5.4",
"set-interval-async": "3.0.3",
"slash": "3.0.0",
"tar": "6.2.1",
"tar-stream": "3.1.7",
-3
View File
@@ -6,7 +6,6 @@ export class ApiV2Error extends Error {
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
readonly expoApiV2ErrorRequestId?: string;
constructor(response: {
message: string;
@@ -14,13 +13,11 @@ export class ApiV2Error extends Error {
stack?: string;
details?: JSONValue;
metadata?: object;
requestId?: string;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
this.expoApiV2ErrorRequestId = response.requestId;
}
}
@@ -16,7 +16,6 @@ describe(ApiV2Client, () => {
stack: 'line 1: hello',
details: { who: 'world' },
metadata: { an: 'object' },
requestId: 'request-id',
},
],
});
@@ -38,7 +37,6 @@ describe(ApiV2Client, () => {
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
expect(error.expoApiV2ErrorRequestId).toEqual('request-id');
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
@@ -38,7 +38,7 @@ describe(ensureProjectConfiguredAsync, () => {
}),
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
const vcsClientMock = jest.mocked(new GitClient());
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
@@ -60,7 +60,7 @@ describe(ensureProjectConfiguredAsync, () => {
});
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy();
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
const vcsClientMock = jest.mocked(new GitClient());
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
@@ -1,26 +0,0 @@
import Log from '../../../log';
import { maybeWarnAboutNonStandardBuildType } from '../build';
const warn = jest.spyOn(Log, 'warn');
describe('maybeWarnAboutNonStandardBuildType', () => {
it('should not warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildRelease',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'release' });
expect(warn).not.toHaveBeenCalled();
});
it('should warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildStage',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'stage' });
expect(warn).toHaveBeenCalled();
});
});
+1 -41
View File
@@ -1,7 +1,6 @@
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import getenv from 'getenv';
import nullthrows from 'nullthrows';
import { transformJob } from './graphql';
@@ -13,7 +12,7 @@ import AndroidCredentialsProvider, {
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
@@ -66,10 +65,6 @@ This means that it will most likely produce an AAB and you will not be able to i
ctx.vcsClient
);
if (gradleContext?.buildType) {
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: gradleContext.buildType });
}
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
@@ -173,38 +168,3 @@ async function ensureAndroidCredentialsAsync(
source: credentialsSource,
};
}
export function maybeWarnAboutNonStandardBuildType({
buildProfile,
buildType,
}: {
buildProfile: Pick<CommonContext<Platform.ANDROID>['buildProfile'], 'gradleCommand'>;
buildType: string;
}): void {
const shouldSuppressWarning = getenv.boolish('EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING', false);
if (shouldSuppressWarning) {
return;
}
const knownValidBuildTypes = ['debug', 'release'];
if (!knownValidBuildTypes.includes(buildType)) {
Log.warn(
`Custom gradle command "${chalk.bold(
buildProfile.gradleCommand
)}" has non-standard build type: "${chalk.bold(
buildType
)}". Expected the command to end with "${chalk.bold('...Debug')}" or "${chalk.bold(
'...Release'
)}"`
);
Log.warn(
`Ensure the command is spelled correctly and ${chalk.bold(
'build.gradle'
)} is configured correctly. To suppress this warning, set ${chalk.bold(
'EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING=true'
)}.`
);
Log.warn(learnMore('https://developer.android.com/build/build-variants#build-types'));
}
}
@@ -8,7 +8,6 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -30,7 +29,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
@@ -73,15 +73,11 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
env: {
...buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
env: buildProfile.env,
},
cache: {
...cacheDefaults,
@@ -96,7 +92,6 @@ export async function prepareJobAsync(
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
buildType,
username,
...(ctx.android.versionCodeOverride && {
+14 -5
View File
@@ -35,10 +35,8 @@ import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import {
LocalFile,
assertProjectTarballSizeDoesNotExceedLimit,
makeProjectMetadataFileAsync,
makeProjectTarballAsync,
maybeWarnAboutProjectTarballSize,
reviewAndCommitChangesAsync,
} from './utils/repository';
import { BuildEvent } from '../analytics/AnalyticsManager';
@@ -46,7 +44,6 @@ import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import { createFingerprintAsync } from '../fingerprint/cli';
import {
AppPlatform,
BuildFragment,
@@ -69,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';
@@ -277,8 +275,19 @@ async function uploadProjectAsync<TPlatform extends Platform>(
);
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
maybeWarnAboutProjectTarballSize(projectTarball.size);
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size);
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
}
projectTarballPath = projectTarball.path;
const [bucketKey, { metadataLocation }] = await Promise.all([
-73
View File
@@ -4,7 +4,6 @@ import fs from 'fs-extra';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
@@ -53,78 +52,6 @@ async function configureAsync({
}
}
export async function doesBuildProfileExistAsync({
projectDir,
profileName,
}: {
projectDir: string;
profileName: string;
}): Promise<boolean> {
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJson = await easJsonAccessor.readRawJsonAsync();
if (!easJson.build?.[profileName]) {
return false;
}
return true;
} catch (error) {
Log.error(`We were unable to read ${chalk.bold('eas.json')} contents. Error: ${error}.`);
throw error;
}
}
export async function createBuildProfileAsync({
projectDir,
profileName,
profileContents,
vcsClient,
nonInteractive,
}: {
projectDir: string;
profileName: string;
profileContents: Record<string, any>;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<void> {
const spinner = ora(`Adding "${profileName}" build profile to ${chalk.bold('eas.json')}`).start();
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
return {
...easJsonRawObject,
build: {
...easJsonRawObject.build,
[profileName]: profileContents,
},
};
});
await easJsonAccessor.writeAsync();
spinner.succeed(
`Successfully added "${profileName}" build profile to ${chalk.bold('eas.json')}.`
);
if (await vcsClient.isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(
vcsClient,
`Add "${profileName}" build profile to eas.json`,
{
nonInteractive,
}
);
}
} catch (error) {
spinner.fail(
`We were not able to configure "${profileName}" build profile inside of ${chalk.bold(
'eas.json'
)}. Error: ${error}.`
);
throw error;
}
}
const EAS_JSON_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
-1
View File
@@ -63,6 +63,5 @@ export interface BuildContext<T extends Platform> {
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
isVerboseLoggingEnabled: boolean;
env: Record<string, string>;
}
@@ -43,7 +43,6 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel,
freezeCredentials,
repack,
isVerboseLoggingEnabled,
env,
}: {
buildProfileName: string;
@@ -66,7 +65,6 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled: boolean;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
@@ -153,7 +151,6 @@ export async function createBuildContextAsync<T extends Platform>({
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
isVerboseLoggingEnabled,
env,
};
if (platform === Platform.ANDROID) {
@@ -1,12 +1,17 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { isEnvironment } from './utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
buildProfile,
buildProfileName,
-2
View File
@@ -10,7 +10,6 @@ import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
@@ -29,7 +28,6 @@ export async function createIosContextAsync(
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
}
checkNodeEnvVariable(ctx);
@@ -9,7 +9,6 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -32,7 +31,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
+1 -6
View File
@@ -65,17 +65,13 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
env: {
...buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
env: buildProfile.env,
},
cache: {
...cacheDefaults,
@@ -92,7 +88,6 @@ export async function prepareJobAsync(
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
username,
...(ctx.ios.buildNumberOverride && {
version: {
+1 -1
View File
@@ -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.171';
const PLUGIN_PACKAGE_VERSION = '1.0.163';
export enum LocalBuildMode {
/**
+1
View File
@@ -62,6 +62,7 @@ export async function collectMetadataAsync<T extends Platform>(
requiredPackageManager: ctx.requiredPackageManager ?? undefined,
selectedImage: ctx.buildProfile.image,
customNodeVersion: ctx.buildProfile.node,
environment: ctx.buildProfile.environment,
simulator: 'simulator' in ctx.buildProfile && ctx.buildProfile.simulator,
};
return sanitizeMetadata(metadata);
+26 -60
View File
@@ -11,7 +11,6 @@ import {
import { LoggerLevel } from '@expo/logger';
import assert from 'assert';
import chalk from 'chalk';
import { pathExists } from 'fs-extra';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from './android/build';
@@ -63,7 +62,7 @@ import {
validateBuildProfileVersionSettingsAsync,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { getEasBuildRunCachedAppPath, runAsync } from '../run/run';
import { runAsync } from '../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
import { createSubmissionContextAsync } from '../submit/context';
import {
@@ -98,32 +97,18 @@ export interface BuildFlags {
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled?: boolean;
}
export async function runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags,
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm,
envOverride,
}: {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
projectDir: string;
flags: BuildFlags;
actor: Actor;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
downloadSimBuildAutoConfirm?: boolean;
envOverride?: Env;
}): Promise<{
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
vcsClient: Client,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
): Promise<{
buildIds: string[];
buildProfiles?: ProfileData<'build'>[];
}> {
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
@@ -207,15 +192,13 @@ export async function runBuildAndSubmitAsync({
for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform);
const { env } = !envOverride
? await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
})
: { env: envOverride };
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
@@ -325,7 +308,7 @@ export async function runBuildAndSubmitAsync({
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
@@ -350,7 +333,6 @@ export async function runBuildAndSubmitAsync({
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
buildProfiles,
};
}
@@ -402,7 +384,6 @@ async function prepareAndStartBuildAsync({
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
env,
});
@@ -550,29 +531,18 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
}
}
export async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
assert(build.artifacts?.applicationArchiveUrl);
const cachedAppPath = getEasBuildRunCachedAppPath(build.project.id, build.id, build.platform);
if (await pathExists(cachedAppPath)) {
Log.newLine();
Log.log(`Using cached app...`);
await runAsync(cachedAppPath, build.platform);
return;
}
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform,
cachedAppPath
build.platform
);
await runAsync(buildPath, build.platform);
}
async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[],
flags: BuildFlags,
autoConfirm?: boolean
flags: BuildFlags
): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
@@ -580,15 +550,11 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine();
const confirm =
autoConfirm ??
(await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'
}?`,
}));
const confirm = await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
});
if (confirm) {
await downloadAndRunAsync(simBuild);
}
@@ -1,26 +0,0 @@
import { BuildProfile } from '@expo/eas-json';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
type Environment = NonNullable<BuildProfile['environment']>;
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
production: EnvironmentVariableEnvironment.Production,
preview: EnvironmentVariableEnvironment.Preview,
development: EnvironmentVariableEnvironment.Development,
};
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export function buildProfileEnvironmentToEnvironment(
environment: BuildProfile['environment']
): EnvironmentVariableEnvironment | null {
if (!environment) {
return null;
}
return BuildProfileEnvironmentToEnvironment[environment];
}
+1 -29
View File
@@ -4,7 +4,7 @@ import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
@@ -169,34 +169,6 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
return { size, path: tarPath };
}
export function maybeWarnAboutProjectTarballSize(size: number): void {
if (size <= 150 /* MiB */ * 1024 /* KiB */ * 1024 /* B */) {
return;
}
Log.warn(
`Your project archive is ${formatBytes(
size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
const MAX_ALLOWED_PROJECT_TARBALL_SIZE =
2 /* GiB */ * 1024 /* MiB */ * 1024 /* KiB */ * 1024; /* B */
export function assertProjectTarballSizeDoesNotExceedLimit(size: number): void {
if (size <= MAX_ALLOWED_PROJECT_TARBALL_SIZE) {
return;
}
throw new Error(
`Project archive is too big. Maximum allowed size is ${formatBytes(
MAX_ALLOWED_PROJECT_TARBALL_SIZE
)}.`
);
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
-9
View File
@@ -72,12 +72,3 @@ export function getProjectGitHubSettingsUrl(accountName: string, projectName: st
getExpoWebsiteBaseUrl()
).toString();
}
export function getHostingDeploymentsUrl(accountName: string, projectName: string): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/hosting/deployments`,
getExpoWebsiteBaseUrl()
).toString();
}
+1 -1
View File
@@ -289,7 +289,7 @@ export async function doesChannelExistAsync(
{ appId, channelName }: { appId: string; channelName: string }
): Promise<boolean> {
try {
await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId,
channelName,
});
@@ -27,7 +27,7 @@ import {
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import Log, { link } from '../log';
import Log from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
@@ -264,16 +264,6 @@ export default abstract class EasCommand extends Command {
const defaultMsg = `${messageLine}${requestIdLine}`;
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
if (defaultMsg.includes('ScopedAccountActorViewerContext') && process.env.EAS_BUILD) {
// We're in EAS, authenticated with a scoped account actor access token.
// We may have not added the scoped actor privacy rule to the right place yet.
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nWe are in the process of migrating EAS to a more granular permissioning system. If you believe what you're doing is a legitimate operation you should be able to perform, report this to us at ${link(
'https://expo.dev/contact'
)}\n\nOriginal error message: ${defaultMsg}`;
}
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nThis can sometimes happen if you are logged in as incorrect user.\nRun ${chalk.bold(
@@ -43,7 +43,6 @@ export async function fetchBuildsAsync({
platform?: RequestedPlatform;
profile?: string;
hasFingerprint?: boolean;
fingerprintHash?: string;
};
}): Promise<BuildFragment[]> {
let builds: BuildFragment[];
@@ -57,9 +56,6 @@ export async function fetchBuildsAsync({
if (filters?.hasFingerprint) {
queryFilters['hasFingerprint'] = filters.hasFingerprint;
}
if (filters?.fingerprintHash) {
queryFilters['fingerprintHash'] = filters.fingerprintHash;
}
if (!filters?.statuses) {
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
@@ -5,7 +5,7 @@ import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPublicExpoConfigAsync } from '../../project/expoConfig';
export type GetServerSideEnvironmentVariablesFn = (
type GetServerSideEnvironmentVariablesFn = (
maybeEnv?: Record<string, string>
) => Promise<Record<string, string>>;
@@ -78,12 +78,7 @@ describe(getProjectIdAsync, () => {
it('gets the project ID from app config if exists', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -96,7 +91,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).resolves.toEqual('1234');
@@ -104,13 +99,7 @@ describe(getProjectIdAsync, () => {
it('throws when the owner is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '54.0.0',
name: 'test',
slug: 'test',
owner: 'wat',
extra: { eas: { projectId: '1234' } },
},
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -123,13 +112,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
owner: 'wat',
extra: { eas: { projectId: '1234' } },
},
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
@@ -139,143 +122,83 @@ describe(getProjectIdAsync, () => {
);
});
describe('when sdkVersion is less than 53', () => {
it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
describe('when sdkVersion is 53 or higher', () => {
it('does not throw when the owner is not specified', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '53.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
sdkVersion: '53.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{ nonInteractive: false }
)
).resolves.not.toThrow();
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'wat',
extra: { eas: { projectId: '1234' } },
},
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -288,7 +211,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
@@ -299,17 +222,10 @@ describe(getProjectIdAsync, () => {
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '2345' } },
},
config: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
@@ -317,7 +233,7 @@ describe(getProjectIdAsync, () => {
const projectId = await getProjectIdAsync(
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
{ name: 'test', slug: 'test' },
{
nonInteractive: false,
}
@@ -336,9 +252,7 @@ describe(getProjectIdAsync, () => {
});
it('throws if writing the ID back to the config fails', async () => {
jest
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail',
config: null,
@@ -348,22 +262,13 @@ describe(getProjectIdAsync, () => {
.mockImplementation(async () => '4567');
await expect(
getProjectIdAsync(
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
{ nonInteractive: false }
)
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
).rejects.toThrow();
});
it('throws if extra.eas.projectId is not a string', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: 1234 } },
},
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -376,7 +281,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
{ name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
{ nonInteractive: false }
)
).rejects.toThrow(
@@ -1,7 +1,6 @@
import { ExpoConfig, getProjectConfigDescription } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import semver from 'semver';
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
@@ -128,33 +127,29 @@ export async function validateOrSetProjectIdAsync({
);
}
const sdkVersion = exp.sdkVersion;
// SDK 53 and above no longer require owner field in app config
if (sdkVersion && semver.satisfies(sdkVersion, '< 53.0.0')) {
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
}
-351
View File
@@ -1,351 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import {
createBuildProfileAsync,
doesBuildProfileExistAsync,
ensureProjectConfiguredAsync,
} from '../../build/configure';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { downloadAndRunAsync, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { createFingerprintAsync } from '../../fingerprint/cli';
import { BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync, promptAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { Client } from '../../vcs/vcs';
const DEFAULT_EAS_BUILD_RUN_PROFILE_NAME = 'development-simulator';
export default class BuildDev extends EasCommand {
static override hidden: true;
static override description =
'run dev client simulator/emulator build with matching fingerprint or create a new one';
static override flags = {
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
profile: Flags.string({
char: 'e',
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
helpValue: 'PROFILE_NAME',
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
...this.ContextOptions.Analytics,
...this.ContextOptions.ProjectId,
};
protected override async runAsync(): Promise<any> {
const { flags } = await this.parse(BuildDev);
const {
loggedIn: { actor, graphqlClient },
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
projectId,
} = await this.getContextAsync(BuildDev, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const platform = await this.selectPlatformAsync(flags.platform);
if (process.platform !== 'darwin' && platform === Platform.IOS) {
Errors.error('Running iOS builds in simulator is only supported on macOS.', { exit: 1 });
}
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: false,
vcsClient,
});
const buildProfile = await this.ensureValidBuildRunProfileExistsAsync({
projectDir,
platform,
selectedBuildProfileName: flags.profile,
vcsClient,
});
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const fingerprint = await createFingerprintAsync(projectDir, {
env,
workflow,
platforms: [platform],
});
if (!fingerprint) {
Errors.error('Failed to calculate fingerprint', { exit: 1 });
}
Log.log(`✨ Calculated fingerprint hash: ${fingerprint.hash}`);
Log.newLine();
const builds = await this.getBuildsAsync({
graphqlClient,
projectId,
platform,
fingerprint,
});
if (builds.length !== 0) {
const build = builds[0];
Log.succeed(
`🎯 Found successful build with matching fingerprint on EAS servers. Running it...`
);
if (build.artifacts?.applicationArchiveUrl) {
await downloadAndRunAsync(build);
await this.startDevServerAsync({ projectDir, platform });
return;
} else {
Log.warn('Artifacts for this build expired. New build will be started.');
}
}
Log.log('🚀 No successful build with matching fingerprint found. Starting a new build...');
const previousBuildsForSelectedProfile = await this.getBuildsAsync({
graphqlClient,
projectId,
platform,
});
if (
previousBuildsForSelectedProfile.length > 0 &&
previousBuildsForSelectedProfile[0].metrics?.buildDuration
) {
Log.log(
`🕒 Previous build for "${buildProfile.profileName}" profile completed in ${Math.floor(
previousBuildsForSelectedProfile[0].metrics.buildDuration / 60000
)} minutes.`
);
}
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags: {
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
nonInteractive: false,
freezeCredentials: false,
wait: true,
clearCache: false,
json: false,
autoSubmit: false,
localBuildOptions: {},
repack: false,
profile: flags.profile ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
},
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm: true,
envOverride: env,
});
await this.startDevServerAsync({ projectDir, platform });
}
private async selectPlatformAsync(platform?: Platform): Promise<Platform> {
if (platform) {
return platform;
}
const { resolvedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'resolvedPlatform',
choices: [
{ title: 'Android', value: Platform.ANDROID },
{ title: 'iOS', value: Platform.IOS },
],
});
return resolvedPlatform;
}
private async validateBuildRunProfileAsync({
platform,
buildProfile,
buildProfileName,
}: {
platform: Platform;
buildProfile: BuildProfile;
buildProfileName: string;
}): Promise<void> {
if (buildProfile.developmentClient !== true) {
Errors.error(
`Profile "${buildProfileName}" must specify "developmentClient: true" to create a dev client build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
if (buildProfile.distribution !== 'internal') {
Errors.error(
`Profile "${buildProfileName}" must specify "distribution: internal" in order to work with eas build:dev command. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
if (platform === Platform.IOS) {
const iosProfile = buildProfile as BuildProfile<Platform.IOS>;
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
Errors.error(
`Profile "${buildProfileName}" must specify "ios.simulator: true" or "withoutCredentials: true" to create an iOS simulator build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
} else {
const androidProfile = buildProfile as BuildProfile<Platform.ANDROID>;
if (
androidProfile.distribution !== 'internal' &&
androidProfile.withoutCredentials !== true
) {
Errors.error(
`Profile "${buildProfileName}" must specify "distribution: internal" or "withoutCredentials: true" to create an Android emulator build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
}
}
private async ensureValidBuildRunProfileExistsAsync({
projectDir,
platform,
selectedBuildProfileName,
vcsClient,
}: {
projectDir: string;
platform: Platform;
selectedBuildProfileName?: string;
vcsClient: Client;
}): Promise<ProfileData<'build'>> {
if (
!!selectedBuildProfileName ||
(await doesBuildProfileExistAsync({
projectDir,
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
}))
) {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const [buildProfile] = await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [platform],
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
projectDir,
});
await this.validateBuildRunProfileAsync({
buildProfileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
platform,
buildProfile: buildProfile.profile,
});
} else {
const createBuildProfile = await confirmAsync({
message: `We want to go ahead and generate "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile for you, that matches eas build:dev criteria. Do you want to proceed?`,
});
if (!createBuildProfile) {
Errors.error(
'Come back later or specify different build compliant with eas build:dev requirements by using "--profile" flag.',
{ exit: 1 }
);
}
await createBuildProfileAsync({
projectDir,
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
profileContents: {
developmentClient: true,
distribution: 'internal',
ios: {
simulator: true,
},
environment: 'development',
},
nonInteractive: false,
vcsClient,
});
}
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const [buildProfile] = await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [platform],
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
projectDir,
});
return buildProfile;
}
private async getBuildsAsync({
graphqlClient,
projectId,
platform,
fingerprint,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
fingerprint?: { hash: string };
}): Promise<BuildFragment[]> {
return await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
filter: {
platform: toAppPlatform(platform),
fingerprintHash: fingerprint?.hash,
status: BuildStatus.Finished,
simulator: platform === Platform.IOS ? true : undefined,
distribution: platform === Platform.ANDROID ? DistributionType.Internal : undefined,
developmentClient: true,
},
offset: 0,
limit: 1,
});
}
private async startDevServerAsync({
projectDir,
platform,
}: {
projectDir: string;
platform: Platform;
}): Promise<void> {
Log.newLine();
Log.log(
`Starting development server: ${chalk.dim(
`npx expo start --dev-client ${platform === Platform.IOS ? '--ios' : '--android'}`
)}`
);
await expoCommandAsync(projectDir, [
'start',
'--dev-client',
platform === Platform.IOS ? '--ios' : '--android',
]);
}
}
@@ -1,166 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import { pathExists } from 'fs-extra';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { getEasBuildRunCachedAppPath } from '../../run/run';
import { downloadAndMaybeExtractAppAsync } from '../../utils/download';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class Download extends EasCommand {
static override description = 'download simulator/emulator builds for a given fingerprint hash';
static override flags = {
fingerprint: Flags.string({
description: 'Fingerprint hash of the build to download',
required: true,
}),
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
'dev-client': Flags.boolean({
description: 'Filter only dev-client builds.',
allowNo: true,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectId,
};
async runAsync(): Promise<void> {
const {
flags: {
json: jsonFlag,
platform,
fingerprint,
'dev-client': developmentClient,
'non-interactive': nonInteractive,
},
} = await this.parse(Download);
const {
loggedIn: { graphqlClient },
projectId,
} = await this.getContextAsync(Download, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const selectedPlatform = await resolvePlatformAsync({ nonInteractive, platform });
const build = await this.getBuildAsync({
graphqlClient,
projectId,
platform: selectedPlatform,
fingerprintHash: fingerprint,
developmentClient,
});
const buildArtifactPath = await this.getPathToBuildArtifactAsync(build, selectedPlatform);
if (jsonFlag) {
const jsonResults = { path: buildArtifactPath };
printJsonOnlyOutput(jsonResults);
} else {
Log.log(`Build downloaded to ${chalk.bold(buildArtifactPath)}`);
}
}
private async getBuildAsync({
graphqlClient,
projectId,
platform,
fingerprintHash,
developmentClient,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: AppPlatform;
fingerprintHash: string;
developmentClient: boolean;
}): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
filter: {
platform,
fingerprintHash,
status: BuildStatus.Finished,
simulator: platform === AppPlatform.Ios ? true : undefined,
distribution: platform === AppPlatform.Android ? DistributionType.Internal : undefined,
developmentClient,
},
offset: 0,
limit: 1,
});
if (builds.length === 0) {
throw new Errors.CLIError(
`No builds available for ${platform} with fingerprint hash ${fingerprintHash}`
);
}
Log.succeed(`🎯 Found successful build with matching fingerprint on EAS servers.`);
return builds[0];
}
async getPathToBuildArtifactAsync(build: BuildFragment, platform: AppPlatform): Promise<string> {
const cachedBuildArtifactPath = getEasBuildRunCachedAppPath(
build.project.id,
build.id,
platform
);
if (await pathExists(cachedBuildArtifactPath)) {
Log.newLine();
Log.log(`Using cached build...`);
return cachedBuildArtifactPath;
}
if (!build.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
platform,
cachedBuildArtifactPath
);
}
}
async function resolvePlatformAsync({
nonInteractive,
platform,
}: {
nonInteractive: boolean;
platform?: Platform;
}): Promise<AppPlatform> {
if (nonInteractive && !platform) {
throw new Error('Platform must be provided in non-interactive mode');
}
if (platform) {
return toAppPlatform(platform);
}
const { selectedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'selectedPlatform',
choices: [
{ title: 'Android', value: AppPlatform.Android },
{ title: 'iOS', value: AppPlatform.Ios },
],
});
return selectedPlatform;
}
+5 -64
View File
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -17,7 +17,6 @@ import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform'
import { selectAsync } from '../../prompts';
import uniq from '../../utils/expodash/uniq';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData } from '../../utils/profiles';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
interface RawBuildFlags {
@@ -37,7 +36,6 @@ interface RawBuildFlags {
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
'verbose-logs'?: boolean;
repack: boolean;
}
@@ -80,12 +78,10 @@ export default class Build extends EasCommand {
description: 'Clear cache before the build',
}),
'auto-submit': Flags.boolean({
char: 's',
default: false,
description:
'Submit on build complete using the submit profile with the same name as the build profile',
exclusive: ['auto-submit-with-profile'],
aliases: ['submit'],
}),
'auto-submit-with-profile': Flags.string({
description: 'Submit on build complete using the submit profile with provided name',
@@ -119,10 +115,6 @@ export default class Build extends EasCommand {
hidden: true,
description: 'Use the golden dev client build repack flow as it works for onboarding',
}),
'verbose-logs': Flags.boolean({
default: false,
description: 'Use verbose logs for the build process',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -166,20 +158,15 @@ export default class Build extends EasCommand {
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
const { buildProfiles } = await runBuildAndSubmitAsync({
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir,
flags: flagsWithPlatform,
flagsWithPlatform,
actor,
getDynamicPrivateProjectConfigAsync,
});
this.maybeSuggestUsingEasBuildDev({
buildProfiles,
nonInteractive: flags.nonInteractive,
});
getDynamicPrivateProjectConfigAsync
);
}
private sanitizeFlags(
@@ -243,7 +230,6 @@ export default class Build extends EasCommand {
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
isVerboseLoggingEnabled: flags['verbose-logs'],
repack: flags.repack,
};
}
@@ -280,51 +266,6 @@ export default class Build extends EasCommand {
requestedPlatform,
};
}
private maybeSuggestUsingEasBuildDev({
buildProfiles,
nonInteractive,
}: {
buildProfiles: ProfileData<'build'>[] | undefined;
nonInteractive: boolean;
}): void {
// suggest using eas build:dev if the build configuration results in simulator/emulator dev client build
if (
!nonInteractive &&
!process.env.CI &&
buildProfiles?.some(({ profile, platform }) => {
if (profile.developmentClient !== true) {
return false;
}
if (profile.distribution !== 'internal') {
return false;
}
if (platform === Platform.IOS) {
const iosProfile = profile as BuildProfile<Platform.IOS>;
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
return false;
}
} else {
const androidProfile = profile as BuildProfile<Platform.ANDROID>;
if (
androidProfile.distribution !== 'internal' &&
androidProfile.withoutCredentials !== true
) {
return false;
}
}
return true;
})
) {
Log.newLine();
Log.log(
`🔎 TIP: You are using a build configuration that could benefit from using ${chalk.bold(
'eas build:dev'
)} command. Run it to install and run cached development build, or create a new one if a compatible build doesn't exist yet.`
);
}
}
}
export async function handleDeprecatedEasJsonAsync(
@@ -99,12 +99,12 @@ export default class BuildInspect extends EasCommand {
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else {
try {
await runBuildAndSubmitAsync({
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir,
flags: {
{
nonInteractive: false,
freezeCredentials: false,
wait: true,
@@ -124,8 +124,8 @@ export default class BuildInspect extends EasCommand {
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync,
});
getDynamicPrivateProjectConfigAsync
);
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
@@ -6,7 +6,8 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import { RequestedPlatform } from '../../platform';
import { enableJsonOutput } from '../../utils/json';
import GitClient from '../../vcs/clients/git';
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,24 +64,18 @@ export default class BuildInternal extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
withServerSideEnvironment: null,
});
if (vcsClient instanceof GitClient) {
// `build:internal` is run on EAS workers and the repo may have been changed
// by pre-install hooks or other scripts. We don't want to require committing changes
// to continue the build.
vcsClient.requireCommit = false;
}
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync({
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir,
flags: {
{
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
@@ -96,7 +91,7 @@ export default class BuildInternal extends EasCommand {
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync,
});
getDynamicPrivateProjectConfigAsync
);
}
}
+14 -1
View File
@@ -1,6 +1,7 @@
import { Errors, Flags } from '@oclif/core';
import assert from 'assert';
import { pathExists } from 'fs-extra';
import path from 'path';
import { getLatestBuildAsync, listAndSelectBuildOnAppAsync } from '../../build/queries';
import EasCommand from '../../commandUtils/EasCommand';
@@ -16,12 +17,13 @@ import Log from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, getEasBuildRunCachedAppPath, runAsync } from '../../run/run';
import { RunArchiveFlags, runAsync } from '../../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../../run/utils';
import {
downloadAndMaybeExtractAppAsync,
extractAppFromLocalArchiveAsync,
} from '../../utils/download';
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
interface RawRunFlags {
latest?: boolean;
@@ -243,6 +245,17 @@ async function maybeGetBuildAsync(
}
}
function getEasBuildRunCachedAppPath(
projectId: string,
buildId: string,
platform: AppPlatform
): string {
return path.join(
getEasBuildRunCacheDirectoryPath(),
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
);
}
async function getPathToSimulatorBuildAppAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
@@ -52,7 +52,7 @@ export default class ChannelDelete extends EasCommand {
let channelId, channelName;
if (nameArg) {
const { id, name } = await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
const { id, name } = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: nameArg,
});
@@ -94,7 +94,7 @@ export default class ChannelEdit extends EasCommand {
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
@@ -86,7 +86,7 @@ export default class ChannelPause extends EasCommand {
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
@@ -86,7 +86,7 @@ export default class ChannelResume extends EasCommand {
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
+3 -35
View File
@@ -1,17 +1,14 @@
import { format as formatTimeAgo } from '@expo/timeago.js';
import { Flags } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import fs from 'node:fs';
import * as path from 'node:path';
import { getHostingDeploymentsUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import Log, { link } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { createProgressTracker } from '../../utils/progress';
import * as WorkerAssets from '../../worker/assets';
@@ -68,7 +65,7 @@ interface DeployInProgressParams {
}
export default class WorkerDeploy extends EasCommand {
static override description = 'deploy your Expo Router web build and API Routes';
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`];
static override state = 'preview';
@@ -123,10 +120,7 @@ export default class WorkerDeploy extends EasCommand {
} = await this.getContextAsync(WorkerDeploy, { ...flags, withServerSideEnvironment: null });
const projectDist = await resolveExportedProjectAsync(flags, projectDir);
const { projectId, exp } = await getDynamicPrivateProjectConfigAsync();
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const { projectId } = await getDynamicPrivateProjectConfigAsync();
logExportedProjectInfo(projectDist);
@@ -305,32 +299,6 @@ export default class WorkerDeploy extends EasCommand {
progress.succeed('Created deployment');
} catch (error: any) {
progress.fail('Failed to create deployment');
if (
flags.isProduction &&
error instanceof CombinedError &&
error.graphQLErrors.some(err => {
return (
err.extensions?.errorCode === 'UNAUTHORIZED_ERROR' &&
err.message.includes('AppDevDomainNameEntity') &&
err.message.includes('CREATE')
);
})
) {
throw new Error(
`You have specified the new deployment should be a production deployment, but a production domain has not been set up yet for this app.\n\nRun ${chalk.bold(
'eas deploy --prod'
)} as an app admin on your machine or promote an existing deployment to production on the ${link(
getHostingDeploymentsUrl(accountName, projectName),
{
dim: false,
text: 'Hosting Dashboard',
fallback: `Hosting Dashboard (${getHostingDeploymentsUrl(accountName, projectName)})`,
}
)} to set up a production domain and then try again.`
);
}
throw error;
}
@@ -1,106 +1,35 @@
import { Platform } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import openBrowserAsync from 'better-opn';
import chalk from 'chalk';
import { getExpoWebsiteBaseUrl } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
import { GetServerSideEnvironmentVariablesFn } from '../../commandUtils/context/ServerSideEnvironmentVariablesContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EasEnvironmentFlagParameters,
EasNonInteractiveAndJsonFlags,
} from '../../commandUtils/flags';
import { diffFingerprint } from '../../fingerprint/cli';
import { abridgedDiff } from '../../fingerprint/diff';
import { Fingerprint, FingerprintDiffItem } from '../../fingerprint/types';
import {
getFingerprintInfoFromLocalProjectForPlatformsAsync,
stringToAppPlatform,
} from '../../fingerprint/utils';
import {
AppPlatform,
BuildFragment,
BuildStatus,
EnvironmentVariableEnvironment,
FingerprintFragment,
UpdateFragment,
} from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { AppPlatform, BuildStatus } from '../../graphql/generated';
import { FingerprintMutation } from '../../graphql/mutations/FingerprintMutation';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { FingerprintQuery } from '../../graphql/queries/FingerprintQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
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, printJsonOnlyOutput } from '../../utils/json';
import { Client } from '../../vcs/vcs';
enum FingerprintOriginType {
Build = 'build',
Update = 'update',
Hash = 'hash',
Project = 'project',
}
type FingerprintOrigin = {
type: FingerprintOriginType;
build?: BuildFragment;
update?: UpdateFragment;
};
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 strict = false;
static override examples = [
'$ eas fingerprint:compare \t # Compare fingerprints in interactive mode',
'$ eas fingerprint:compare <FINGERPRINT-HASH> \t # Compare fingerprint against local directory',
'$ eas fingerprint:compare <FINGERPRINT-HASH-1> <FINGERPRINT-HASH-2> \t # Compare provided fingerprints',
'$ eas fingerprint:compare --build-id <BUILD-ID> \t # Compare fingerprint from build against local directory',
'$ eas fingerprint:compare --build-id <BUILD-ID> --environment production \t # Compare fingerprint from build against local directory with the "production" environment',
'$ eas fingerprint:compare --build-id <BUILD-ID-1> --build-id <BUILD-ID-2>\t # Compare fingerprint from a build against another build',
'$ eas fingerprint:compare --build-id <BUILD-ID> --update-id <UPDATE-ID>\t # Compare fingerprint from build against fingerprint from update',
'$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> \t # Compare fingerprint from update against provided fingerprint',
];
static override args = [
{
name: 'hash1',
description:
"If provided alone, HASH1 is compared against the current project's fingerprint.",
required: false,
},
{
name: 'hash2',
description: 'If two hashes are provided, HASH1 is compared against HASH2.',
required: false,
},
];
static override 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',
multiple: true,
}),
'update-id': Flags.string({
aliases: ['updateId'],
description: 'Compare the fingerprint with the update with the specified ID',
multiple: true,
}),
open: Flags.boolean({
description: 'Open the fingerprint comparison in the browser',
}),
environment: Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description:
'If generating a fingerprint from the local directory, use the specified environment.',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -110,95 +39,85 @@ export default class FingerprintCompare extends EasCommand {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
...this.ContextOptions.ServerSideEnvironmentVariables,
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(FingerprintCompare);
const { hash1, hash2 } = args;
const {
json,
'non-interactive': nonInteractive,
'build-id': buildIds,
'update-id': updateIds,
open,
environment,
} = flags;
const [buildId1, buildId2] = buildIds ?? [];
const [updateId1, updateId2] = updateIds ?? [];
const { flags } = await this.parse(FingerprintCompare);
const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags;
const {
projectId,
privateProjectConfig: { projectDir },
loggedIn: { graphqlClient },
vcsClient,
getServerSideEnvironmentVariablesAsync,
} = await this.getContextAsync(FingerprintCompare, {
nonInteractive,
withServerSideEnvironment: environment ?? null,
withServerSideEnvironment: null,
});
if (json) {
if (jsonFlag) {
enableJsonOutput();
}
const firstFingerprintInfo = await getFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
{
nonInteractive,
buildId: buildId1,
updateId: updateId1,
hash: hash1,
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');
}
);
const { fingerprint: firstFingerprint, origin: firstFingerprintOrigin } = firstFingerprintInfo;
const isFirstFingerprintSpecifiedByFlagOrArg = hash1 || buildId1 || updateId1;
const isSecondFingerprintSpecifiedByFlagOrArg = hash2 || buildId2 || updateId2;
const secondFingerprintInfo = await getFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
{
nonInteractive,
buildId: buildId2,
updateId: updateId2,
hash: hash2,
useProjectFingerprint:
isFirstFingerprintSpecifiedByFlagOrArg && !isSecondFingerprintSpecifiedByFlagOrArg,
environmentForProjectFingerprint: environment,
},
firstFingerprintInfo
);
const { fingerprint: secondFingerprint, origin: secondFingerprintOrigin } =
secondFingerprintInfo;
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
return;
}
}
if (json) {
printJsonOnlyOutput({ fingerprint1: firstFingerprint, fingerprint2: secondFingerprint });
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;
}
if (firstFingerprint.hash === secondFingerprint.hash) {
Log.log(
`✅ ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} matches ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
);
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(
`🔄 ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} differs from ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
);
Log.log(`🔄 Project fingerprint differs from build`);
}
const fingerprintDiffs = diffFingerprint(projectDir, firstFingerprint, secondFingerprint);
const fingerprintDiffs = diffFingerprint(projectDir, fingerprint, projectFingerprint);
if (!fingerprintDiffs) {
Log.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher');
return;
@@ -248,390 +167,9 @@ export default class FingerprintCompare extends EasCommand {
for (const diff of contentDiffs) {
printContentDiff(diff);
}
if (nonInteractive) {
return;
}
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
const fingerprintCompareUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/compare/${firstFingerprintInfo.fingerprint.hash}/${secondFingerprintInfo.fingerprint.hash}`,
getExpoWebsiteBaseUrl()
);
if (!open) {
Log.newLine();
Log.withInfo(
`💡 Use the --open flag to view the comparison in the browser. ${learnMore(
fingerprintCompareUrl.toString()
)}`
);
return;
}
await openBrowserAsync(fingerprintCompareUrl.toString());
}
}
async function getFingerprintInfoAsync(
graphqlClient: ExpoGraphqlClient,
projectDir: string,
projectId: string,
vcsClient: Client,
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn,
{
buildId,
updateId,
hash,
useProjectFingerprint,
environmentForProjectFingerprint,
nonInteractive,
}: {
buildId?: string;
updateId?: string;
hash: string;
useProjectFingerprint?: boolean;
environmentForProjectFingerprint?: string;
nonInteractive: boolean;
},
firstFingerprintInfo?: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
}
): Promise<{ fingerprint: Fingerprint; origin: FingerprintOrigin }> {
if (hash) {
return await getFingerprintInfoFromHashAsync(graphqlClient, projectId, hash);
} else if (updateId) {
return await getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
graphqlClient,
projectId,
nonInteractive,
updateId
);
} else if (buildId) {
return await getFingerprintInfoFromBuildIdAsync(graphqlClient, buildId);
} else if (useProjectFingerprint) {
if (!firstFingerprintInfo) {
throw new Error(
'First fingerprint must be provided in order to compare against the project.'
);
}
return await getFingerprintInfoFromLocalProjectAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environment: environmentForProjectFingerprint,
});
}
if (nonInteractive) {
throw new Error(
'Insufficent arguments provided for fingerprint comparison in non-interactive mode'
);
}
return await getFingerprintInfoInteractiveAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environmentForProjectFingerprint,
});
}
async function getFingerprintInfoInteractiveAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environmentForProjectFingerprint,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
vcsClient: Client;
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn;
firstFingerprintInfo?: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
};
environmentForProjectFingerprint?: string;
}): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const prompt = firstFingerprintInfo
? 'Select the second fingerprint to compare against'
: 'Select a reference fingerprint for comparison';
const originType = await selectAsync<FingerprintOriginType>(prompt, [
...(firstFingerprintInfo
? [{ title: 'Current project fingerprint', value: FingerprintOriginType.Project }]
: []),
{ title: 'Build fingerprint', value: FingerprintOriginType.Build },
{ title: 'Update fingerprint', value: FingerprintOriginType.Update },
{ title: 'Enter a fingerprint hash manually', value: FingerprintOriginType.Hash },
]);
if (originType === FingerprintOriginType.Project) {
if (!firstFingerprintInfo) {
throw new Error(
'First fingerprint must be provided in order to compare against the project.'
);
}
return await getFingerprintInfoFromLocalProjectAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environment: environmentForProjectFingerprint,
});
} else if (originType === FingerprintOriginType.Build) {
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
throw new Error('Must select build with fingerprint for comparison.');
}
return await getFingerprintInfoFromBuildIdAsync(graphqlClient, buildId);
} else if (originType === FingerprintOriginType.Update) {
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'On which branch would you like search for an update?',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
paginatedQueryOptions: {
json: false,
nonInteractive: false,
offset: 0,
},
});
const selectedUpdateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName: selectedBranch.name,
paginatedQueryOptions: {
json: false,
nonInteractive: false,
offset: 0,
},
});
const updateGroupId = selectedUpdateGroup[0].group;
return await getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
graphqlClient,
projectId,
false,
updateGroupId
);
} else if (originType === FingerprintOriginType.Hash) {
const { hash } = await promptAsync({
type: 'text',
name: 'hash',
message: 'Provide the fingerprint hash',
validate: (value: string) => !!value.trim(),
hint: '0000000000000000000000000000000000000000',
});
return await getFingerprintInfoFromHashAsync(graphqlClient, projectId, hash);
} else {
throw new Error(`Unsupported fingerprint origin type: ${originType}`);
}
}
async function getFingerprintInfoFromLocalProjectAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environment,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
vcsClient: Client;
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn;
firstFingerprintInfo: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
};
environment?: string;
}): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const firstFingerprintPlatforms = firstFingerprintInfo.platforms;
if (!firstFingerprintPlatforms || firstFingerprintPlatforms.length === 0) {
throw new Error(
`Cannot compare the local directory against the provided fingerprint hash "${firstFingerprintInfo.fingerprint.hash}" because the associated platform could not be determined. Ensure the fingerprint is linked to a build or update to identify the platform.`
);
}
if (environment) {
Log.log(`🔧 Using environment: ${environment}`);
}
const env = environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: undefined;
const fingerprint = await getFingerprintInfoFromLocalProjectForPlatformsAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
firstFingerprintPlatforms,
{ env }
);
return { fingerprint, origin: { type: FingerprintOriginType.Project } };
}
async function getFingerprintFromUpdateFragmentAsync(
updateWithFingerprint: UpdateFragment
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
if (!updateWithFingerprint.fingerprint) {
throw new Error(`Fingerprint for update ${updateWithFingerprint.id} was not computed.`);
} else if (!updateWithFingerprint.fingerprint.debugInfoUrl) {
throw new Error(`Fingerprint source for update ${updateWithFingerprint.id} was not computed.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(
updateWithFingerprint.fingerprint
),
platforms: [stringToAppPlatform(updateWithFingerprint.platform)],
origin: {
type: FingerprintOriginType.Update,
update: updateWithFingerprint,
},
};
}
async function getFingerprintInfoFromHashAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
hash: string
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
graphqlClient,
projectId,
hash
);
const fingerprint = await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment);
let platforms;
const fingerprintBuilds = fingerprintFragment.builds?.edges.map(edge => edge.node) ?? [];
const fingerprintUpdates = fingerprintFragment.updates?.edges.map(edge => edge.node) ?? [];
if (fingerprintBuilds.length > 0) {
platforms = [fingerprintBuilds[0].platform];
} else if (fingerprintUpdates.length > 0) {
platforms = [stringToAppPlatform(fingerprintUpdates[0].platform)];
}
return {
fingerprint,
platforms,
origin: {
type: FingerprintOriginType.Hash,
},
};
}
async function getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
nonInteractive: boolean,
updateGroupIdOrUpdateId: string
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
// Some people may pass in update group id instead of update id, so add interactive support for that
try {
const maybeUpdateGroupId = updateGroupIdOrUpdateId;
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: maybeUpdateGroupId,
});
if (updateGroup.length === 1) {
const update = updateGroup[0];
return await getFingerprintFromUpdateFragmentAsync(update);
}
if (nonInteractive) {
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
const updateUrl =
getExpoWebsiteBaseUrl() +
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/updates/${maybeUpdateGroupId}`;
throw new Error(
`Please pass in your update ID from ${updateUrl} or use interactive mode to select the update ID.`
);
}
const update = await selectAsync<UpdateFragment>(
'Select a platform to compute the fingerprint from',
updateGroup.map(update => ({
title: update.platform,
value: update,
}))
);
return await getFingerprintFromUpdateFragmentAsync(update);
} catch (error: any) {
if (!error?.message.includes('Could not find any updates with group ID')) {
throw error;
}
}
const updateId = updateGroupIdOrUpdateId;
const updateWithFingerprint = await UpdateQuery.viewByUpdateAsync(graphqlClient, {
updateId,
});
return await getFingerprintFromUpdateFragmentAsync(updateWithFingerprint);
}
async function getFingerprintInfoFromBuildIdAsync(
graphqlClient: ExpoGraphqlClient,
buildId: string
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
if (!buildWithFingerprint.fingerprint) {
throw new Error(`Fingerprint for build ${buildId} was not computed.`);
} else if (!buildWithFingerprint.fingerprint.debugInfoUrl) {
throw new Error(`Fingerprint source for build ${buildId} was not computed.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(buildWithFingerprint.fingerprint),
platforms: [buildWithFingerprint.platform],
origin: {
type: FingerprintOriginType.Build,
build: buildWithFingerprint,
},
};
}
async function getFingerprintFragmentFromHashAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
hash: string
): Promise<FingerprintFragment> {
const fingerprint = await FingerprintQuery.byHashAsync(graphqlClient, {
appId: projectId,
hash,
});
if (!fingerprint) {
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
throw new Error(`Fingerprint with hash ${hash} was not uploaded for ${displayName}.`);
}
return fingerprint;
}
async function getFingerprintFromFingerprintFragmentAsync(
fingerprintFragment: Pick<FingerprintFragment, 'debugInfoUrl' | 'hash'>
): Promise<Fingerprint> {
const fingerprintDebugUrl = fingerprintFragment.debugInfoUrl;
if (!fingerprintDebugUrl) {
throw new Error(
`The source for fingerprint hash ${fingerprintFragment.hash} was not computed.`
);
}
const fingerprintResponse = await fetch(fingerprintDebugUrl);
return (await fingerprintResponse.json()) as Fingerprint;
}
function printContentDiff(diff: FingerprintDiffItem): void {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
@@ -737,10 +275,7 @@ const PRETTY_CONTENT_ID: Record<string, string> = {
'expoAutolinkingConfig:ios': 'Expo autolinking config (iOS)',
'expoAutolinkingConfig:android': 'Expo autolinking config (Android)',
'packageJson:scripts': 'package.json scripts',
expoConfig: 'Expo app config',
'package:react-native': 'React Native package.json',
'rncoreAutolinkingConfig:ios': 'React Native Community autolinking config (iOS)',
'rncoreAutolinkingConfig:android': 'React Native Community autolinking config (Android)',
expoConfig: 'Expo config',
};
function printContentSource({
@@ -784,23 +319,6 @@ function printContentsDiff(contents1: string | Buffer, contents2: string | Buffe
abridgedDiff(prettifiedContents1, prettifiedContents2, 0);
}
function prettyPrintFingerprint(fingerprint: Fingerprint, origin: FingerprintOrigin): string {
if (origin.type === FingerprintOriginType.Project) {
return `fingerprint ${fingerprint.hash} from local directory`;
} else if (origin.type === FingerprintOriginType.Update) {
return `fingerprint ${fingerprint.hash} from ${
origin.update?.platform ? stringToAppPlatform(origin.update?.platform) : ''
} ${origin.type}`;
} else if (origin.type === FingerprintOriginType.Build) {
return `fingerprint ${fingerprint.hash} from ${origin.build?.platform} ${origin.type}`;
}
return `fingerprint ${fingerprint.hash}`;
}
function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function isJSON(str: string): boolean {
try {
JSON.parse(str);
@@ -810,7 +328,29 @@ function isJSON(str: string): boolean {
}
}
async function selectBuildToCompareAsync(
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,
@@ -1,164 +0,0 @@
import { Env } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import { getExpoWebsiteBaseUrl } from '../../api';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EasEnvironmentFlagParameters,
EasNonInteractiveAndJsonFlags,
} from '../../commandUtils/flags';
import {
appPlatformToPlatform,
getFingerprintInfoFromLocalProjectForPlatformsAsync,
stringToAppPlatform,
} from '../../fingerprint/utils';
import { AppPlatform, EnvironmentVariableEnvironment } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { link } from '../../log';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getProfilesAsync } from '../../utils/profiles';
export default class FingerprintGenerate extends EasCommand {
static override description = 'generate fingerprints from the current project';
static override strict = false;
static override examples = [
'$ eas fingerprint:generate \t # Generate fingerprint in interactive mode',
'$ eas fingerprint:generate --build-profile preview \t # Generate a fingerprint using the "preview" build profile',
'$ eas fingerprint:generate --environment preview \t # Generate a fingerprint using the "preview" environment',
'$ eas fingerprint:generate --json --non-interactive --platform android \t # Output fingerprint json to stdout',
];
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
...EASEnvironmentFlag,
environment: Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
exclusive: ['build-profile'],
}),
'build-profile': Flags.string({
char: 'e',
description: 'Name of the build profile from eas.json.',
exclusive: ['environment'],
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
...this.ContextOptions.ServerSideEnvironmentVariables,
...this.ContextOptions.DynamicProjectConfig,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(FingerprintGenerate);
const {
json,
'non-interactive': nonInteractive,
platform: platformStringFlag,
environment,
'build-profile': buildProfileName,
} = flags;
const {
projectId,
privateProjectConfig: { projectDir },
loggedIn: { graphqlClient },
vcsClient,
getServerSideEnvironmentVariablesAsync,
getDynamicPrivateProjectConfigAsync,
} = await this.getContextAsync(FingerprintGenerate, {
nonInteractive,
withServerSideEnvironment: environment ?? null,
});
if (json) {
enableJsonOutput();
}
let platform: AppPlatform;
if (platformStringFlag) {
platform = stringToAppPlatform(platformStringFlag);
} else {
if (nonInteractive) {
throw new Error('Platform must be specified in non-interactive mode with the --p flag');
}
platform = await selectRequestedPlatformAsync();
}
let env: Env | undefined = undefined;
if (environment) {
Log.log(`🔧 Using environment: ${environment}`);
env = { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' };
} else if (buildProfileName) {
Log.log(`🔧 Using build profile: ${buildProfileName}`);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const buildProfile = (
await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [appPlatformToPlatform(platform)],
profileName: buildProfileName ?? undefined,
projectDir,
})
)[0];
if (!buildProfile) {
throw new Error(`Build profile ${buildProfile} not found for platform: ${platform}`);
}
const configResult = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
env = configResult.env;
}
const fingerprint = await getFingerprintInfoFromLocalProjectForPlatformsAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
[platform],
{ env }
);
if (json) {
printJsonOnlyOutput(fingerprint);
return;
}
Log.log(`✅ Fingerprint generated: ${fingerprint.hash}`);
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
const fingerprintUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprint.hash}`,
getExpoWebsiteBaseUrl()
);
Log.log(`🔍 View the fingerprint at ${link(fingerprintUrl.toString())}`);
Log.log(`💡 If you want to see the entire fingerprint output, pass in the --json flag.`);
}
}
async function selectRequestedPlatformAsync(): Promise<AppPlatform> {
const { requestedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'requestedPlatform',
choices: [
{ title: 'Android', value: AppPlatform.Android },
{ title: 'iOS', value: AppPlatform.Ios },
],
});
return requestedPlatform;
}
@@ -162,10 +162,7 @@ export default class Onboarding extends EasCommand {
cloneMethod,
});
const vcsClient = new GitClient({
maybeCwdOverride: finalTargetProjectDirectory,
requireCommit: false,
});
const vcsClient = new GitClient(finalTargetProjectDirectory);
if (!app.githubRepository) {
await fs.remove(path.join(finalTargetProjectDirectory, '.git'));
await runCommandAsync({
@@ -288,12 +285,12 @@ export default class Onboarding extends EasCommand {
} else {
Log.log('🚀 Now we are going to trigger your first build');
Log.log();
const { buildIds } = await runBuildAndSubmitAsync({
const { buildIds } = await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir: finalTargetProjectDirectory,
flags: {
finalTargetProjectDirectory,
{
nonInteractive: true,
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
@@ -310,9 +307,8 @@ export default class Onboarding extends EasCommand {
repack: true,
},
actor,
// eslint-disable-next-line async-protect/async-suffix
getDynamicPrivateProjectConfigAsync: getDynamicProjectConfigFn,
});
getDynamicProjectConfigFn
);
const buildId = buildIds[0];
Log.log();
Log.log('🚀 You can now go back to the website to continue:');
@@ -18,7 +18,8 @@ import AndroidSubmitCommand from '../../submit/android/AndroidSubmitCommand';
import { SubmissionContext, createSubmissionContextAsync } from '../../submit/context';
import IosSubmitCommand from '../../submit/ios/IosSubmitCommand';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import GitClient from '../../vcs/clients/git';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
import NoVcsClient from '../../vcs/clients/noVcs';
/**
* This command will be run on the EAS workers.
@@ -64,16 +65,10 @@ export default class SubmitInternal extends EasCommand {
vcsClient,
} = await this.getContextAsync(SubmitInternal, {
nonInteractive: true,
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
withServerSideEnvironment: null,
});
if (vcsClient instanceof GitClient) {
// `build:internal` is run on EAS workers and the repo may have been changed
// by pre-install hooks or other scripts. We don't want to require committing changes
// to continue the build.
vcsClient.requireCommit = false;
}
const submissionProfile = await EasJsonUtils.getSubmitProfileAsync(
EasJsonAccessor.fromProjectPath(projectDir),
flags.platform,
@@ -54,7 +54,6 @@ export default class UpdateConfigure extends EasCommand {
platform: flags['platform'],
vcsClient,
env: undefined,
forceNativeConfigSync: true,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
+10 -18
View File
@@ -6,43 +6,37 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
BackgroundJobReceiptDataFragment,
ScheduleUpdateGroupDeletionMutation,
ScheduleUpdateGroupDeletionMutationVariables,
DeleteUpdateGroupMutation,
UpdateMutationDeleteUpdateGroupArgs,
} from '../../graphql/generated';
import { BackgroundJobReceiptNode } from '../../graphql/types/BackgroundJobReceipt';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
async function scheduleUpdateGroupDeletionAsync(
async function deleteUpdateGroupAsync(
graphqlClient: ExpoGraphqlClient,
{
group,
}: {
group: string;
}
): Promise<BackgroundJobReceiptDataFragment> {
const result = await withErrorHandlingAsync(
): Promise<DeleteUpdateGroupMutation> {
return await withErrorHandlingAsync(
graphqlClient
.mutation<ScheduleUpdateGroupDeletionMutation, ScheduleUpdateGroupDeletionMutationVariables>(
.mutation<DeleteUpdateGroupMutation, UpdateMutationDeleteUpdateGroupArgs>(
gql`
mutation ScheduleUpdateGroupDeletion($group: ID!) {
mutation DeleteUpdateGroup($group: ID!) {
update {
scheduleUpdateGroupDeletion(group: $group) {
id
...BackgroundJobReceiptData
deleteUpdateGroup(group: $group) {
group
}
}
}
${BackgroundJobReceiptNode}
`,
{ group }
)
.toPromise()
);
return result.update.scheduleUpdateGroupDeletion;
}
export default class UpdateDelete extends EasCommand {
@@ -96,9 +90,7 @@ export default class UpdateDelete extends EasCommand {
}
}
const receipt = await scheduleUpdateGroupDeletionAsync(graphqlClient, { group });
const successfulReceipt = await pollForBackgroundJobReceiptAsync(graphqlClient, receipt);
Log.debug('Deletion result', { successfulReceipt });
await deleteUpdateGroupAsync(graphqlClient, { group });
if (jsonFlag) {
printJsonOnlyOutput({ group });
+87 -150
View File
@@ -3,7 +3,6 @@ import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { getExpoWebsiteBaseUrl } from '../../api';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { transformFingerprintSource } from '../../build/graphql';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
@@ -13,9 +12,8 @@ import { EasNonInteractiveAndJsonFlags, EasUpdateEnvironmentFlag } from '../../c
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
AssetMapSourceInput,
EnvironmentVariableEnvironment,
FingerprintInfoGroup as GraphqlFingerprintInfoGroup,
FingerprintInfoGroup,
PublishUpdateGroupInput,
StatuspageServiceName,
UpdateInfoGroup,
@@ -23,11 +21,9 @@ import {
UpdateRolloutInfoGroup,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { maybeUploadAssetMapAsync } from '../../project/maybeUploadAssetMapAsync';
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
@@ -37,7 +33,6 @@ import {
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
filterCollectedAssetsByRequestedPlatforms,
findCompatibleBuildsAsync,
generateEasMetadataAsync,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
@@ -242,10 +237,6 @@ export default class UpdatePublish extends EasCommand {
jsonFlag,
});
const maybeServerEnv = environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: {};
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
@@ -256,7 +247,11 @@ export default class UpdatePublish extends EasCommand {
exp,
platformFlag: requestedPlatform,
clearCache,
extraEnv: maybeServerEnv,
extraEnv: {
...(environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: {}),
},
});
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
@@ -273,14 +268,9 @@ export default class UpdatePublish extends EasCommand {
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
let realizedPlatforms: UpdatePublishPlatform[] = [];
let assetMapSource: AssetMapSourceInput | null = null;
try {
const [collectedAssets, maybeAssetMapSource] = await Promise.all([
collectAssetsAsync(distRoot),
maybeUploadAssetMapAsync(distRoot, graphqlClient),
]);
assetMapSource = maybeAssetMapSource;
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterCollectedAssetsByRequestedPlatforms(collectedAssets, requestedPlatform);
realizedPlatforms = Object.keys(assets) as UpdatePublishPlatform[];
@@ -403,7 +393,7 @@ export default class UpdatePublish extends EasCommand {
...workflows,
web: Workflow.UNKNOWN,
},
env: maybeServerEnv,
env: undefined,
});
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
@@ -488,19 +478,14 @@ export default class UpdatePublish extends EasCommand {
},
};
},
{} as GraphqlFingerprintInfoGroup
{} as FingerprintInfoGroup
);
const assetMapGroup = assetMapSource
? Object.fromEntries(platforms.map(platform => [platform, assetMapSource]))
: null;
return {
branchId: branch.id,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
fingerprintInfoGroup: transformedFingerprintInfoGroup,
assetMapGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
@@ -574,143 +559,95 @@ export default class UpdatePublish extends EasCommand {
}
if (jsonFlag) {
printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
return;
}
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
const runtimeToCompatibleBuilds = await Promise.all(
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
findCompatibleBuildsAsync(graphqlClient, projectId, obj)
)
);
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate ? [{ label: 'Android update ID', value: newAndroidUpdate.id }] : []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
...(newAndroidUpdate?.rolloutControlUpdate
? [
{
label: 'Android Rollout',
value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`,
},
]
: []),
...(newIosUpdate?.rolloutControlUpdate
? [
{
label: 'iOS Rollout',
value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`,
},
]
: []),
{ label: 'Message', value: updateMessage ?? '' },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
if (isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)) {
Log.warn(
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
learnMoreMessage: 'Consider optimizing your usage of assets',
dim: false,
})}.`
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
...(newAndroidUpdate?.rolloutControlUpdate
? [
{
label: 'Android Rollout',
value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`,
},
]
: []),
...(newIosUpdate?.rolloutControlUpdate
? [
{
label: 'iOS Rollout',
value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`,
},
]
: []),
{ label: 'Message', value: updateMessage ?? '' },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
}
const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
)?.fingerprintInfoGroupWithCompatibleBuilds;
if (fingerprintsWithoutCompatibleBuilds) {
const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
([_platform, fingerprintInfo]) => !fingerprintInfo.build
);
if (missingBuilds.length > 0) {
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
Log.warn('No compatible builds found for the following fingerprints:');
for (const [platform, fingerprintInfo] of missingBuilds) {
const fingerprintUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
getExpoWebsiteBaseUrl()
);
Log.warn(
formatFields(
[
{
label: `${this.prettyPlatform(platform)} fingerprint`,
value: fingerprintInfo.fingerprintHash,
},
{ label: 'URL', value: fingerprintUrl.toString() },
],
{
labelFormat: label => ` ${chalk.dim(label)}:`,
}
)
);
Log.addNewLineIfNone();
}
if (
isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)
) {
Log.warn(
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
learnMoreMessage: 'Consider optimizing your usage of assets',
dim: false,
})}.`
);
Log.addNewLineIfNone();
}
}
}
}
private prettyPlatform(updatePlatform: string): string {
switch (updatePlatform) {
case 'android':
return 'Android';
case 'ios':
return 'iOS';
default:
return updatePlatform;
}
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
-465
View File
@@ -1,465 +0,0 @@
import { IOSConfig } from '@expo/config-plugins';
import { Platform } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import fg from 'fast-glob';
import fs from 'fs-extra';
import StreamZip from 'node-stream-zip';
import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import { getBuildLogsUrl } from '../build/utils/url';
import EasCommand from '../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import {
BuildMetadataInput,
DistributionType,
LocalBuildArchiveSourceType,
UploadSessionType,
} from '../graphql/generated';
import { FingerprintMutation } from '../graphql/mutations/FingerprintMutation';
import { LocalBuildMutation } from '../graphql/mutations/LocalBuildMutation';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { promptAsync } from '../prompts';
import * as xcode from '../run/ios/xcode';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { fromNow } from '../utils/date';
import { enableJsonOutput, printJsonOnlyOutput } from '../utils/json';
import { getTmpDirectory } from '../utils/paths';
import { parseBinaryPlistBuffer } from '../utils/plist';
import { createProgressTracker } from '../utils/progress';
export default class BuildUpload extends EasCommand {
static override description = 'upload a local build and generate a sharable link';
static override hidden = true;
static override flags = {
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
'build-path': Flags.string({
description: 'Path for the local build',
}),
fingerprint: Flags.string({
description: 'Fingerprint hash of the local build',
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildUpload);
const {
'build-path': buildPath,
fingerprint: manualFingerprintHash,
json: jsonFlag,
'non-interactive': nonInteractive,
} = flags;
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildUpload, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const platform = await this.selectPlatformAsync({ platform: flags.platform, nonInteractive });
const localBuildPath = await resolveLocalBuildPathAsync({
platform,
inputBuildPath: buildPath,
nonInteractive,
});
const {
fingerprintHash: buildFingerprintHash,
developmentClient,
simulator,
...otherMetadata
} = await extractAppMetadataAsync(localBuildPath, platform);
let fingerprint = manualFingerprintHash ?? buildFingerprintHash;
if (fingerprint) {
if (
manualFingerprintHash &&
buildFingerprintHash &&
manualFingerprintHash !== buildFingerprintHash &&
!nonInteractive
) {
const selectedAnswer = await promptAsync({
name: 'fingerprint',
message: `The provided fingerprint hash ${manualFingerprintHash} does not match the fingerprint hash of the build ${buildFingerprintHash}. Which fingerprint do you want to use?`,
type: 'select',
choices: [
{ title: manualFingerprintHash, value: manualFingerprintHash },
{ title: buildFingerprintHash, value: buildFingerprintHash },
],
});
fingerprint = String(selectedAnswer.fingerprint);
}
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
hash: fingerprint,
});
}
Log.log(`Using build ${localBuildPath}`);
Log.log(`Fingerprint hash: ${fingerprint ?? 'Unknown'}`);
Log.log('Uploading your app archive to EAS');
const bucketKey = await uploadAppArchiveAsync(graphqlClient, localBuildPath);
const build = await LocalBuildMutation.createLocalBuildAsync(
graphqlClient,
projectId,
{ platform: toAppPlatform(platform), simulator },
{ type: LocalBuildArchiveSourceType.Gcs, bucketKey },
{
distribution: DistributionType.Internal,
fingerprintHash: fingerprint,
developmentClient,
...otherMetadata,
}
);
if (jsonFlag) {
printJsonOnlyOutput({ url: getBuildLogsUrl(build) });
return;
}
Log.withTick(`Shareable link to the build: ${getBuildLogsUrl(build)}`);
}
private async selectPlatformAsync({
nonInteractive,
platform,
}: {
nonInteractive: boolean;
platform?: Platform;
}): Promise<Platform> {
if (nonInteractive && !platform) {
throw new Error('Platform must be provided in non-interactive mode');
}
if (platform) {
return platform;
}
const { resolvedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'resolvedPlatform',
choices: [
{ title: 'Android', value: Platform.ANDROID },
{ title: 'iOS', value: Platform.IOS },
],
});
return resolvedPlatform;
}
}
async function resolveLocalBuildPathAsync({
platform,
inputBuildPath,
nonInteractive,
}: {
platform: Platform;
inputBuildPath?: string;
nonInteractive: boolean;
}): Promise<string> {
const rootDir = process.cwd();
let applicationArchivePatternOrPath: string[] = [];
if (inputBuildPath) {
applicationArchivePatternOrPath.push(inputBuildPath);
} else if (platform === Platform.ANDROID) {
applicationArchivePatternOrPath.push('android/app/build/outputs/**/*.{apk,aab}');
} else {
const xcworkspacePath = await xcode.resolveXcodeProjectAsync(rootDir);
const schemes = IOSConfig.BuildScheme.getRunnableSchemesFromXcodeproj(rootDir);
if (xcworkspacePath && schemes.length > 0) {
for (const scheme of schemes) {
const buildSettings = await xcode.getXcodeBuildSettingsAsync(xcworkspacePath, scheme.name);
applicationArchivePatternOrPath = applicationArchivePatternOrPath.concat(
buildSettings.map(({ buildSettings }) => `${buildSettings.BUILD_DIR}/**/*.app`)
);
}
}
}
let applicationArchives = await findArtifactsAsync({
rootDir,
patternOrPathArray: applicationArchivePatternOrPath,
});
if (applicationArchives.length === 0 && !nonInteractive && !inputBuildPath) {
Log.warn(`No application archives found at ${applicationArchivePatternOrPath}.`);
const { path } = await promptAsync({
type: 'text',
name: 'path',
message: 'Provide a path to the application archive:',
validate: value => (value ? true : 'Path may not be empty.'),
});
applicationArchives = await findArtifactsAsync({
rootDir,
patternOrPathArray: [path],
});
}
if (applicationArchives.length === 1) {
return applicationArchives[0];
}
if (applicationArchives.length > 1) {
// sort by modification time
const applicationArchivesInfo = await Promise.all(
applicationArchives.map(async archivePath => ({
path: archivePath,
stat: await fs.stat(archivePath),
}))
);
applicationArchivesInfo.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
if (nonInteractive) {
return applicationArchivesInfo[0].path;
}
const { selectedPath } = await promptAsync({
type: 'select',
name: 'selectedPath',
message: 'Found multiple application archives. Select one:',
choices: applicationArchivesInfo.map(archive => {
return {
title: `${
archive.path.startsWith(rootDir) ? path.relative(rootDir, archive.path) : archive.path
} (${fromNow(archive.stat.mtime)} ago)`,
value: archive.path,
};
}),
});
return selectedPath;
}
throw new Error(`Found no application archives at ${inputBuildPath}.`);
}
async function findArtifactsAsync({
rootDir,
patternOrPathArray,
}: {
rootDir: string;
patternOrPathArray: string[];
}): Promise<string[]> {
const files = new Set<string>();
for (const patternOrPath of patternOrPathArray) {
if (path.isAbsolute(patternOrPath) && (await fs.pathExists(patternOrPath))) {
files.add(patternOrPath);
} else {
const filesFound = await fg(patternOrPath, {
cwd: rootDir,
onlyFiles: false,
});
filesFound.forEach(file => files.add(file));
}
}
return [...files].map(filePath => {
// User may provide an absolute path as input in which case
// fg will return an absolute path.
if (path.isAbsolute(filePath)) {
return filePath;
}
// User may also provide a relative path in which case
// fg will return a path relative to rootDir.
return path.join(rootDir, filePath);
});
}
async function uploadAppArchiveAsync(
graphqlClient: ExpoGraphqlClient,
originalPath: string
): Promise<string> {
let filePath = originalPath;
if ((await fs.stat(filePath)).isDirectory()) {
await fs.mkdirp(getTmpDirectory());
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
const parentPath = path.dirname(originalPath);
const folderName = path.basename(originalPath);
await tar.create({ cwd: parentPath, file: tarPath, gzip: true }, [folderName]);
filePath = tarPath;
}
const fileSize = (await fs.stat(filePath)).size;
const bucketKey = await uploadFileAtPathToGCSAsync(
graphqlClient,
UploadSessionType.EasShareGcsAppArchive,
filePath,
createProgressTracker({
total: fileSize,
message: 'Uploading to EAS',
completedMessage: 'Uploaded to EAS',
})
);
return bucketKey;
}
function getInfoPlistMetadata(infoPlist: any): {
appName?: string;
appIdentifier?: string;
simulator: boolean;
} {
const appName = infoPlist?.CFBundleDisplayName ?? infoPlist?.CFBundleName;
const appIdentifier = infoPlist?.CFBundleIdentifier;
const simulator = infoPlist?.DTPlatformName?.includes('simulator');
return {
appName,
appIdentifier,
simulator,
};
}
async function extractAppMetadataAsync(
buildPath: string,
platform: Platform
): Promise<{ developmentClient: boolean; simulator: boolean } & BuildMetadataInput> {
let developmentClient = false;
let fingerprintHash: string | undefined;
// By default, we assume the iOS apps are for simulators
let simulator = platform === Platform.IOS;
let appName: string | undefined;
let appIdentifier: string | undefined;
const basePath = platform === Platform.ANDROID ? 'assets/' : buildPath;
const fingerprintFilePath =
platform === Platform.ANDROID ? 'fingerprint' : 'EXUpdates.bundle/fingerprint';
const devMenuBundlePath =
platform === Platform.ANDROID ? 'EXDevMenuApp.android.js' : 'EXDevMenu.bundle/';
const buildExtension = path.extname(buildPath);
if (['.apk', '.aab'].includes(buildExtension)) {
const zip = new StreamZip.async({ file: buildPath });
try {
developmentClient = Boolean(await zip.entry(path.join(basePath, devMenuBundlePath)));
if (await zip.entry(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = (await zip.entryData(path.join(basePath, fingerprintFilePath))).toString(
'utf-8'
);
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
} finally {
await zip.close();
}
} else if (buildExtension === '.app') {
developmentClient = await fs.exists(path.join(basePath, devMenuBundlePath));
if (await fs.exists(path.join(basePath, 'Info.plist'))) {
const infoPlistBuffer = await fs.readFile(path.join(basePath, 'Info.plist'));
const infoPlist = parseBinaryPlistBuffer(infoPlistBuffer);
({ simulator, appIdentifier, appName } = getInfoPlistMetadata(infoPlist));
}
if (await fs.exists(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = await fs.readFile(path.join(basePath, fingerprintFilePath), 'utf8');
}
} else if (buildExtension === '.ipa') {
const zip = new StreamZip.async({ file: buildPath });
try {
const entries = await zip.entries();
const entriesKeys = Object.keys(entries);
await Promise.all(
entriesKeys.map(async path => {
const infoPlistRegex = /^Payload\/[^/]+\.app\/Info\.plist$/;
if (infoPlistRegex.test(path)) {
const infoPlistBuffer = await zip.entryData(entries[path]);
const infoPlist = parseBinaryPlistBuffer(infoPlistBuffer);
({ simulator, appIdentifier, appName } = getInfoPlistMetadata(infoPlist));
return;
}
if (path.includes('/EXDevMenu.bundle')) {
developmentClient = true;
return;
}
if (path.includes('EXUpdates.bundle/fingerprint')) {
fingerprintHash = (await zip.entryData(entries[path])).toString('utf-8');
}
})
);
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
} finally {
await zip.close();
}
} else {
// Use tar to list files in the archive
try {
let fingerprintHashPromise: Promise<string> | undefined;
let infoPlistPromise: Promise<Buffer> | undefined;
await tar.list({
file: buildPath,
// eslint-disable-next-line async-protect/async-suffix
onentry: entry => {
if (entry.path.endsWith(devMenuBundlePath)) {
developmentClient = true;
}
if (entry.path.endsWith(fingerprintFilePath)) {
fingerprintHashPromise = new Promise<string>(async (resolve, reject) => {
try {
let content = '';
for await (const chunk of entry) {
content += chunk.toString('utf8');
}
resolve(content);
} catch (error) {
reject(error);
}
});
}
if (entry.path.endsWith('Info.plist')) {
infoPlistPromise = new Promise<Buffer>(async (resolve, reject) => {
try {
const chunks: Buffer[] = [];
for await (const chunk of entry) {
chunks.push(chunk);
}
const content = Buffer.concat(chunks);
resolve(content);
} catch (error) {
reject(error);
}
});
}
},
});
if (fingerprintHashPromise !== undefined) {
fingerprintHash = await fingerprintHashPromise;
}
if (infoPlistPromise !== undefined) {
const infoPlist = parseBinaryPlistBuffer(await infoPlistPromise);
({ simulator, appIdentifier, appName } = getInfoPlistMetadata(infoPlist));
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
}
}
return {
developmentClient,
fingerprintHash,
simulator,
appName,
appIdentifier,
};
}
+25 -136
View File
@@ -1,48 +1,25 @@
import { Flags } from '@oclif/core';
import { CombinedError } from '@urql/core';
import * as path from 'node:path';
import { getWorkflowRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
import {
WorkflowProjectSourceType,
WorkflowRunByIdQuery,
WorkflowRunStatus,
} from '../../graphql/generated';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WorkflowProjectSourceType } from '../../graphql/generated';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { sleepAsync } from '../../utils/promise';
import { WorkflowFile } from '../../utils/workflowFile';
const EXIT_CODES = {
WORKFLOW_FAILED: 11,
WORKFLOW_CANCELED: 12,
WAIT_ABORTED: 13,
};
export default class WorkflowRun extends EasCommand {
static override description = 'run an EAS workflow';
static override description = 'Run an EAS workflow';
static override args = [{ name: 'file', description: 'Path to the workflow file to run' }];
static override flags = {
...EASNonInteractiveFlag,
wait: Flags.boolean({
default: false,
allowNo: true,
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
summary: 'Wait for workflow run to complete',
}),
...EasJsonOnlyFlag,
};
static override contextDefinition = {
@@ -55,10 +32,6 @@ export default class WorkflowRun extends EasCommand {
async runAsync(): Promise<void> {
const { flags, args } = await this.parse(WorkflowRun);
if (flags.json) {
enableJsonOutput();
}
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -134,120 +107,36 @@ export default class WorkflowRun extends EasCommand {
throw err;
}
let workflowRunId: string;
try {
({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, {
appId: projectId,
workflowRevisionInput: {
fileName: path.basename(args.file),
yamlConfig,
},
workflowRunInput: {
projectSource: {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
const { id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(
graphqlClient,
{
appId: projectId,
workflowRevisionInput: {
fileName: path.basename(args.file),
yamlConfig,
},
},
}));
workflowRunInput: {
projectSource: {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
},
},
}
);
Log.newLine();
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
Log.succeed(
`Workflow started successfully. ${link(
getWorkflowRunUrl(account.name, projectName, workflowRunId)
)}`
);
} catch (err) {
Log.error('Failed to start the workflow with the API.');
throw err;
}
if (!flags.wait) {
Log.succeed('Workflow run started successfully.');
if (flags.json) {
printJsonOnlyOutput({
id: workflowRunId,
url: getWorkflowRunUrl(account.name, projectName, workflowRunId),
});
}
process.exit(0);
}
Log.newLine();
const { status } = await waitForWorkflowRunToEndAsync(graphqlClient, {
workflowRunId,
});
if (flags.json) {
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
printJsonOnlyOutput({
...workflowRun,
url: getWorkflowRunUrl(account.name, projectName, workflowRunId),
});
}
if (status === WorkflowRunStatus.Failure) {
process.exit(EXIT_CODES.WORKFLOW_FAILED);
} else if (status === WorkflowRunStatus.Canceled) {
process.exit(EXIT_CODES.WORKFLOW_CANCELED);
}
}
}
async function waitForWorkflowRunToEndAsync(
graphqlClient: ExpoGraphqlClient,
{ workflowRunId }: { workflowRunId: string }
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
const spinner = ora('Currently waiting for workflow run to start.').start();
let failedFetchesCount = 0;
while (true) {
try {
const workflowRun = await WorkflowRunQuery.byIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
failedFetchesCount = 0;
switch (workflowRun.status) {
case WorkflowRunStatus.InProgress:
spinner.start('Workflow run is in progress.');
break;
case WorkflowRunStatus.ActionRequired:
spinner.warn('Workflow run is waiting for action.');
break;
case WorkflowRunStatus.PendingCancel:
case WorkflowRunStatus.Canceled:
spinner.warn('Workflow run has been canceled.');
return workflowRun;
case WorkflowRunStatus.Failure:
spinner.fail('Workflow run has failed.');
return workflowRun;
case WorkflowRunStatus.Success:
spinner.succeed('Workflow run completed successfully.');
return workflowRun;
}
} catch {
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
failedFetchesCount += 1;
if (failedFetchesCount > 6) {
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
process.exit(EXIT_CODES.WAIT_ABORTED);
}
}
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
}
}
@@ -317,7 +317,7 @@ export async function createAppAsync(
}
}
export function isAppleError(error: any): error is {
function isAppleError(error: any): error is {
data: {
errors: {
id: string;
@@ -1,222 +0,0 @@
import { App, BetaGroup, Session, User, UserRole } from '@expo/apple-utils';
import { isAppleError } from './ensureAppExists';
import Log from '../../../log';
import { ora } from '../../../ora';
import { confirmAsync } from '../../../prompts';
// The name of the internal TestFlight group, this should probably never change.
const AUTO_GROUP_NAME = 'Team (Expo)';
/**
* Ensure a TestFlight internal group with access to all builds exists for the app and has all admin users invited to it.
* This allows users to instantly access their builds from TestFlight after it finishes processing.
*/
export async function ensureTestFlightGroupExistsAsync(app: App): Promise<void> {
if (process.env.EAS_NO_AUTO_TESTFLIGHT_SETUP) {
Log.debug('EAS_NO_AUTO_TESTFLIGHT_SETUP is set, skipping TestFlight setup');
return;
}
const groups = await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
});
if (groups.length > 0) {
Log.debug(`Found ${groups.length} TestFlight groups`);
Log.debug('Skipping creating a new TestFlight group');
return;
}
const group = await ensureInternalGroupAsync({
app,
groups,
});
const users = await User.getAsync(app.context);
const admins = users.filter(user => user.attributes.roles?.includes(UserRole.ADMIN));
await addAllUsersToInternalGroupAsync(group, admins);
}
async function ensureInternalGroupAsync({
groups,
app,
}: {
groups: BetaGroup[];
app: App;
}): Promise<BetaGroup> {
let betaGroup = groups.find(group => group.attributes.name === AUTO_GROUP_NAME);
if (!betaGroup) {
const spinner = ora().start('Creating TestFlight group...');
try {
// Apple throw an error if you create the group too quickly after creating the app. We'll retry a few times.
await pollRetryAsync(
async () => {
betaGroup = await app.createBetaGroupAsync({
name: AUTO_GROUP_NAME,
isInternalGroup: true,
// Automatically add latest builds to the group without needing to run the command.
hasAccessToAllBuilds: true,
});
},
{
shouldRetry(error) {
if (isAppleError(error)) {
spinner.text = `TestFlight still preparing, retrying in 10 seconds...`;
return error.data.errors.some(
error => error.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID'
);
}
return false;
},
}
);
spinner.succeed(`TestFlight group created: ${AUTO_GROUP_NAME}`);
} catch (error: any) {
spinner.fail('Failed to create TestFlight group...');
throw error;
}
}
if (!betaGroup) {
throw new Error('Failed to create internal TestFlight group');
}
// `hasAccessToAllBuilds` is a newer feature that allows the group to automatically have access to all builds. This cannot be patched so we need to recreate the group.
if (!betaGroup.attributes.hasAccessToAllBuilds) {
if (
await confirmAsync({
message: 'Regenerate internal TestFlight group to allow automatic access to all builds?',
})
) {
await BetaGroup.deleteAsync(app.context, { id: betaGroup.id });
return await ensureInternalGroupAsync({
app,
groups: await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
}),
});
}
}
return betaGroup;
}
async function addAllUsersToInternalGroupAsync(group: BetaGroup, users: User[]): Promise<void> {
let emails = users
.filter(user => user.attributes.email)
.map(user => ({
email: user.attributes.email!,
firstName: user.attributes.firstName ?? '',
lastName: user.attributes.lastName ?? '',
}));
const { betaTesters } = group.attributes;
const existingEmails = betaTesters?.map(tester => tester.attributes.email).filter(Boolean) ?? [];
// Filter out existing beta testers.
if (betaTesters) {
emails = emails.filter(
user => !existingEmails.find(existingEmail => existingEmail === user.email)
);
}
// No new users to add to the internal group.
if (!emails.length) {
// No need to log which users are here on subsequent runs as devs already know the drill at this point.
Log.debug(`All current admins are already added to the group: ${group.attributes.name}`);
return;
}
Log.debug(`Adding ${emails.length} users to internal group: ${group.attributes.name}`);
Log.debug(`Users: ${emails.map(user => user.email).join(', ')}`);
const data = await group.createBulkBetaTesterAssignmentsAsync(emails);
const success = data.attributes.betaTesters.every(tester => {
if (tester.assignmentResult === 'FAILED') {
if (tester.errors && Array.isArray(tester.errors) && tester.errors.length) {
if (
tester.errors.length === 1 &&
tester.errors[0].key === 'Halliday.tester.already.exists'
) {
return true;
}
for (const error of tester.errors) {
Log.error(
`Error adding user ${tester.email} to TestFlight group "${group.attributes.name}": ${error.key}`
);
}
}
return false;
}
if (tester.assignmentResult === 'NOT_QUALIFIED_FOR_INTERNAL_GROUP') {
return false;
}
return true;
});
if (!success) {
const groupUrl = await getTestFlightGroupUrlAsync(group);
Log.error(
`Unable to add all admins to TestFlight group "${
group.attributes.name
}". You can add them manually in App Store Connect. ${groupUrl ?? ''}`
);
} else {
Log.log(
`TestFlight access enabled for: ` +
data.attributes.betaTesters
.map(tester => tester.email)
.filter(Boolean)
.join(', ')
);
// TODO: When we have more TestFlight functionality, we can link to it from here.
}
}
async function getTestFlightGroupUrlAsync(group: BetaGroup): Promise<string | null> {
if (group.context.providerId) {
try {
const session = await Session.getSessionForProviderIdAsync(group.context.providerId);
return `https://appstoreconnect.apple.com/teams/${session.provider.publicProviderId}/apps/6741088859/testflight/groups/${group.id}`;
} catch (error) {
// Avoid crashing if we can't get the session.
Log.debug('Failed to get session for provider ID', error);
}
}
return null;
}
async function pollRetryAsync<T>(
fn: () => Promise<T>,
{
shouldRetry,
retries = 15,
// 25 seconds was the minium interval I calculated when measuring against 5 second intervals.
// Switching to 10 seconds to account for days where Apple APIs are faster.
interval = 10000,
}: { shouldRetry?: (error: Error) => boolean; retries?: number; interval?: number } = {}
): Promise<T> {
let lastError: Error | null = null;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error: any) {
if (shouldRetry && !shouldRetry(error)) {
throw error;
}
lastError = error;
await new Promise(resolve => setTimeout(resolve, interval));
}
}
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw lastError;
}
@@ -29,8 +29,8 @@ function resolveProfileType(
return resolveProfileTypeIos(profileClass, isEnterprise);
case ApplePlatform.TV_OS:
return resolveProfileTypeAppleTv(profileClass, isEnterprise);
case ApplePlatform.VISION_OS:
case ApplePlatform.MAC_OS:
case ApplePlatform.VISION_OS:
throw new Error(`${applePlatform} profiles are not supported`);
}
}
-112
View File
@@ -1,112 +0,0 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { createFingerprintAsync } from './cli';
import { Fingerprint } from './types';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform } from '../graphql/generated';
import { FingerprintMutation } from '../graphql/mutations/FingerprintMutation';
import Log from '../log';
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
import { Client } from '../vcs/vcs';
export async function getFingerprintInfoFromLocalProjectForPlatformsAsync(
graphqlClient: ExpoGraphqlClient,
projectDir: string,
projectId: string,
vcsClient: Client,
platforms: AppPlatform[],
{ env }: { env?: Record<string, string> } = {}
): Promise<Fingerprint> {
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const optionsFromWorkflow = getFingerprintOptionsFromWorkflow(platforms, workflows);
const projectFingerprint = await createFingerprintAsync(projectDir, {
...optionsFromWorkflow,
platforms: platforms.map(appPlatformToString),
debug: true,
env,
});
if (!projectFingerprint) {
throw new Error('Project fingerprints can only be computed for projects with SDK 52 or higher');
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: projectFingerprint.hash,
fingerprint: {
fingerprintSources: projectFingerprint.sources,
isDebugFingerprintSource: Log.isDebug,
},
graphqlClient,
});
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
hash: uploadedFingerprint.hash,
source: uploadedFingerprint.fingerprintSource,
});
return projectFingerprint;
}
function getFingerprintOptionsFromWorkflow(
platforms: AppPlatform[],
workflowsByPlatform: Record<Platform, Workflow>
): { workflow?: Workflow; ignorePaths?: string[] } {
if (platforms.length === 0) {
throw new Error('Could not determine platform from fingerprint sources');
}
// Single platform case
if (platforms.length === 1) {
const platform = platforms[0];
return { workflow: workflowsByPlatform[appPlatformToPlatform(platform)] };
}
// Multiple platforms case
const workflows = platforms.map(platform => workflowsByPlatform[appPlatformToPlatform(platform)]);
// If all workflows are the same, return the common workflow
const [firstWorkflow, ...restWorkflows] = workflows;
if (restWorkflows.every(workflow => workflow === firstWorkflow)) {
return { workflow: firstWorkflow };
}
// Generate ignorePaths for mixed workflows
const ignorePaths = platforms
.filter(platform => workflowsByPlatform[appPlatformToPlatform(platform)] === Workflow.MANAGED)
.map(platform => `${appPlatformToString(platform)}/**/*`);
return { ignorePaths };
}
export 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}`);
}
}
export 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 function stringToAppPlatform(platform: string): AppPlatform {
switch (platform) {
case 'android':
return AppPlatform.Android;
case 'ios':
return AppPlatform.Ios;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
File diff suppressed because it is too large. Load diff
@@ -1,55 +0,0 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
BuildFragment,
BuildMetadataInput,
CreateLocalBuildMutation,
LocalBuildArchiveSourceInput,
LocalBuildJobInput,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
export const LocalBuildMutation = {
async createLocalBuildAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
job: LocalBuildJobInput,
artifactSource: LocalBuildArchiveSourceInput,
metadata: BuildMetadataInput
): Promise<BuildFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateLocalBuildMutation>(
gql`
mutation createLocalBuildMutation(
$appId: ID!
$jobInput: LocalBuildJobInput!
$artifactSource: LocalBuildArchiveSourceInput!
$metadata: BuildMetadataInput
) {
build {
createLocalBuild(
appId: $appId
job: $jobInput
artifactSource: $artifactSource
metadata: $metadata
) {
build {
id
...BuildFragment
}
}
}
}
${print(BuildFragmentNode)}
`,
{ appId, jobInput: job, artifactSource, metadata }
)
.toPromise()
);
return data.build.createLocalBuild.build;
},
};
@@ -20,22 +20,20 @@ export interface SignedUrl {
export const UploadSessionMutation = {
async createUploadSessionAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType,
filename?: string
type: UploadSessionType
): Promise<SignedUrl> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
gql`
mutation CreateUploadSessionMutation($type: UploadSessionType!, $filename: String) {
mutation CreateUploadSessionMutation($type: UploadSessionType!) {
uploadSession {
createUploadSession(type: $type, filename: $filename)
createUploadSession(type: $type)
}
}
`,
{
type,
filename,
}
)
.toPromise()
@@ -1,40 +0,0 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
BackgroundJobReceiptByIdQuery,
BackgroundJobReceiptByIdQueryVariables,
} from '../generated';
import { BackgroundJobReceiptNode } from '../types/BackgroundJobReceipt';
export const BackgroundJobReceiptQuery = {
async byIdAsync(
graphqlClient: ExpoGraphqlClient,
backgroundJobReceiptId: string
): Promise<BackgroundJobReceiptByIdQuery['backgroundJobReceipt']['byId'] | null> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BackgroundJobReceiptByIdQuery, BackgroundJobReceiptByIdQueryVariables>(
gql`
query BackgroundJobReceiptById($id: ID!) {
backgroundJobReceipt {
byId(id: $id) {
id
...BackgroundJobReceiptData
}
}
}
${BackgroundJobReceiptNode}
`,
{ id: backgroundJobReceiptId },
{
additionalTypenames: ['BackgroundJobReceipt'],
}
)
.toPromise()
);
return data.backgroundJobReceipt.byId ?? null;
},
};
@@ -7,8 +7,6 @@ import { withErrorHandlingAsync } from '../client';
import {
UpdateBranchBasicInfoFragment,
UpdateFragment,
ViewUpdateChannelBasicInfoOnAppQuery,
ViewUpdateChannelBasicInfoOnAppQueryVariables,
ViewUpdateChannelOnAppQuery,
ViewUpdateChannelOnAppQueryVariables,
ViewUpdateChannelsOnAppQuery,
@@ -43,42 +41,6 @@ export function composeUpdateBranchObject(
}
export const ChannelQuery = {
async viewUpdateChannelBasicInfoAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName }: ViewUpdateChannelBasicInfoOnAppQueryVariables
): Promise<
NonNullable<ViewUpdateChannelBasicInfoOnAppQuery['app']['byId']['updateChannelByName']>
> {
const response = await withErrorHandlingAsync(
graphqlClient
.query<ViewUpdateChannelBasicInfoOnAppQuery, ViewUpdateChannelBasicInfoOnAppQueryVariables>(
gql`
query ViewUpdateChannelBasicInfoOnApp($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
...UpdateChannelBasicInfoFragment
}
}
}
}
${print(UpdateChannelBasicInfoFragmentNode)}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
);
const { updateChannelByName } = response.app.byId;
if (!updateChannelByName) {
throw new ChannelNotFoundError(`Could not find channel with the name ${channelName}`);
}
return updateChannelByName;
},
async viewUpdateChannelAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName, filter }: ViewUpdateChannelOnAppQueryVariables
@@ -1,98 +0,0 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
FingerprintFilterInput,
FingerprintFragment,
FingerprintsByAppIdQuery,
} from '../generated';
import { FingerprintFragmentNode } from '../types/Fingerprint';
export const FingerprintQuery = {
async byHashAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
hash,
}: {
appId: string;
hash: string;
}
): Promise<FingerprintFragment | null> {
const fingerprintConnection = await FingerprintQuery.getFingerprintsAsync(graphqlClient, {
appId,
fingerprintFilter: { hashes: [hash] },
first: 1,
});
const fingerprints = fingerprintConnection.edges.map(edge => edge.node);
return fingerprints[0] ?? null;
},
async getFingerprintsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
first,
after,
last,
before,
fingerprintFilter,
}: {
appId: string;
first?: number;
after?: string;
last?: number;
before?: string;
fingerprintFilter?: FingerprintFilterInput;
}
): Promise<FingerprintsByAppIdQuery['app']['byId']['fingerprintsPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<FingerprintsByAppIdQuery>(
gql`
query FingerprintsByAppId(
$appId: String!
$after: String
$first: Int
$before: String
$last: Int
$fingerprintFilter: FingerprintFilterInput
) {
app {
byId(appId: $appId) {
id
fingerprintsPaginated(
after: $after
first: $first
before: $before
last: $last
filter: $fingerprintFilter
) {
edges {
node {
id
...FingerprintFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
}
${print(FingerprintFragmentNode)}
`,
{ appId, after, first, before, last, fingerprintFilter },
{ additionalTypenames: ['Fingerprint'] }
)
.toPromise()
);
return data.app?.byId.fingerprintsPaginated;
},
};
@@ -4,8 +4,6 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
UpdateByIdQuery,
UpdateByIdQueryVariables,
UpdateFragment,
ViewUpdateGroupsOnAppQuery,
ViewUpdateGroupsOnAppQueryVariables,
@@ -139,30 +137,4 @@ export const UpdateQuery = {
return response.app.byId.updateGroups;
},
async viewByUpdateAsync(
graphqlClient: ExpoGraphqlClient,
{ updateId }: UpdateByIdQueryVariables
): Promise<UpdateFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<UpdateByIdQuery, UpdateByIdQueryVariables>(
gql`
query UpdateByIdQuery($updateId: ID!) {
updates {
byId(updateId: $updateId) {
id
...UpdateFragment
}
}
}
${print(UpdateFragmentNode)}
`,
{ updateId },
{ additionalTypenames: ['Update'] }
)
.toPromise()
);
return data.updates.byId;
},
};
@@ -1,87 +0,0 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
WorkflowRunByIdQuery,
WorkflowRunByIdQueryVariables,
WorkflowRunByIdWithJobsQuery,
WorkflowRunByIdWithJobsQueryVariables,
} from '../generated';
export const WorkflowRunQuery = {
async byIdAsync(
graphqlClient: ExpoGraphqlClient,
workflowRunId: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<WorkflowRunByIdQuery, WorkflowRunByIdQueryVariables>(
gql`
query WorkflowRunById($workflowRunId: ID!) {
workflowRuns {
byId(workflowRunId: $workflowRunId) {
id
status
}
}
}
`,
{ workflowRunId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['WorkflowRun'],
}
)
.toPromise()
);
return data.workflowRuns.byId;
},
async withJobsByIdAsync(
graphqlClient: ExpoGraphqlClient,
workflowRunId: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<WorkflowRunByIdWithJobsQuery, WorkflowRunByIdWithJobsQueryVariables>(
gql`
query WorkflowRunByIdWithJobs($workflowRunId: ID!) {
workflowRuns {
byId(workflowRunId: $workflowRunId) {
id
name
status
createdAt
workflow {
id
name
fileName
}
jobs {
id
key
name
type
status
outputs
createdAt
}
}
}
}
`,
{ workflowRunId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['WorkflowRun'],
}
)
.toPromise()
);
return data.workflowRuns.byId;
},
};
@@ -1,17 +0,0 @@
import gql from 'graphql-tag';
export const BackgroundJobReceiptNode = gql`
fragment BackgroundJobReceiptData on BackgroundJobReceipt {
id
state
tries
willRetry
resultId
resultType
resultData
errorCode
errorMessage
createdAt
updatedAt
}
`;
@@ -57,11 +57,6 @@ export const BuildFragmentNode = gql`
completedAt
expirationDate
isForIosSimulator
metrics {
buildWaitTime
buildQueueTime
buildDuration
}
}
`;
@@ -5,23 +5,5 @@ export const FingerprintFragmentNode = gql`
id
hash
debugInfoUrl
builds(first: 1) {
edges {
node {
id
... on Build {
platform
}
}
}
}
updates(first: 1) {
edges {
node {
id
platform
}
}
}
}
`;
@@ -38,7 +38,6 @@ export const UpdateFragmentNode = gql`
fingerprint {
id
hash
debugInfoUrl
source {
type
bucketKey
+2 -2
View File
@@ -8,7 +8,7 @@ import { createAppleTasks } from './apple/tasks';
import { getAppStoreAuthAsync } from './auth';
import { createAppleWriter, getStaticConfigFilePath } from './config/resolve';
import { MetadataDownloadError, MetadataValidationError } from './errors';
import { subscribeTelemetryAsync } from './utils/telemetry';
import { subscribeTelemetry } from './utils/telemetry';
import { Analytics, MetadataEvent } from '../analytics/AnalyticsManager';
import { CredentialsContext } from '../credentials/context';
import Log from '../log';
@@ -51,7 +51,7 @@ export async function downloadMetadataAsync({
profile,
});
const { unsubscribeTelemetry, executionId } = await subscribeTelemetryAsync(
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
analytics,
MetadataEvent.APPLE_METADATA_DOWNLOAD,
{ app, auth }
+2 -2
View File
@@ -7,7 +7,7 @@ import { getAppStoreAuthAsync } from './auth';
import { createAppleReader, loadConfigAsync } from './config/resolve';
import { MetadataConfig } from './config/schema';
import { MetadataUploadError, MetadataValidationError, logMetadataValidationError } from './errors';
import { subscribeTelemetryAsync } from './utils/telemetry';
import { subscribeTelemetry } from './utils/telemetry';
import { Analytics, MetadataEvent } from '../analytics/AnalyticsManager';
import { CredentialsContext } from '../credentials/context';
import Log from '../log';
@@ -38,7 +38,7 @@ export async function uploadMetadataAsync({
profile,
});
const { unsubscribeTelemetry, executionId } = await subscribeTelemetryAsync(
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
analytics,
MetadataEvent.APPLE_METADATA_UPLOAD,
{ app, auth }
@@ -1,8 +1,8 @@
import { App } from '@expo/apple-utils';
import { TelemetryContext, makeDataScrubberAsync } from '../telemetry';
import { TelemetryContext, makeDataScrubber } from '../telemetry';
describe(makeDataScrubberAsync, () => {
describe(makeDataScrubber, () => {
const stub: TelemetryContext = {
// Only the App ID is considered to be sensitive
app: new App({} as any, 'SECRET_APP_ID', {} as any),
@@ -18,31 +18,25 @@ describe(makeDataScrubberAsync, () => {
} as any,
};
it('scrubs the app.id', async () => {
expect((await makeDataScrubberAsync(stub))('some text SECRET_APP_ID')).toBe(
'some text {APPLE_APP_ID}'
);
it('scrubs the app.id', () => {
expect(makeDataScrubber(stub)('some text SECRET_APP_ID')).toBe('some text {APPLE_APP_ID}');
});
it('scrubs the auth.username', async () => {
expect((await makeDataScrubberAsync(stub))('some text SECRET_USERNAME')).toBe(
'some text {APPLE_USERNAME}'
);
it('scrubs the auth.username', () => {
expect(makeDataScrubber(stub)('some text SECRET_USERNAME')).toBe('some text {APPLE_USERNAME}');
});
it('scrubs the auth.password', async () => {
expect((await makeDataScrubberAsync(stub))('SECRET_PASSWORD')).toBe('{APPLE_PASSWORD}');
it('scrubs the auth.password', () => {
expect(makeDataScrubber(stub)('SECRET_PASSWORD')).toBe('{APPLE_PASSWORD}');
});
it('scrubs the auth.context.token', async () => {
expect((await makeDataScrubberAsync(stub))('some text SECRET_TOKEN')).toBe(
'some text {APPLE_TOKEN}'
);
it('scrubs the auth.context.token', () => {
expect(makeDataScrubber(stub)('some text SECRET_TOKEN')).toBe('some text {APPLE_TOKEN}');
});
it('scrubs the auth.context.token when using token instances', async () => {
it('scrubs the auth.context.token when using token instances', () => {
const token = { getToken: () => 'SECRET_TOKEN_INSTANCE' } as any;
const scrubber = await makeDataScrubberAsync({
const scrubber = makeDataScrubber({
...stub,
auth: {
...stub.auth,
@@ -56,31 +50,29 @@ describe(makeDataScrubberAsync, () => {
expect(scrubber('some text SECRET_TOKEN_INSTANCE')).toBe('some text {APPLE_TOKEN}');
});
it('scrubs the auth.context.teamId', async () => {
expect((await makeDataScrubberAsync(stub))('SECRET_TEAM_ID')).toBe('{APPLE_TEAM_ID}');
it('scrubs the auth.context.teamId', () => {
expect(makeDataScrubber(stub)('SECRET_TEAM_ID')).toBe('{APPLE_TEAM_ID}');
});
it('scrubs the auth.context.teamId', async () => {
expect((await makeDataScrubberAsync(stub))('some provider 1337')).toBe(
'some provider {APPLE_PROVIDER_ID}'
);
it('scrubs the auth.context.teamId', () => {
expect(makeDataScrubber(stub)('some provider 1337')).toBe('some provider {APPLE_PROVIDER_ID}');
});
it('scrubber returns string representation of falsy values', async () => {
const scrubber = await makeDataScrubberAsync(stub);
it('scrubber returns string representation of falsy values', () => {
const scrubber = makeDataScrubber(stub);
expect(scrubber(null)).toBe('null');
expect(scrubber(undefined)).toBe('undefined');
expect(scrubber(false)).toBe('false');
});
it('scrubs multiple sensitive values at once', async () => {
expect(
(await makeDataScrubberAsync(stub))('SECRET_TOKEN SECRET_USERNAME SECRET_PASSWORD')
).toBe('{APPLE_TOKEN} {APPLE_USERNAME} {APPLE_PASSWORD}');
it('scrubs multiple sensitive values at once', () => {
expect(makeDataScrubber(stub)('SECRET_TOKEN SECRET_USERNAME SECRET_PASSWORD')).toBe(
'{APPLE_TOKEN} {APPLE_USERNAME} {APPLE_PASSWORD}'
);
});
it('scrubs json and transforms it to string', async () => {
const scrubber = await makeDataScrubberAsync(stub);
it('scrubs json and transforms it to string', () => {
const scrubber = makeDataScrubber(stub);
expect(scrubber({ foo: 'bar' })).toBe('{"foo":"bar"}');
expect(scrubber({ value: 'SECRET_APP_ID' })).toBe('{"value":"{APPLE_APP_ID}"}');
});
@@ -14,18 +14,18 @@ export type TelemetryContext = {
* When providing the app and auth info, we can scrub that data from the telemetry.
* Returns an execution ID to group all events of a single run together, and a unsubscribe function.
*/
export async function subscribeTelemetryAsync(
export function subscribeTelemetry(
analytics: Analytics,
event: MetadataEvent,
options: TelemetryContext
): Promise<{
): {
/** Unsubscribe the telemetry from all apple-utils events */
unsubscribeTelemetry: () => void;
/** The unique id added to all telemetry events from a single execution */
executionId: string;
}> {
} {
const executionId = uuidv4();
const scrubber = await makeDataScrubberAsync(options);
const scrubber = makeDataScrubber(options);
const { interceptors } = getRequestClient();
const responseInterceptorId = interceptors.response.use(
@@ -68,11 +68,8 @@ export async function subscribeTelemetryAsync(
}
/** Exposed for testing */
export async function makeDataScrubberAsync({
app,
auth,
}: TelemetryContext): Promise<<T>(data: T) => string> {
const token = await getAuthTokenStringAsync(auth);
export function makeDataScrubber({ app, auth }: TelemetryContext): <T>(data: T) => string {
const token = getAuthTokenString(auth);
const patterns: Record<string, RegExp | null> = {
APPLE_APP_ID: new RegExp(app.id, 'gi'),
APPLE_USERNAME: auth.username ? new RegExp(auth.username, 'gi') : null,
@@ -101,13 +98,13 @@ export async function makeDataScrubberAsync({
};
}
async function getAuthTokenStringAsync(auth: TelemetryContext['auth']): Promise<string | null> {
function getAuthTokenString(auth: TelemetryContext['auth']): string | null {
if (!auth.context?.token) {
return null;
}
if (typeof auth.context.token === 'object') {
return await auth.context.token.getToken();
return auth.context.token.getToken();
}
return auth.context.token;
@@ -424,7 +424,7 @@ describe(collectAssetsAsync, () => {
expect(await collectAssetsAsync(inputDir)).toEqual({
android: {
launchAsset: {
fileExtension: '.js',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/android.js`,
},
@@ -432,7 +432,7 @@ describe(collectAssetsAsync, () => {
},
ios: {
launchAsset: {
fileExtension: '.js',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/ios.js`,
},
@@ -440,7 +440,7 @@ describe(collectAssetsAsync, () => {
},
web: {
launchAsset: {
fileExtension: '.js',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/web.js`,
},
@@ -69,7 +69,7 @@ describe(resolveGradleBuildContextAsync, () => {
} as any,
vcsClient
);
expect(gradleContext).toEqual({ moduleName: 'app', flavor: 'abc', buildType: 'release' });
expect(gradleContext).toEqual({ moduleName: 'app', flavor: 'abc' });
});
it('returns undefined if build.gradle does not exist', async () => {
@@ -144,12 +144,6 @@ describe(parseGradleCommand, () => {
});
expect(result).toEqual({ moduleName: 'app', flavor: 'example', buildType: 'release' });
});
test('parsing :app:buildExampleStage', async () => {
const result = parseGradleCommand(':app:buildExampleStage', {
android: { productFlavors: { example: {} } },
});
expect(result).toEqual({ moduleName: 'app', flavor: 'example', buildType: 'stage' });
});
test('parsing :app:buildExampleDebug when flavor is named with uper-case', async () => {
const result = parseGradleCommand(':app:buildExampleRelease', {
android: { productFlavors: { Example: {} } },
@@ -75,9 +75,7 @@ export async function getApplicationIdFromBareAsync(
gradleContext.flavor
);
if (applicationIdSuffix) {
throw new Error(
'"applicationIdSuffix" in app/build.gradle is not supported, configure the full application ID under productFlavors.'
);
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
}
const applicationId = gradleUtils.resolveConfigValue(
buildGradle,
@@ -91,9 +89,7 @@ export async function getApplicationIdFromBareAsync(
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
if (buildGradle.match(/applicationIdSuffix/)) {
throw new Error(
'"applicationIdSuffix" in app/build.gradle is not supported, configure the full application ID under productFlavors.'
);
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
}
if (buildGradle.match(/productFlavors/)) {
throw new AmbiguousApplicationIdError(
@@ -9,7 +9,6 @@ import { Client } from '../../vcs/vcs';
export interface GradleBuildContext {
moduleName?: string;
flavor?: string;
buildType?: string;
}
export async function resolveGradleBuildContextAsync(
@@ -33,11 +32,9 @@ export async function resolveGradleBuildContextAsync(
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior.`
);
}
return {
moduleName: parsedGradleCommand?.moduleName ?? gradleUtils.DEFAULT_MODULE_NAME,
flavor: parsedGradleCommand?.flavor,
buildType: parsedGradleCommand?.buildType,
};
} else {
return { moduleName: gradleUtils.DEFAULT_MODULE_NAME };
@@ -82,7 +82,7 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
const [moduleName, taskName] =
splitCmd.length > 1 ? [splitCmd[0], splitCmd[1]] : [undefined, splitCmd[0]];
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)([A-Z][a-z]+)/);
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)(Release|Debug)/);
if (!matchResult) {
throw new Error(`Failed to parse gradle command: ${cmd}`);
}
@@ -104,14 +104,9 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
throw new Error(`flavor ${firstLetter.toLowerCase().concat(rest)} is not defined`);
}
}
const buildType = matchResult[3]
? matchResult[3].charAt(0).toLowerCase() + matchResult[3].slice(1)
: undefined;
return {
moduleName,
flavor,
buildType,
buildType: matchResult[3] ? matchResult[3].toLowerCase() : undefined,
};
}
@@ -37,7 +37,7 @@ export async function validateCustomBuildConfigAsync({
throw new Error(
`Custom build configuration file ${chalk.bold(
relativeConfigPath
)} is ignored by your version control system or .easignore. Remove it from the ignore list to successfully create custom build.`
)} is ignored by your version control system. Remove it from the ignore list to successfully create custom build.`
);
}
@@ -1,234 +0,0 @@
import { getConfig } from '@expo/config';
import { vol } from 'memfs';
import * as prompts from '../../../prompts';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../exemptEncryption';
jest.mock('fs');
jest.mock('../../../prompts');
beforeEach(async () => {
jest.resetAllMocks();
vol.reset();
});
test('prompts non-exempt encryption is not used', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest.mocked(prompts.confirmAsync).mockReset().mockResolvedValueOnce(true);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
name: 'myproject',
slug: 'myproject',
},
});
});
test('does not prompt if the value is already set', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
name: 'myproject',
slug: 'myproject',
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
}),
},
projectRoot
);
// Reset but no mock values.
jest.mocked(prompts.confirmAsync).mockReset();
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(prompts.confirmAsync).toHaveBeenCalledTimes(0);
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
name: 'myproject',
slug: 'myproject',
});
});
test('prompts non-exempt encryption in CI', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
// Reset but no mock values.
jest.mocked(prompts.confirmAsync).mockReset();
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
name: 'myproject',
slug: 'myproject',
},
});
});
test('prompts non-exempt encryption is not used but app.config.js', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.config.js':
`module.exports = ` +
JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest.mocked(prompts.confirmAsync).mockReset().mockResolvedValueOnce(true);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
// Not set.
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({});
});
test('prompts non-exempt encryption is used', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest
.mocked(prompts.confirmAsync)
.mockReset()
.mockResolvedValueOnce(false)
// Are you sure?
.mockResolvedValueOnce(true);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(prompts.confirmAsync).toHaveBeenCalledTimes(2);
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
name: 'myproject',
slug: 'myproject',
},
});
});
test('prompts non-exempt encryption is used but backed out', async () => {
const projectRoot = '/project';
vol.fromJSON(
{
'package.json': JSON.stringify({}),
'app.json': JSON.stringify({
expo: {
name: 'myproject',
slug: 'myproject',
},
}),
},
projectRoot
);
jest
.mocked(prompts.confirmAsync)
.mockReset()
.mockResolvedValueOnce(false)
// Are you sure?
.mockResolvedValueOnce(false);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir: projectRoot,
exp: getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
nonInteractive: false,
});
expect(getConfig(projectRoot, { skipSDKVersionRequirement: true }).rootConfig).toEqual({
expo: {
name: 'myproject',
slug: 'myproject',
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
},
});
});
@@ -1,120 +0,0 @@
import { ExpoConfig, getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
import chalk from 'chalk';
import Log, { learnMore } from '../../log';
import { confirmAsync } from '../../prompts';
/** Non-exempt encryption must be set on every build in App Store Connect, we move it to before the build process to attempt only setting it once for the entire life-cycle of the project. */
export async function ensureNonExemptEncryptionIsDefinedForManagedProjectAsync({
projectDir,
exp,
nonInteractive,
}: {
projectDir: string;
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<void> {
// TODO: We could add bare workflow support in the future.
// TODO: We could add wizard support for non-exempt encryption in the future.
const ITSAppUsesNonExemptEncryption =
exp.ios?.infoPlist?.ITSAppUsesNonExemptEncryption ?? exp.ios?.config?.usesNonExemptEncryption;
if (ITSAppUsesNonExemptEncryption == null) {
await configureNonExemptEncryptionAsync({
projectDir,
exp,
nonInteractive,
});
} else {
Log.debug(`ITSAppUsesNonExemptEncryption is defined in the app config.`);
}
}
async function configureNonExemptEncryptionAsync({
projectDir,
exp,
nonInteractive,
}: {
projectDir: string;
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<void> {
const description = getProjectConfigDescription(projectDir);
if (nonInteractive) {
Log.warn(
chalk`${description} is missing {bold ios.infoPlist.ITSAppUsesNonExemptEncryption} boolean. Manual configuration is required in App Store Connect before the app can be tested.`
);
return;
}
let onlyExemptEncryption = await confirmAsync({
message: `iOS app only uses standard/exempt encryption? ${chalk.dim(
learnMore(
'https://developer.apple.com/documentation/Security/complying-with-encryption-export-regulations'
)
)}`,
initial: true,
});
if (!onlyExemptEncryption) {
const confirm = await confirmAsync({
message: `Are you sure your app uses non-exempt encryption? Selecting 'Yes' will require annual self-classification reports for the US government.`,
initial: true,
});
if (!confirm) {
Log.warn(
chalk`Set {bold ios.infoPlist.ITSAppUsesNonExemptEncryption} in ${description} to release Apple builds faster.`
);
onlyExemptEncryption = true;
}
}
const ITSAppUsesNonExemptEncryption = !onlyExemptEncryption;
// Only set this value if the answer is no, this enables developers to see the more in-depth prompt in App Store Connect. They can set the value manually in the app.json to avoid the EAS prompt in subsequent builds.
if (ITSAppUsesNonExemptEncryption) {
Log.warn(
`You'll need to manually configure the encryption status in App Store Connect before your build can be tested.`
);
return;
}
// NOTE: Is is it possible to assert that the config needs to be modifiable before building the app?
const modification = await modifyConfigAsync(
projectDir,
{
ios: {
...(exp.ios ?? {}),
infoPlist: {
...(exp.ios?.infoPlist ?? {}),
ITSAppUsesNonExemptEncryption,
},
},
},
{
skipSDKVersionRequirement: true,
}
);
if (modification.type !== 'success') {
Log.log();
if (modification.type === 'warn') {
// The project is using a dynamic config, give the user a helpful log and bail out.
Log.log(chalk.yellow(modification.message));
}
const edits = {
ios: {
infoPlist: {
ITSAppUsesNonExemptEncryption,
},
},
};
Log.log(chalk.cyan(`Add the following to ${description}:`));
Log.log();
Log.log(JSON.stringify(edits, null, 2));
Log.log();
}
}
@@ -1,53 +0,0 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AssetMapSourceInput, AssetMapSourceType, UploadSessionType } from '../graphql/generated';
import Log from '../log';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
export async function maybeUploadAssetMapAsync(
distRoot: string,
graphqlClient: ExpoGraphqlClient
): Promise<AssetMapSourceInput | null> {
const assetMapPath = path.join(distRoot, 'assetmap.json');
if (!(await fs.pathExists(assetMapPath))) {
return null;
}
let gcsBucketKey = undefined;
const fileStat = await fs.promises.stat(assetMapPath);
try {
gcsBucketKey = await uploadFileAtPathToGCSAsync(
graphqlClient,
UploadSessionType.EasUpdateAssetsMetadata,
assetMapPath,
createProgressTracker({
total: fileStat.size,
message: ratio =>
`Uploading assetmap.json (${formatBytes(fileStat.size * ratio)} / ${formatBytes(
fileStat.size
)})`,
completedMessage: (duration: string) => `Uploaded assetmap.json ${chalk.dim(duration)}`,
})
);
} catch (err: any) {
let errMessage = 'Failed to upload assetmap to EAS';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return null;
}
return {
type: AssetMapSourceType.Gcs,
bucketKey: gcsBucketKey,
};
}
+1 -9
View File
@@ -16,18 +16,10 @@ export async function validateMetroConfigForManagedWorkflowAsync(
}
const metroConfig = await loadConfigAsync(ctx.projectDir);
// SDK <50 | plugins whose presence can be used to determine if the config is extending Expo's Metro config
const hasHashAssetFilesPlugin = metroConfig.transformer?.assetPlugins?.find((plugin: string) =>
plugin.match(/expo-asset[/|\\]tools[/|\\]hashAssetFiles/)
);
// SDK >=51 | this is a custom property that we inject to ensure cache invalidation between projects.
const isExtendingExpoMetroConfig = metroConfig.transformer.hasOwnProperty(
'_expoRelativeProjectRoot'
);
if (!isExtendingExpoMetroConfig && !hasHashAssetFilesPlugin) {
if (!hasHashAssetFilesPlugin) {
Log.warn(
`It looks like that you are using a custom ${chalk.bold(
'metro.config.js'
+1 -1
View File
@@ -88,7 +88,7 @@ export function isUsingEASUpdate(exp: ExpoConfig, projectId: string): boolean {
return exp.updates?.url === getEASUpdateURL(projectId);
}
export async function getExpoUpdatesPackageVersionIfInstalledAsync(
async function getExpoUpdatesPackageVersionIfInstalledAsync(
projectDir: string
): Promise<string | null> {
const maybePackageJson = resolveFrom.silent(projectDir, 'expo-updates/package.json');
+4 -49
View File
@@ -17,14 +17,11 @@ import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from '.
import { resolveRuntimeVersionUsingCLIAsync } from './resolveRuntimeVersionAsync';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getDefaultBranchNameAsync } from '../branch/utils';
import { fetchBuildsAsync } from '../commandUtils/builds';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { FingerprintOptions, createFingerprintsByKeyAsync } from '../fingerprint/cli';
import {
AppPlatform,
AssetMetadataStatus,
BuildFragment,
PartialManifestAsset,
UpdateRolloutInfo,
UpdateRolloutInfoGroup,
@@ -53,6 +50,7 @@ import { truthy } from '../utils/expodash/filter';
import groupBy from '../utils/expodash/groupBy';
import mapMapAsync from '../utils/expodash/mapMapAsync';
import uniqBy from '../utils/expodash/uniqBy';
import { FingerprintOptions, createFingerprintsByKeyAsync } from '../utils/fingerprintCli';
import { Client } from '../vcs/vcs';
// update publish does not currently support web
@@ -94,7 +92,7 @@ type UpdateInfoGroup = {
};
// Partial copy of `@expo/dev-server` `BundleAssetWithFileHashes`
export type AssetMap = Record<
type AssetMap = Record<
string,
{
httpServerLocation: string;
@@ -374,7 +372,7 @@ export function filterCollectedAssetsByRequestedPlatforms(
}
/** Try to load the asset map for logging the names of assets published */
async function loadAssetMapAsync(distRoot: string): Promise<AssetMap | null> {
export async function loadAssetMapAsync(distRoot: string): Promise<AssetMap | null> {
const assetMapPath = path.join(distRoot, 'assetmap.json');
if (!(await fs.pathExists(assetMapPath))) {
@@ -421,8 +419,7 @@ export async function collectAssetsAsync(dir: string): Promise<CollectedAssets>
for (const platform of Object.keys(metadata.fileMetadata) as ExpoConfigPlatform[]) {
collectedAssets[platform] = {
launchAsset: {
// path.extname() returns an empty string when there's no extension so we use || to fall back to .bundle
fileExtension: path.extname(metadata.fileMetadata[platform].bundle) || '.bundle',
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(dir, metadata.fileMetadata[platform].bundle),
},
@@ -1001,48 +998,6 @@ export async function maybeCalculateFingerprintForRuntimeVersionInfoObjectsWitho
return [...runtimesWithComputedFingerprint, ...runtimesWithPreviouslyComputedFingerprints];
}
export async function findCompatibleBuildsAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
runtimeToPlatformsAndFingerprintInfoMapping: {
runtimeVersion: string;
platforms: UpdatePublishPlatform[];
fingerprintInfoGroup: FingerprintInfoGroup;
}
): Promise<{
runtimeVersion: string;
platforms: UpdatePublishPlatform[];
fingerprintInfoGroupWithCompatibleBuilds: {
android?: (FingerprintInfo & { build?: BuildFragment }) | undefined;
ios?: (FingerprintInfo & { build?: BuildFragment }) | undefined;
};
}> {
const { fingerprintInfoGroup } = runtimeToPlatformsAndFingerprintInfoMapping;
const entriesPromises = Object.entries(fingerprintInfoGroup).map(
async ([platform, fingerprintInfo]) => {
const build = (
await fetchBuildsAsync({
graphqlClient,
projectId: appId,
filters: {
fingerprintHash: fingerprintInfo.fingerprintHash,
},
})
)[0] as BuildFragment | undefined;
return [platform, { ...fingerprintInfo, build }];
}
);
const entries = await Promise.all(entriesPromises);
const fingerprintInfoGroupWithCompatibleBuilds = Object.fromEntries(entries) as {
android?: (FingerprintInfo & { build?: BuildFragment }) | undefined;
ios?: (FingerprintInfo & { build?: BuildFragment }) | undefined;
};
return {
...runtimeToPlatformsAndFingerprintInfoMapping,
fingerprintInfoGroupWithCompatibleBuilds,
};
}
export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
android: 'Android',
ios: 'iOS',
@@ -1,11 +1,7 @@
import chalk from 'chalk';
import fs from 'node:fs';
import {
assertProjectTarballSizeDoesNotExceedLimit,
makeProjectTarballAsync,
maybeWarnAboutProjectTarballSize,
} from '../build/utils/repository';
import { makeProjectTarballAsync } from '../build/utils/repository';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AccountUploadSessionType } from '../graphql/generated';
import Log, { learnMore } from '../log';
@@ -40,8 +36,19 @@ export async function uploadAccountScopedProjectSourceAsync({
const projectTarball = await makeProjectTarballAsync(vcsClient);
projectTarballPath = projectTarball.path;
maybeWarnAboutProjectTarballSize(projectTarball.size);
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size);
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
}
const projectArchiveBucketKey = await uploadAccountScopedFileAtPathToGCSAsync(graphqlClient, {
accountId,
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff. Show more