Compare commits

..
Author SHA1 Message Date
Expo Bot 1a608cc86e Upgrade eas-build dependencies 2025-02-03 16:44:13 +00:00
104 changed files with 1014 additions and 6515 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
+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
-204
View File
@@ -8,216 +8,12 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 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))
### 🐛 Bug fixes
- Add missing sourcemap normalization to EAS Hosting worker deployment uploads. ([#3004](https://github.com/expo/eas-cli/pull/3004) by [@kitten](https://github.com/kitten))
### 🧹 Chores
## [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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.3.3",
"version": "14.7.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
@@ -1,3 +0,0 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
@@ -1,58 +0,0 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "0.1.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/config": "10.0.6",
"@babel/code-frame": "7.23.5",
"@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,41 +0,0 @@
import { getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
// import from '@expo/config';
type RunOptions = any;
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,147 +0,0 @@
import { Platform } 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';
// import from '@expo/config';
type CalculateFingerprintHashProps = any;
type RemoteBuildCachePlugin = any;
type RunOptions = any;
async function resolveRemoteBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: {
projectRoot: string;
platform: Platform;
fingerprintHash: string;
runOptions: RunOptions;
}): 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 remote build cache, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadEASRemoteBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: {
projectRoot: string;
runOptions: RunOptions;
platform: Platform;
fingerprintHash: string;
buildPath?: string;
}): 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 remote cache}`);
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 EASRemoteBuildCacheProvider: RemoteBuildCachePlugin = {
resolveRemoteBuildCache: resolveRemoteBuildCacheAsync,
uploadRemoteBuildCache: uploadEASRemoteBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASRemoteBuildCacheProvider;
@@ -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/**/*"]
}
+87 -154
View File
@@ -70,7 +70,6 @@ eas --help COMMAND
* [`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)
@@ -108,7 +107,6 @@ eas --help COMMAND
* [`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 +160,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -179,7 +177,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -196,7 +194,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -210,7 +208,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -262,7 +260,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -283,7 +281,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -303,7 +301,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -323,7 +321,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -346,7 +344,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -383,7 +381,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -402,7 +400,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -419,7 +417,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.3.3/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.7.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -438,7 +436,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -458,28 +456,7 @@ 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.3.3/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.3.3/packages/eas-cli/src/commands/build/download.ts)_
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:inspect`
@@ -514,7 +491,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.3.3/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.7.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -566,7 +543,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.3.3/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.7.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -595,7 +572,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.3.3/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.7.1/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -621,7 +598,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.3.3/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.7.1/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -671,7 +648,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.3.3/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.7.1/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -690,7 +667,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.3.3/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.7.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -709,7 +686,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.3.3/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.7.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -726,7 +703,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.3.3/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.7.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -747,7 +724,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -769,7 +746,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.3.3/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.7.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -789,7 +766,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -811,7 +788,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.3.3/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.7.1/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -833,7 +810,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.3.3/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.7.1/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -866,7 +843,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.3.3/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.7.1/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -889,7 +866,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -910,7 +887,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.3.3/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -927,7 +904,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -945,11 +922,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.3.3/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.7.1/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
@@ -967,13 +944,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.3.3/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.7.1/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -998,7 +975,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/deploy/alias.ts)_
## `eas deploy:promote`
@@ -1035,7 +1012,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.3.3/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.7.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1055,7 +1032,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.3.3/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.7.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1076,7 +1053,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.3.3/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.7.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1097,7 +1074,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1111,7 +1088,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.3.3/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.7.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1125,7 +1102,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1154,7 +1131,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.3.3/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.7.1/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1178,7 +1155,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.3.3/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.7.1/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1199,7 +1176,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.3.3/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.7.1/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1224,7 +1201,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.3.3/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.7.1/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1249,7 +1226,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.3.3/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.7.1/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1272,7 +1249,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.3.3/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.7.1/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1293,7 +1270,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.3.3/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.7.1/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1324,83 +1301,39 @@ 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.3.3/packages/eas-cli/src/commands/env/update.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
compare fingerprints of the current project, builds, and updates
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]
$ eas fingerprint:compare [HASH1] [HASH2] [--build-id <value>] [--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
--build-id=<value> Compare the fingerprint with the build with the specified ID
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
compare fingerprints of the current project, builds, and updates
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 c71a7d475aa6f75291bc93cd74aef395c3c94eee # Compare fingerprint against local directory
$ eas fingerprint:compare <FINGERPRINT-HASH-1> <FINGERPRINT-HASH-2> # Compare provided fingerprints
$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee f0d6a916e73f401d428e6e006e07b12453317ba2 # 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
$ eas fingerprint:compare --build-id 82bc6456-611a-48cb-8db4-5f9eb2ca1003 # Compare fingerprint from build against local directory
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.3.3/packages/eas-cli/src/commands/fingerprint/generate.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas help [COMMAND]`
@@ -1508,7 +1441,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.3.3/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.7.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1525,7 +1458,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.3.3/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.7.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1542,7 +1475,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.3.3/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.7.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1572,7 +1505,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.3.3/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1586,7 +1519,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.3.3/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.7.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1609,7 +1542,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1627,7 +1560,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1658,7 +1591,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v14.7.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1700,7 +1633,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1720,7 +1653,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.3.3/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.7.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1741,7 +1674,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.3.3/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.7.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1765,7 +1698,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.3.3/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.7.1/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1787,7 +1720,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1819,7 +1752,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.3.3/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.7.1/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1847,7 +1780,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.3.3/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.7.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1867,7 +1800,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.3.3/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.7.1/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1887,7 +1820,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1908,7 +1841,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1928,7 +1861,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1946,7 +1879,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1968,7 +1901,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1985,7 +1918,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2027,7 +1960,7 @@ ALIASES
## `eas deploy [options]`
deploy your Expo Router web build and API Routes
Deploy your Expo Router web build and API Routes.
```
USAGE
@@ -2045,7 +1978,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
@@ -2069,7 +2002,7 @@ 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.3.3/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.7.1/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:run [FILE]`
@@ -2089,7 +2022,7 @@ DESCRIPTION
Run an EAS workflow
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.3.3/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.7.1/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:validate PATH`
@@ -2109,5 +2042,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.3.3/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.7.1/packages/eas-cli/src/commands/workflow/validate.ts)_
<!-- commandsstop -->
+21 -1849
View File
File diff suppressed because it is too large. Load diff
+5 -7
View File
@@ -1,19 +1,19 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.3.3",
"version": "14.7.1",
"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.6",
"@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.3.3",
"@expo/eas-build-job": "1.0.169",
"@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.169",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -68,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",
@@ -79,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 () => {});
@@ -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,
+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([
-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 -5
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,
+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.168';
export enum LocalBuildMode {
/**
@@ -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,7 +97,6 @@ export interface BuildFlags {
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled?: boolean;
}
export async function runBuildAndSubmitAsync({
@@ -123,7 +121,6 @@ export async function runBuildAndSubmitAsync({
envOverride?: Env;
}): Promise<{
buildIds: string[];
buildProfiles?: ProfileData<'build'>[];
}> {
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
@@ -350,7 +347,6 @@ export async function runBuildAndSubmitAsync({
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
buildProfiles,
};
}
@@ -402,7 +398,6 @@ async function prepareAndStartBuildAsync({
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
env,
});
@@ -550,21 +545,11 @@ 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);
}
+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'
)}`
);
}
}
+22 -76
View File
@@ -1,7 +1,6 @@
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,
@@ -9,19 +8,19 @@ import {
ensureProjectConfiguredAsync,
} from '../../build/configure';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { downloadAndRunAsync, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { 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 { 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 { runAsync } from '../../run/run';
import { downloadAndMaybeExtractAppAsync } from '../../utils/download';
import { createFingerprintAsync } from '../../utils/fingerprintCli';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { Client } from '../../vcs/vcs';
@@ -110,11 +109,18 @@ export default class BuildDev extends EasCommand {
Log.log(`✨ Calculated fingerprint hash: ${fingerprint.hash}`);
Log.newLine();
const builds = await this.getBuildsAsync({
graphqlClient,
projectId,
platform,
fingerprint,
const builds = 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,
});
if (builds.length !== 0) {
const build = builds[0];
@@ -123,8 +129,11 @@ export default class BuildDev extends EasCommand {
);
if (build.artifacts?.applicationArchiveUrl) {
await downloadAndRunAsync(build);
await this.startDevServerAsync({ projectDir, platform });
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform
);
await runAsync(buildPath, build.platform);
return;
} else {
Log.warn('Artifacts for this build expired. New build will be started.');
@@ -133,22 +142,6 @@ export default class BuildDev extends EasCommand {
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,
@@ -172,7 +165,6 @@ export default class BuildDev extends EasCommand {
downloadSimBuildAutoConfirm: true,
envOverride: env,
});
await this.startDevServerAsync({ projectDir, platform });
}
private async selectPlatformAsync(platform?: Platform): Promise<Platform> {
@@ -302,50 +294,4 @@ export default class BuildDev extends EasCommand {
});
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;
}
+2 -59
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;
}
@@ -119,10 +117,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,7 +160,7 @@ export default class Build extends EasCommand {
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
const { buildProfiles } = await runBuildAndSubmitAsync({
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
@@ -175,11 +169,6 @@ export default class Build extends EasCommand {
actor,
getDynamicPrivateProjectConfigAsync,
});
this.maybeSuggestUsingEasBuildDev({
buildProfiles,
nonInteractive: flags.nonInteractive,
});
}
private sanitizeFlags(
@@ -243,7 +232,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 +268,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(
@@ -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,16 +64,10 @@ 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({
+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,46 +1,50 @@
import { Platform, Workflow } 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 { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import {
AppPlatform,
BuildFragment,
BuildStatus,
EnvironmentVariableEnvironment,
FingerprintFragment,
UpdateFragment,
} from '../../graphql/generated';
import { FingerprintMutation } from '../../graphql/mutations/FingerprintMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { FingerprintQuery } from '../../graphql/queries/FingerprintQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
import {
getDisplayNameForProjectIdAsync,
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { selectAsync } from '../../prompts';
import { Fingerprint, FingerprintDiffItem } from '../../utils/fingerprint';
import { createFingerprintAsync, diffFingerprint } from '../../utils/fingerprintCli';
import { abridgedDiff } from '../../utils/fingerprintDiff';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { Client } from '../../vcs/vcs';
export interface FingerprintCompareFlags {
buildId?: string;
updateId?: string;
hash1?: string;
hash2?: string;
nonInteractive: boolean;
json: boolean;
}
enum FingerprintOriginType {
Build = 'build',
Update = 'update',
@@ -55,18 +59,14 @@ type FingerprintOrigin = {
};
export default class FingerprintCompare extends EasCommand {
static override description = 'compare fingerprints of the current project, builds, and updates';
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',
'$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee \t # Compare fingerprint against local directory',
'$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee f0d6a916e73f401d428e6e006e07b12453317ba2 \t # Compare provided fingerprints',
'$ eas fingerprint:compare --build-id 82bc6456-611a-48cb-8db4-5f9eb2ca1003 \t # Compare fingerprint from build against local directory',
];
static override args = [
@@ -87,20 +87,10 @@ export default class FingerprintCompare extends EasCommand {
'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,7 +100,6 @@ export default class FingerprintCompare extends EasCommand {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
...this.ContextOptions.ServerSideEnvironmentVariables,
};
async runAsync(): Promise<void> {
@@ -119,61 +108,38 @@ export default class FingerprintCompare extends EasCommand {
const {
json,
'non-interactive': nonInteractive,
'build-id': buildIds,
'update-id': updateIds,
open,
environment,
'build-id': buildId,
'update-id': updateId,
} = flags;
const [buildId1, buildId2] = buildIds ?? [];
const [updateId1, updateId2] = updateIds ?? [];
const sanitizedFlagsAndArgs = { json, nonInteractive, buildId, updateId, hash1, hash2 };
const {
projectId,
privateProjectConfig: { projectDir },
loggedIn: { graphqlClient },
vcsClient,
getServerSideEnvironmentVariablesAsync,
} = await this.getContextAsync(FingerprintCompare, {
nonInteractive,
withServerSideEnvironment: environment ?? null,
withServerSideEnvironment: null,
});
if (json) {
enableJsonOutput();
}
const firstFingerprintInfo = await getFingerprintInfoAsync(
const firstFingerprintInfo = await getFirstFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
{
nonInteractive,
buildId: buildId1,
updateId: updateId1,
hash: hash1,
}
sanitizedFlagsAndArgs
);
const { fingerprint: firstFingerprint, origin: firstFingerprintOrigin } = firstFingerprintInfo;
const isFirstFingerprintSpecifiedByFlagOrArg = hash1 || buildId1 || updateId1;
const isSecondFingerprintSpecifiedByFlagOrArg = hash2 || buildId2 || updateId2;
const secondFingerprintInfo = await getFingerprintInfoAsync(
const secondFingerprintInfo = await getSecondFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
{
nonInteractive,
buildId: buildId2,
updateId: updateId2,
hash: hash2,
useProjectFingerprint:
isFirstFingerprintSpecifiedByFlagOrArg && !isSecondFingerprintSpecifiedByFlagOrArg,
environmentForProjectFingerprint: environment,
},
firstFingerprintInfo
firstFingerprintInfo,
sanitizedFlagsAndArgs
);
const { fingerprint: secondFingerprint, origin: secondFingerprintOrigin } =
secondFingerprintInfo;
@@ -187,7 +153,10 @@ export default class FingerprintCompare extends EasCommand {
Log.log(
`✅ ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} matches ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
)} matches fingerprint from ${prettyPrintFingerprint(
secondFingerprint,
secondFingerprintOrigin
)}`
);
return;
} else {
@@ -248,244 +217,22 @@ 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;
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} ${origin.type}`;
} else if (origin.type === FingerprintOriginType.Build) {
return `fingerprint ${fingerprint.hash} from ${origin.build?.platform} ${origin.type}`;
}
): 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,
});
return `fingerprint ${fingerprint.hash} from hash`;
}
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 } };
function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
async function getFingerprintFromUpdateFragmentAsync(
@@ -509,84 +256,99 @@ async function getFingerprintFromUpdateFragmentAsync(
};
}
async function getFingerprintInfoFromHashAsync(
async function getFirstFingerprintInfoAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
hash: string
{
buildId: buildIdFromArg,
updateId: updateIdFromArg,
hash1,
nonInteractive,
}: FingerprintCompareFlags
): 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,
}))
if (hash1) {
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
graphqlClient,
projectId,
hash1
);
return await getFingerprintFromUpdateFragmentAsync(update);
} catch (error: any) {
if (!error?.message.includes('Could not find any updates with group ID')) {
throw error;
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,
},
};
}
if (updateIdFromArg) {
// Some people may pass in update group id instead of update id, so add interactive support for that
try {
const maybeUpdateGroupId = updateIdFromArg;
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: maybeUpdateGroupId,
});
if (updateGroup.length === 1) {
const update = updateGroup[0];
return await getFingerprintFromUpdateFragmentAsync(update);
}
if (nonInteractive) {
const [accountName, project] = await Promise.all([
(await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name,
AppQuery.byIdAsync(graphqlClient, projectId),
]);
const updateUrl =
getExpoWebsiteBaseUrl() +
`/accounts/${accountName}/projects/${project.name}/updates/${updateIdFromArg}`;
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 updateWithFingerprint = await UpdateQuery.viewByUpdateAsync(graphqlClient, {
updateId: updateIdFromArg,
});
return await getFingerprintFromUpdateFragmentAsync(updateWithFingerprint);
}
let buildId: string | null = buildIdFromArg ?? null;
if (!buildId) {
if (nonInteractive) {
throw new Error('Build ID must be provided in non-interactive mode');
}
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
throw new Error('Must select build with fingerprint for comparison.');
}
}
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 }> {
Log.log(`Comparing fingerprints of the current project and build ${buildId}…`);
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
if (!buildWithFingerprint.fingerprint) {
throw new Error(`Fingerprint for build ${buildId} was not computed.`);
@@ -603,6 +365,72 @@ async function getFingerprintInfoFromBuildIdAsync(
};
}
async function getSecondFingerprintInfoAsync(
graphqlClient: ExpoGraphqlClient,
projectDir: string,
projectId: string,
vcsClient: Client,
firstFingerprintInfo: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
},
{ hash2 }: FingerprintCompareFlags
): Promise<{ fingerprint: Fingerprint; origin: FingerprintOrigin }> {
if (hash2) {
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
graphqlClient,
projectId,
hash2
);
if (!fingerprintFragment) {
throw new Error(`Fingerprint with hash ${hash2} was not uploaded.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment),
origin: { type: FingerprintOriginType.Hash },
};
}
const firstFingerprintPlatforms = firstFingerprintInfo.platforms;
if (!firstFingerprintPlatforms) {
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.`
);
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const optionsFromWorkflow = getFingerprintOptionsFromWorkflow(
firstFingerprintPlatforms,
workflows
);
const projectFingerprint = await createFingerprintAsync(projectDir, {
...optionsFromWorkflow,
platforms: firstFingerprintPlatforms.map(appPlatformToString),
debug: true,
env: undefined,
});
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 { fingerprint: projectFingerprint, origin: { type: FingerprintOriginType.Project } };
}
async function getFingerprintFragmentFromHashAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
@@ -632,6 +460,37 @@ async function getFingerprintFromFingerprintFragmentAsync(
return (await fingerprintResponse.json()) as Fingerprint;
}
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 };
}
function printContentDiff(diff: FingerprintDiffItem): void {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
@@ -737,10 +596,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 +640,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 +649,40 @@ 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}`);
}
}
function stringToAppPlatform(platform: string): AppPlatform {
switch (platform) {
case 'android':
return AppPlatform.Android;
case 'ios':
return AppPlatform.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({
@@ -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;
-386
View File
@@ -1,386 +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 {
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 { 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,
} = 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 }
);
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;
}
type AppMetadata = {
fingerprintHash?: string;
developmentClient: boolean;
simulator: boolean;
};
async function extractAppMetadataAsync(
buildPath: string,
platform: Platform
): Promise<AppMetadata> {
let developmentClient = false;
let fingerprintHash: string | undefined;
const simulator = platform === Platform.IOS;
const basePath = platform === Platform.ANDROID ? 'assets/' : buildPath;
const fingerprintFilePath =
platform === Platform.ANDROID ? 'fingerprint' : 'EXUpdates.bundle/fingerprint';
const devMenuBundlePath =
platform === Platform.ANDROID ? 'EXDevMenuApp.android.js' : 'EXDevMenu.bundle/';
const buildExtension = path.extname(buildPath);
if (['.apk', '.aab'].includes(buildExtension)) {
const zip = new StreamZip.async({ file: buildPath });
try {
developmentClient = Boolean(await zip.entry(path.join(basePath, devMenuBundlePath)));
if (await zip.entry(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = (await zip.entryData(path.join(basePath, fingerprintFilePath))).toString(
'utf-8'
);
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
} finally {
await zip.close();
}
} else if (buildExtension === '.app') {
developmentClient = await fs.exists(path.join(basePath, devMenuBundlePath));
if (await fs.exists(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = await fs.readFile(path.join(basePath, fingerprintFilePath), 'utf8');
}
} else {
// Use tar to list files in the archive
try {
let fingerprintHashPromise: Promise<string> | undefined;
await tar.list({
file: buildPath,
// eslint-disable-next-line async-protect/async-suffix
onentry: entry => {
if (entry.path.endsWith(devMenuBundlePath)) {
developmentClient = true;
}
if (entry.path.endsWith(fingerprintFilePath)) {
fingerprintHashPromise = new Promise<string>(async (resolve, reject) => {
try {
let content = '';
for await (const chunk of entry) {
content += chunk.toString('utf8');
}
resolve(content);
} catch (error) {
reject(error);
}
});
}
},
});
if (fingerprintHashPromise !== undefined) {
fingerprintHash = await fingerprintHashPromise;
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
}
}
return {
developmentClient,
fingerprintHash,
simulator,
};
}
@@ -129,7 +129,7 @@ export default class WorkflowRun extends EasCommand {
Log.newLine();
Log.succeed(
`Workflow run started successfully. See logs: ${link(
`Workflow started successfully. ${link(
getWorkflowRunUrl(account.name, projectName, workflowRunId)
)}`
);
@@ -13,40 +13,20 @@ const AUTO_GROUP_NAME = 'Team (Expo)';
* 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 group = await ensureInternalGroupAsync(app);
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> {
async function ensureInternalGroupAsync(app: App): Promise<BetaGroup> {
const groups = await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
});
let betaGroup = groups.find(group => group.attributes.name === AUTO_GROUP_NAME);
if (!betaGroup) {
const spinner = ora().start('Creating TestFlight group...');
@@ -57,6 +37,8 @@ async function ensureInternalGroupAsync({
async () => {
betaGroup = await app.createBetaGroupAsync({
name: AUTO_GROUP_NAME,
publicLinkEnabled: false,
publicLinkLimitEnabled: false,
isInternalGroup: true,
// Automatically add latest builds to the group without needing to run the command.
hasAccessToAllBuilds: true,
@@ -65,7 +47,7 @@ async function ensureInternalGroupAsync({
{
shouldRetry(error) {
if (isAppleError(error)) {
spinner.text = `TestFlight still preparing, retrying in 10 seconds...`;
spinner.text = `TestFlight not ready, retrying in 25 seconds...`;
return error.data.errors.some(
error => error.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID'
@@ -94,14 +76,7 @@ async function ensureInternalGroupAsync({
})
) {
await BetaGroup.deleteAsync(app.context, { id: betaGroup.id });
return await ensureInternalGroupAsync({
app,
groups: await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
}),
});
return await ensureInternalGroupAsync(app);
}
}
@@ -199,10 +174,9 @@ async function pollRetryAsync<T>(
fn: () => Promise<T>,
{
shouldRetry,
retries = 15,
retries = 10,
// 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,
interval = 25000,
}: { shouldRetry?: (error: Error) => boolean; retries?: number; interval?: number } = {}
): Promise<T> {
let lastError: Error | null = null;
-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}`);
}
}
+21 -289
View File
@@ -131,7 +131,6 @@ export type Account = {
googleServiceAccountKeys: Array<GoogleServiceAccountKey>;
/** Android credentials for account */
googleServiceAccountKeysPaginated: AccountGoogleServiceAccountKeysConnection;
hasBuilds: Scalars['Boolean']['output'];
id: Scalars['ID']['output'];
isCurrent: Scalars['Boolean']['output'];
isDisabled: Scalars['Boolean']['output'];
@@ -148,12 +147,10 @@ export type Account = {
owner?: Maybe<User>;
/** Owning UserActor of this account if personal account */
ownerUserActor?: Maybe<UserActor>;
pendingSentryInstallation?: Maybe<PendingSentryInstallation>;
profileImageUrl: Scalars['String']['output'];
pushSecurityEnabled: Scalars['Boolean']['output'];
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean']['output'];
sentryInstallation?: Maybe<SentryInstallation>;
/** Snacks associated with this account */
snacks: Array<Snack>;
/** SSO configuration for this account */
@@ -1025,7 +1022,6 @@ export enum AndroidBuildType {
export type AndroidBuilderEnvironmentInput = {
bun?: InputMaybe<Scalars['String']['input']>;
corepack?: InputMaybe<Scalars['Boolean']['input']>;
env?: InputMaybe<Scalars['JSONObject']['input']>;
expoCli?: InputMaybe<Scalars['String']['input']>;
image?: InputMaybe<Scalars['String']['input']>;
@@ -1057,12 +1053,9 @@ export type AndroidFcmInput = {
export type AndroidFcmMutation = {
__typename?: 'AndroidFcmMutation';
/**
* Create an FCM V0/Legacy credential
* @deprecated FCM Legacy credentials are no longer supported by Google. Use createFcmV1Credential instead.
*/
/** Create an FCM credential */
createAndroidFcm: AndroidFcm;
/** Delete an FCM V0/Legacy credential */
/** Delete an FCM credential */
deleteAndroidFcm: DeleteAndroidFcmResult;
};
@@ -1325,7 +1318,6 @@ export type App = Project & {
privacy: Scalars['String']['output'];
/** @deprecated No longer supported */
privacySetting: AppPrivacy;
profileImageUrl?: Maybe<Scalars['String']['output']>;
/**
* Whether there have been any classic update publishes
* @deprecated Classic updates have been deprecated.
@@ -1350,7 +1342,6 @@ export type App = Project & {
* @deprecated Classic updates have been deprecated.
*/
sdkVersion: Scalars['String']['output'];
sentryProject?: Maybe<SentryProject>;
slug: Scalars['String']['output'];
/** EAS Submissions associated with this app */
submissions: Array<Submission>;
@@ -1534,7 +1525,6 @@ export type AppLikedByArgs = {
export type AppRuntimesArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<RuntimeFilterInput>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
};
@@ -2550,21 +2540,6 @@ export type AscApiKeyInput = {
keyP8: Scalars['String']['input'];
};
export type AssetMapGroup = {
android?: InputMaybe<AssetMapSourceInput>;
ios?: InputMaybe<AssetMapSourceInput>;
web?: InputMaybe<AssetMapSourceInput>;
};
export type AssetMapSourceInput = {
bucketKey?: InputMaybe<Scalars['String']['input']>;
type?: InputMaybe<AssetMapSourceType>;
};
export enum AssetMapSourceType {
Gcs = 'GCS'
}
export type AssetMetadataResult = {
__typename?: 'AssetMetadataResult';
status: AssetMetadataStatus;
@@ -2683,11 +2658,6 @@ export enum AuthProviderIdentifier {
StubIdp = 'STUB_IDP'
}
export type AverageMetrics = {
__typename?: 'AverageMetrics';
averageDownloadSize: Scalars['Int']['output'];
};
export type BackgroundJobReceipt = {
__typename?: 'BackgroundJobReceipt';
account: Account;
@@ -2779,7 +2749,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
/** @deprecated Use 'updateChannel' field instead. */
channel?: Maybe<Scalars['String']['output']>;
childBuild?: Maybe<Build>;
cliVersion?: Maybe<Scalars['String']['output']>;
completedAt?: Maybe<Scalars['DateTime']['output']>;
createdAt: Scalars['DateTime']['output'];
customNodeVersion?: Maybe<Scalars['String']['output']>;
@@ -2795,7 +2764,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
gitCommitHash?: Maybe<Scalars['String']['output']>;
gitCommitMessage?: Maybe<Scalars['String']['output']>;
gitRef?: Maybe<Scalars['String']['output']>;
/** @deprecated Use 'githubRepository' field instead */
githubRepositoryOwnerAndName?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
/** Queue position is 1-indexed */
@@ -3052,7 +3020,6 @@ export type BuildMetrics = {
export enum BuildMode {
Build = 'BUILD',
Custom = 'CUSTOM',
Local = 'LOCAL',
Repack = 'REPACK',
Resign = 'RESIGN'
}
@@ -3070,8 +3037,6 @@ export type BuildMutation = {
createAndroidBuild: CreateBuildResult;
/** Create an iOS build */
createIosBuild: CreateBuildResult;
/** Create a local build */
createLocalBuild: CreateBuildResult;
/** Delete an EAS Build build */
deleteBuild: Build;
/** Retry an Android EAS Build */
@@ -3109,14 +3074,6 @@ export type BuildMutationCreateIosBuildArgs = {
};
export type BuildMutationCreateLocalBuildArgs = {
appId: Scalars['ID']['input'];
artifactSource: LocalBuildArchiveSourceInput;
job: LocalBuildJobInput;
metadata?: InputMaybe<BuildMetadataInput>;
};
export type BuildMutationDeleteBuildArgs = {
buildId: Scalars['ID']['input'];
};
@@ -3216,7 +3173,6 @@ export enum BuildPriority {
export type BuildPublicData = {
__typename?: 'BuildPublicData';
artifacts: PublicArtifacts;
buildMode?: Maybe<BuildMode>;
distribution?: Maybe<DistributionType>;
id: Scalars['ID']['output'];
isForIosSimulator: Scalars['Boolean']['output'];
@@ -3424,11 +3380,6 @@ export type CreateAccessTokenResponse = {
token: Scalars['String']['output'];
};
export type CreateAndConfigureRepositoryInput = {
appId: Scalars['ID']['input'];
installationIdentifier: Scalars['Int']['input'];
};
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID']['input'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
@@ -3521,12 +3472,6 @@ export type CreateIosSubmissionInput = {
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
};
export type CreateSentryProjectInput = {
appId: Scalars['ID']['input'];
sentryProjectId: Scalars['String']['input'];
sentryProjectSlug: Scalars['String']['input'];
};
export type CreateSharedEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
fileName?: InputMaybe<Scalars['String']['input']>;
@@ -3746,11 +3691,6 @@ export type DeleteSsoUserResult = {
id: Scalars['ID']['output'];
};
export type DeleteSentryProjectResult = {
__typename?: 'DeleteSentryProjectResult';
id: Scalars['ID']['output'];
};
export type DeleteUpdateBranchResult = {
__typename?: 'DeleteUpdateBranchResult';
id: Scalars['ID']['output'];
@@ -4061,8 +4001,6 @@ export enum EntityTypeName {
CustomerEntity = 'CustomerEntity',
GoogleServiceAccountKeyEntity = 'GoogleServiceAccountKeyEntity',
IosAppCredentialsEntity = 'IosAppCredentialsEntity',
LogRocketOrganizationEntity = 'LogRocketOrganizationEntity',
LogRocketProjectEntity = 'LogRocketProjectEntity',
UserInvitationEntity = 'UserInvitationEntity',
UserPermissionEntity = 'UserPermissionEntity',
WorkerCustomDomainEntity = 'WorkerCustomDomainEntity',
@@ -4450,13 +4388,6 @@ export type GenerateLogRocketReplayTokenResult = {
replayToken: Scalars['String']['output'];
};
export type GenerateSentryTokenResult = {
__typename?: 'GenerateSentryTokenResult';
installationId: Scalars['ID']['output'];
orgSlug: Scalars['String']['output'];
token: Scalars['String']['output'];
};
export type GetSignedAssetUploadSpecificationsResult = {
__typename?: 'GetSignedAssetUploadSpecificationsResult';
specifications: Array<Scalars['String']['output']>;
@@ -4712,7 +4643,6 @@ export type GitHubRepositoryMutation = {
__typename?: 'GitHubRepositoryMutation';
/** Configure EAS by pushing a commit to the default branch which updates or creates app.json, eas.json, and installs necessary dependencies. */
configureEAS: BackgroundJobReceipt;
createAndConfigureRepository: BackgroundJobReceipt;
/** Create a GitHub repository for an App */
createGitHubRepository: GitHubRepository;
/** Delete a GitHub repository by ID */
@@ -4725,11 +4655,6 @@ export type GitHubRepositoryMutationConfigureEasArgs = {
};
export type GitHubRepositoryMutationCreateAndConfigureRepositoryArgs = {
input: CreateAndConfigureRepositoryInput;
};
export type GitHubRepositoryMutationCreateGitHubRepositoryArgs = {
githubRepositoryData: CreateGitHubRepositoryInput;
};
@@ -5114,7 +5039,6 @@ export type IosBuilderEnvironmentInput = {
bun?: InputMaybe<Scalars['String']['input']>;
bundler?: InputMaybe<Scalars['String']['input']>;
cocoapods?: InputMaybe<Scalars['String']['input']>;
corepack?: InputMaybe<Scalars['Boolean']['input']>;
env?: InputMaybe<Scalars['JSONObject']['input']>;
expoCli?: InputMaybe<Scalars['String']['input']>;
fastlane?: InputMaybe<Scalars['String']['input']>;
@@ -5237,7 +5161,6 @@ export type IosSubmissionConfigInput = {
ascApiKey?: InputMaybe<AscApiKeyInput>;
ascApiKeyId?: InputMaybe<Scalars['String']['input']>;
ascAppIdentifier: Scalars['String']['input'];
groups?: InputMaybe<Array<Scalars['String']['input']>>;
isVerboseFastlaneEnabled?: InputMaybe<Scalars['Boolean']['input']>;
};
@@ -5364,34 +5287,12 @@ export type LinkLogRocketOrganizationToExpoAccountInput = {
state: Scalars['String']['input'];
};
export type LinkSentryInstallationToExpoAccountInput = {
accountId: Scalars['ID']['input'];
code: Scalars['String']['input'];
installationId: Scalars['ID']['input'];
sentryOrgSlug: Scalars['String']['input'];
};
export type LinkSharedEnvironmentVariableInput = {
appId: Scalars['ID']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
export type LocalBuildArchiveSourceInput = {
bucketKey: Scalars['String']['input'];
type: LocalBuildArchiveSourceType;
};
export enum LocalBuildArchiveSourceType {
Gcs = 'GCS'
}
export type LocalBuildJobInput = {
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
platform: AppPlatform;
simulator?: InputMaybe<Scalars['Boolean']['input']>;
};
export type LogNameTypeMapping = {
__typename?: 'LogNameTypeMapping';
publicName: Scalars['String']['output'];
@@ -5794,15 +5695,6 @@ export type PaymentDetails = {
id: Scalars['ID']['output'];
};
export type PendingSentryInstallation = {
__typename?: 'PendingSentryInstallation';
account: Account;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
installationId: Scalars['String']['output'];
orgSlug: Scalars['String']['output'];
};
export enum Permission {
Admin = 'ADMIN',
Own = 'OWN',
@@ -5838,6 +5730,7 @@ export enum ProjectArchiveSourceType {
Gcs = 'GCS',
Git = 'GIT',
None = 'NONE',
S3 = 'S3',
Url = 'URL'
}
@@ -5868,7 +5761,6 @@ export type PublicArtifacts = {
};
export type PublishUpdateGroupInput = {
assetMapGroup?: InputMaybe<AssetMapGroup>;
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']['input']>;
branchId: Scalars['String']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
@@ -5905,7 +5797,6 @@ export type RequestsFilters = {
method?: InputMaybe<Array<RequestMethod>>;
os?: InputMaybe<Array<UserAgentOs>>;
pathname?: InputMaybe<Scalars['String']['input']>;
platform?: InputMaybe<Array<UserAgentPlatform>>;
requestId?: InputMaybe<Array<Scalars['WorkerDeploymentRequestID']['input']>>;
responseType?: InputMaybe<Array<ResponseType>>;
status?: InputMaybe<Array<Scalars['Int']['input']>>;
@@ -6054,7 +5945,7 @@ export type RootMutation = {
androidAppBuildCredentials: AndroidAppBuildCredentialsMutation;
/** Mutations that modify the credentials for an Android app */
androidAppCredentials: AndroidAppCredentialsMutation;
/** Mutations that modify an FCM V0/Legacy credential */
/** Mutations that modify an FCM credential */
androidFcm: AndroidFcmMutation;
/** Mutations that modify a Keystore */
androidKeystore: AndroidKeystoreMutation;
@@ -6130,10 +6021,6 @@ export type RootMutation = {
notificationSubscription: NotificationSubscriptionMutation;
/** Mutations that create, update, and delete Robots */
robot: RobotMutation;
/** Mutations for Sentry installations */
sentryInstallation: SentryInstallationMutation;
/** Mutations for Sentry projects */
sentryProject: SentryProjectMutation;
/** Mutations that modify an EAS Submit submission */
submission: SubmissionMutation;
update: UpdateMutation;
@@ -6324,7 +6211,6 @@ export type Runtime = {
fingerprint?: Maybe<Fingerprint>;
firstBuildCreatedAt?: Maybe<Scalars['DateTime']['output']>;
id: Scalars['ID']['output'];
isFingerprint: Scalars['Boolean']['output'];
updatedAt: Scalars['DateTime']['output'];
updates: AppUpdatesConnection;
version: Scalars['String']['output'];
@@ -6378,7 +6264,6 @@ export type RuntimeEdge = {
export type RuntimeFilterInput = {
/** Only return runtimes shared with this branch */
branchId?: InputMaybe<Scalars['String']['input']>;
runtimeVersions?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type RuntimeQuery = {
@@ -6443,7 +6328,6 @@ export type SsoUser = Actor & UserActor & {
preferences: UserPreferences;
/** Associated accounts */
primaryAccount: Account;
primaryAccountProfileImageUrl?: Maybe<Scalars['String']['output']>;
profilePhoto: Scalars['String']['output'];
/** Snacks associated with this account */
snacks: Array<Snack>;
@@ -6539,69 +6423,6 @@ export type SecondFactorRegenerateBackupCodesResult = {
plaintextBackupCodes: Array<Scalars['String']['output']>;
};
export type SentryInstallation = {
__typename?: 'SentryInstallation';
account: Account;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
installationId: Scalars['String']['output'];
orgSlug: Scalars['String']['output'];
};
export type SentryInstallationMutation = {
__typename?: 'SentryInstallationMutation';
/** Confirm a pending Sentry installation */
confirmPendingSentryInstallation: SentryInstallation;
/** Generate a Sentry token for an installation */
generateSentryToken: GenerateSentryTokenResult;
/** Link a Sentry installation to an Expo account */
linkSentryInstallationToExpoAccount: PendingSentryInstallation;
};
export type SentryInstallationMutationConfirmPendingSentryInstallationArgs = {
installationId: Scalars['ID']['input'];
};
export type SentryInstallationMutationGenerateSentryTokenArgs = {
accountId: Scalars['ID']['input'];
};
export type SentryInstallationMutationLinkSentryInstallationToExpoAccountArgs = {
input: LinkSentryInstallationToExpoAccountInput;
};
export type SentryProject = {
__typename?: 'SentryProject';
app: App;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
sentryInstallationId: Scalars['ID']['output'];
sentryProjectId: Scalars['String']['output'];
sentryProjectSlug: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
};
export type SentryProjectMutation = {
__typename?: 'SentryProjectMutation';
/** Create a Sentry project */
createSentryProject: SentryProject;
/** Delete a Sentry project by ID */
deleteSentryProject: DeleteSentryProjectResult;
};
export type SentryProjectMutationCreateSentryProjectArgs = {
input: CreateSentryProjectInput;
};
export type SentryProjectMutationDeleteSentryProjectArgs = {
sentryProjectId: Scalars['ID']['input'];
};
export type Snack = Project & {
__typename?: 'Snack';
/** Description of the Snack */
@@ -6985,7 +6806,6 @@ export type Update = ActivityTimelineProjectActivity & {
activityTimestamp: Scalars['DateTime']['output'];
actor?: Maybe<Actor>;
app: App;
assetMapUrl?: Maybe<Scalars['String']['output']>;
awaitingCodeSigningInfo: Scalars['Boolean']['output'];
branch: UpdateBranch;
branchId: Scalars['ID']['output'];
@@ -7227,7 +7047,6 @@ export type UpdateInfoGroup = {
export type UpdateInsights = {
__typename?: 'UpdateInsights';
averageMetrics: AverageMetrics;
cumulativeMetrics: CumulativeMetrics;
id: Scalars['ID']['output'];
totalUniqueUsers: Scalars['Int']['output'];
@@ -7245,13 +7064,8 @@ export type UpdateInsightsTotalUniqueUsersArgs = {
export type UpdateMutation = {
__typename?: 'UpdateMutation';
/**
* Delete an EAS update group
* @deprecated Use scheduleUpdateGroupDeletion instead
*/
/** Delete an EAS update group */
deleteUpdateGroup: DeleteUpdateGroupResult;
/** Delete an EAS update group in the background */
scheduleUpdateGroupDeletion: BackgroundJobReceipt;
/** Set code signing info for an update */
setCodeSigningInfo: Update;
/** Set rollout percentage for an update */
@@ -7264,11 +7078,6 @@ export type UpdateMutationDeleteUpdateGroupArgs = {
};
export type UpdateMutationScheduleUpdateGroupDeletionArgs = {
group: Scalars['ID']['input'];
};
export type UpdateMutationSetCodeSigningInfoArgs = {
codeSigningInfo: CodeSigningInfoInput;
updateId: Scalars['ID']['input'];
@@ -7345,7 +7154,6 @@ export type UploadSessionCreateAppScopedUploadSessionArgs = {
export type UploadSessionCreateUploadSessionArgs = {
filename?: InputMaybe<Scalars['String']['input']>;
type: UploadSessionType;
};
@@ -7354,11 +7162,9 @@ export enum UploadSessionType {
EasBuildGcsProjectSources = 'EAS_BUILD_GCS_PROJECT_SOURCES',
/** @deprecated Use EAS_BUILD_GCS_PROJECT_SOURCES instead. */
EasBuildProjectSources = 'EAS_BUILD_PROJECT_SOURCES',
EasShareGcsAppArchive = 'EAS_SHARE_GCS_APP_ARCHIVE',
/** @deprecated Use EAS_SUBMIT_GCS_APP_ARCHIVE instead. */
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE',
EasSubmitGcsAppArchive = 'EAS_SUBMIT_GCS_APP_ARCHIVE',
EasUpdateAssetsMetadata = 'EAS_UPDATE_ASSETS_METADATA',
EasUpdateFingerprint = 'EAS_UPDATE_FINGERPRINT'
}
@@ -7446,7 +7252,6 @@ export type User = Actor & UserActor & {
preferences: UserPreferences;
/** Associated accounts */
primaryAccount: Account;
primaryAccountProfileImageUrl?: Maybe<Scalars['String']['output']>;
profilePhoto: Scalars['String']['output'];
/** Get all certified second factor authentication methods */
secondFactorDevices: Array<UserSecondFactorDevice>;
@@ -7552,7 +7357,6 @@ export type UserActor = {
preferences: UserPreferences;
/** Associated accounts */
primaryAccount: Account;
primaryAccountProfileImageUrl?: Maybe<Scalars['String']['output']>;
profilePhoto: Scalars['String']['output'];
/** Snacks associated with this user's personal account */
snacks: Array<Snack>;
@@ -7688,13 +7492,6 @@ export enum UserAgentOs {
Windows = 'WINDOWS'
}
export enum UserAgentPlatform {
Android = 'ANDROID',
Apple = 'APPLE',
Unknown = 'UNKNOWN',
Web = 'WEB'
}
export type UserAppPinMutation = {
__typename?: 'UserAppPinMutation';
pinApp: Scalars['ID']['output'];
@@ -7722,9 +7519,7 @@ export type UserAuditLog = {
targetEntityMutationType: TargetEntityMutationType;
targetEntityTypeName: UserEntityTypeName;
targetEntityTypePublicName: Scalars['String']['output'];
/** @deprecated Use userActor instead */
user: User;
userActor: UserActor;
websiteMessage: Scalars['String']['output'];
};
@@ -8313,7 +8108,6 @@ export type WorkerDeploymentRequestNode = {
method: Scalars['String']['output'];
os?: Maybe<UserAgentOs>;
pathname: Scalars['String']['output'];
platform: UserAgentPlatform;
region?: Maybe<Scalars['String']['output']>;
requestId: Scalars['WorkerDeploymentRequestID']['output'];
requestTimestamp: Scalars['DateTime']['output'];
@@ -8332,8 +8126,6 @@ export type WorkerDeploymentRequests = {
byCountry: Array<WorkerDeploymentRequestsCountryEdge>;
byMethod: Array<WorkerDeploymentRequestsMethodEdge>;
byOS: Array<WorkerDeploymentRequestsOperatingSystemEdge>;
byPathname: Array<WorkerDeploymentRequestsPathnameEdge>;
byPlatform: Array<WorkerDeploymentRequestsPlatformEdge>;
byResponseType: Array<WorkerDeploymentRequestsResponseTypeEdge>;
byStatusType: Array<WorkerDeploymentRequestsStatusTypeEdge>;
interval: Scalars['Int']['output'];
@@ -8380,18 +8172,6 @@ export type WorkerDeploymentRequestsByOsArgs = {
};
export type WorkerDeploymentRequestsByPathnameArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<RequestsOrderBy>;
};
export type WorkerDeploymentRequestsByPlatformArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<RequestsOrderBy>;
};
export type WorkerDeploymentRequestsByResponseTypeArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<RequestsOrderBy>;
@@ -8460,7 +8240,7 @@ export type WorkerDeploymentRequestsCacheStatusEdge = {
export type WorkerDeploymentRequestsContinentEdge = {
__typename?: 'WorkerDeploymentRequestsContinentEdge';
continent?: Maybe<ContinentCode>;
continent: ContinentCode;
node: WorkerDeploymentRequestsAggregationNode;
};
@@ -8482,18 +8262,6 @@ export type WorkerDeploymentRequestsOperatingSystemEdge = {
os?: Maybe<UserAgentOs>;
};
export type WorkerDeploymentRequestsPathnameEdge = {
__typename?: 'WorkerDeploymentRequestsPathnameEdge';
node: WorkerDeploymentRequestsAggregationNode;
pathname: Scalars['String']['output'];
};
export type WorkerDeploymentRequestsPlatformEdge = {
__typename?: 'WorkerDeploymentRequestsPlatformEdge';
node: WorkerDeploymentRequestsAggregationNode;
platform: UserAgentPlatform;
};
export type WorkerDeploymentRequestsResponseTypeEdge = {
__typename?: 'WorkerDeploymentRequestsResponseTypeEdge';
node: WorkerDeploymentRequestsAggregationNode;
@@ -8514,8 +8282,6 @@ export type WorkerDeploymentRequestsTimeseriesEdge = {
byCountry: Array<WorkerDeploymentRequestsCountryEdge>;
byMethod: Array<WorkerDeploymentRequestsMethodEdge>;
byOS: Array<WorkerDeploymentRequestsOperatingSystemEdge>;
byPathname: Array<WorkerDeploymentRequestsPathnameEdge>;
byPlatform: Array<WorkerDeploymentRequestsPlatformEdge>;
byResponseType: Array<WorkerDeploymentRequestsResponseTypeEdge>;
byStatusType: Array<WorkerDeploymentRequestsStatusTypeEdge>;
node?: Maybe<WorkerDeploymentRequestsAggregationNode>;
@@ -8639,11 +8405,9 @@ export enum WorkflowJobType {
Build = 'BUILD',
Custom = 'CUSTOM',
Deploy = 'DEPLOY',
Fingerprint = 'FINGERPRINT',
GetBuild = 'GET_BUILD',
MaestroTest = 'MAESTRO_TEST',
RequireApproval = 'REQUIRE_APPROVAL',
Slack = 'SLACK',
Submission = 'SUBMISSION',
Update = 'UPDATE'
}
@@ -8736,11 +8500,8 @@ export type WorkflowRun = ActivityTimelineProjectActivity & {
requestedGitRef?: Maybe<Scalars['String']['output']>;
retriedWorkflowRun?: Maybe<WorkflowRun>;
retries: Array<WorkflowRun>;
sourceExpiresAt?: Maybe<Scalars['DateTime']['output']>;
status: WorkflowRunStatus;
triggerEventType: WorkflowRunTriggerEventType;
triggeringLabelName?: Maybe<Scalars['String']['output']>;
triggeringSchedule?: Maybe<Scalars['String']['output']>;
updatedAt: Scalars['DateTime']['output'];
workflow: Workflow;
workflowRevision?: Maybe<WorkflowRevision>;
@@ -8808,13 +8569,12 @@ export enum WorkflowRunStatus {
}
export enum WorkflowRunTriggerEventType {
GithubPullRequestLabeled = 'GITHUB_PULL_REQUEST_LABELED',
Github = 'GITHUB',
GithubPullRequestOpened = 'GITHUB_PULL_REQUEST_OPENED',
GithubPullRequestReopened = 'GITHUB_PULL_REQUEST_REOPENED',
GithubPullRequestSynchronize = 'GITHUB_PULL_REQUEST_SYNCHRONIZE',
GithubPush = 'GITHUB_PUSH',
Manual = 'MANUAL',
Schedule = 'SCHEDULE'
Manual = 'MANUAL'
}
export type WorkflowRunsConnection = {
@@ -8932,12 +8692,12 @@ export type AppInfoQueryVariables = Exact<{
export type AppInfoQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, fullName: string } } };
export type ScheduleUpdateGroupDeletionMutationVariables = Exact<{
export type DeleteUpdateGroupMutationVariables = Exact<{
group: Scalars['ID']['input'];
}>;
export type ScheduleUpdateGroupDeletionMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', scheduleUpdateGroupDeletion: { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any } } };
export type DeleteUpdateGroupMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', deleteUpdateGroup: { __typename?: 'DeleteUpdateGroupResult', group: string } } };
export type CreateAndroidAppBuildCredentialsMutationVariables = Exact<{
androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput;
@@ -9372,7 +9132,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -9382,7 +9142,7 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type UpdateBuildMetadataMutationVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -9390,7 +9150,7 @@ export type UpdateBuildMetadataMutationVariables = Exact<{
}>;
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type RetryIosBuildMutationVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -9398,7 +9158,7 @@ export type RetryIosBuildMutationVariables = Exact<{
}>;
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -9492,16 +9252,6 @@ export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]
export type CreateKeystoreGenerationUrlMutation = { __typename?: 'RootMutation', keystoreGenerationUrl: { __typename?: 'KeystoreGenerationUrlMutation', createKeystoreGenerationUrl: { __typename?: 'KeystoreGenerationUrl', id: string, url: string } } };
export type CreateLocalBuildMutationVariables = Exact<{
appId: Scalars['ID']['input'];
jobInput: LocalBuildJobInput;
artifactSource: LocalBuildArchiveSourceInput;
metadata?: InputMaybe<BuildMetadataInput>;
}>;
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
export type GetSignedUploadMutationVariables = Exact<{
contentTypes: Array<Scalars['String']['input']> | Scalars['String']['input'];
}>;
@@ -9554,7 +9304,6 @@ export type CreateIosSubmissionMutation = { __typename?: 'RootMutation', submiss
export type CreateUploadSessionMutationVariables = Exact<{
type: UploadSessionType;
filename?: InputMaybe<Scalars['String']['input']>;
}>;
@@ -9645,13 +9394,6 @@ export type LatestAppVersionQueryVariables = Exact<{
export type LatestAppVersionQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, latestAppVersionByPlatformAndApplicationIdentifier?: { __typename?: 'AppVersion', id: string, storeVersion: string, buildVersion: string } | null } } };
export type BackgroundJobReceiptByIdQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
export type BackgroundJobReceiptByIdQuery = { __typename?: 'RootQuery', backgroundJobReceipt: { __typename?: 'BackgroundJobReceiptQuery', byId: { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any } } };
export type ViewBranchQueryVariables = Exact<{
appId: Scalars['String']['input'];
name: Scalars['String']['input'];
@@ -9705,21 +9447,21 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithFingerprintByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
}>;
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9729,15 +9471,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
}>;
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
export type ViewUpdateChannelBasicInfoOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
channelName: Scalars['String']['input'];
}>;
export type ViewUpdateChannelBasicInfoOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } | null } } };
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9945,13 +9679,11 @@ export type AccountFragment = { __typename?: 'Account', id: string, name: string
export type AppFragment = { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null };
export type BackgroundJobReceiptDataFragment = { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -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,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
}
}
`;
+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`,
},
@@ -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(
@@ -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,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,
-40
View File
@@ -1,6 +1,5 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fg from 'fast-glob';
import semver from 'semver';
import Log from '../../log';
@@ -33,45 +32,6 @@ export async function getXcodeVersionAsync(): Promise<string | undefined> {
}
}
type XcodeBuildSettings = {
action: string;
buildSettings: {
BUILD_DIR: string;
CONFIGURATION_BUILD_DIR: string;
EXECUTABLE_FOLDER_PATH: string;
PRODUCT_BUNDLE_IDENTIFIER: string;
TARGET_BUILD_DIR: string;
UNLOCALIZED_RESOURCES_FOLDER_PATH: string;
};
target: string;
};
export async function getXcodeBuildSettingsAsync(
xcworkspacePath: string,
scheme: string
): Promise<XcodeBuildSettings[]> {
const { stdout } = await spawnAsync('xcodebuild', [
'-workspace',
xcworkspacePath,
'-scheme',
scheme,
'-showBuildSettings',
'-json',
]);
return JSON.parse(stdout);
}
export async function resolveXcodeProjectAsync(projectRoot: string): Promise<string | undefined> {
const ignoredPaths = ['**/@(Carthage|Pods|vendor|node_modules)/**'];
const paths = await fg(`ios/*.xcworkspace`, {
cwd: projectRoot,
onlyDirectories: true,
ignore: ignoredPaths,
});
return paths[0];
}
export async function openAppStoreAsync(appId: string): Promise<void> {
const link = getAppStoreLink(appId);
await spawnAsync(`open`, [link]);
-14
View File
@@ -1,9 +1,6 @@
import path from 'path';
import { runAppOnAndroidEmulatorAsync } from './android/run';
import { runAppOnIosSimulatorAsync } from './ios/run';
import { AppPlatform } from '../graphql/generated';
import { getEasBuildRunCacheDirectoryPath } from '../utils/paths';
export interface RunArchiveFlags {
latest?: boolean;
@@ -22,14 +19,3 @@ export async function runAsync(
await runAppOnAndroidEmulatorAsync(simulatorBuildPath);
}
}
export function getEasBuildRunCachedAppPath(
projectId: string,
buildId: string,
platform: AppPlatform
): string {
return path.join(
getEasBuildRunCacheDirectoryPath(),
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ function isAab(build: BuildFragment): boolean {
}
function didArtifactsExpire(build: BuildFragment): boolean {
return new Date().getTime() > new Date(build.expirationDate).getTime();
return new Date().getTime() - new Date(build.completedAt).getTime() > 30 * 24 * 60 * 60 * 1000; // 30 days
}
export function isRunnableOnSimulatorOrEmulator(build: BuildFragment): boolean {
@@ -3,10 +3,7 @@ import { AndroidConfig, AndroidManifest, XML } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { RequestedPlatform } from '../../platform';
import {
getExpoUpdatesPackageVersionIfInstalledAsync,
isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync,
} from '../../project/projectUtils';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from '../../project/projectUtils';
import { expoUpdatesCommandAsync } from '../../utils/expoUpdatesCli';
import { ensureValidVersions } from '../utils';
@@ -35,16 +32,13 @@ export async function syncUpdatesConfigurationAsync({
return;
}
const expoUpdatesPackageVersion = await getExpoUpdatesPackageVersionIfInstalledAsync(projectDir);
// sync AndroidManifest.xml
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
const androidManifest = await getAndroidManifestAsync(projectDir);
const updatedAndroidManifest = await AndroidConfig.Updates.setUpdatesConfigAsync(
projectDir,
exp,
androidManifest,
expoUpdatesPackageVersion
androidManifest
);
await AndroidConfig.Manifest.writeAndroidManifestAsync(
androidManifestPath,
+1 -3
View File
@@ -371,7 +371,6 @@ export async function ensureEASUpdateIsConfiguredAsync({
vcsClient,
platform,
env,
forceNativeConfigSync,
}: {
exp: ExpoConfig;
projectId: string;
@@ -379,7 +378,6 @@ export async function ensureEASUpdateIsConfiguredAsync({
vcsClient: Client;
platform: RequestedPlatform | null;
env: Env | undefined;
forceNativeConfigSync?: boolean;
}): Promise<void> {
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(
projectDir,
@@ -411,7 +409,7 @@ export async function ensureEASUpdateIsConfiguredAsync({
workflows,
});
if (forceNativeConfigSync || projectChanged || !hasExpoUpdates) {
if (projectChanged || !hasExpoUpdates) {
await ensureEASUpdateIsConfiguredNativelyAsync(vcsClient, {
exp: expWithUpdates,
projectDir,
@@ -3,10 +3,7 @@ import { IOSConfig } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { RequestedPlatform } from '../../platform';
import {
getExpoUpdatesPackageVersionIfInstalledAsync,
isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync,
} from '../../project/projectUtils';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from '../../project/projectUtils';
import { expoUpdatesCommandAsync } from '../../utils/expoUpdatesCli';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
@@ -36,14 +33,12 @@ export async function syncUpdatesConfigurationAsync({
return;
}
const expoUpdatesPackageVersion = await getExpoUpdatesPackageVersionIfInstalledAsync(projectDir);
const expoPlist = await readExpoPlistAsync(projectDir);
// TODO(wschurman): this dependency needs to be updated for fingerprint
const updatedExpoPlist = await IOSConfig.Updates.setUpdatesConfigAsync(
projectDir,
exp,
expoPlist,
expoUpdatesPackageVersion
expoPlist
);
await writeExpoPlistAsync(vcsClient, projectDir, updatedExpoPlist);
}
+1 -5
View File
@@ -20,11 +20,7 @@ export async function uploadFileAtPathToGCSAsync(
path: string,
handleProgressEvent: ProgressHandler = () => {}
): Promise<string> {
const signedUrl = await UploadSessionMutation.createUploadSessionAsync(
graphqlClient,
type,
type === UploadSessionType.EasShareGcsAppArchive ? path : undefined
);
const signedUrl = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
await uploadWithSignedUrlWithProgressAsync(path, signedUrl, handleProgressEvent);
return signedUrl.bucketKey;
@@ -1,5 +1,5 @@
import { abridgedDiff } from '../../fingerprint/diff';
import Log from '../../log';
import { abridgedDiff } from '../fingerprintDiff';
jest.mock('chalk', () => ({
red: jest.fn(text => `red(${text})`),
@@ -22,7 +22,7 @@ describe('abridgedDiff', () => {
abridgedDiff(str1, str2, 1);
const expectedOutput = [
'cyan(@@ -1,3 +1,3 @@)',
'cyan(@@ -2,1 +2,1 @@)',
' gray(Line1)',
'red(-Line2)',
'green(+LineX)',
@@ -106,7 +106,7 @@ describe('abridgedDiff', () => {
abridgedDiff(str1, str2, 1);
const expectedOutput = [
'cyan(@@ -2,3 +2,3 @@)',
'cyan(@@ -3,1 +3,1 @@)',
' gray(Line1)',
'red(-Line2)',
'green(+LineX)',
@@ -1,164 +0,0 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
BackgroundJobReceiptDataFragment,
BackgroundJobResultType,
BackgroundJobState,
} from '../../graphql/generated';
import { BackgroundJobReceiptQuery } from '../../graphql/queries/BackgroundJobReceiptQuery';
import { pollForBackgroundJobReceiptAsync } from '../pollForBackgroundJobReceiptAsync';
jest.mock('../../graphql/queries/BackgroundJobReceiptQuery');
describe(pollForBackgroundJobReceiptAsync, () => {
it('returns successful receipt when polling eventually succeeds', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const receiptId = '123';
const backgroundJobReceiptInProgress: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.InProgress,
willRetry: false,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
const backgroundJobReceiptSuccess: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.Success,
willRetry: false,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
jest
.mocked(BackgroundJobReceiptQuery.byIdAsync)
.mockResolvedValueOnce(backgroundJobReceiptInProgress)
.mockResolvedValueOnce(backgroundJobReceiptInProgress)
.mockResolvedValueOnce(backgroundJobReceiptSuccess);
const result = await pollForBackgroundJobReceiptAsync(
graphqlClient,
backgroundJobReceiptInProgress,
{ pollInterval: 100 }
);
expect(result).toEqual(backgroundJobReceiptSuccess);
});
it('throws error when polling eventually fails', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const receiptId = '123';
const backgroundJobReceiptInProgress: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.InProgress,
willRetry: false,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
const backgroundJobReceiptFailure: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.Failure,
errorMessage: 'Watch out for falling rocks!',
willRetry: false,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
jest
.mocked(BackgroundJobReceiptQuery.byIdAsync)
.mockResolvedValueOnce(backgroundJobReceiptInProgress)
.mockResolvedValueOnce(backgroundJobReceiptInProgress)
.mockResolvedValueOnce(backgroundJobReceiptFailure);
await expect(
pollForBackgroundJobReceiptAsync(graphqlClient, backgroundJobReceiptInProgress, {
pollInterval: 100,
})
).rejects.toThrow('Background job failed with error: Watch out for falling rocks!');
});
it('succeeds when polling eventually succeeds after a failure with retry', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const receiptId = '123';
const backgroundJobReceiptInProgress: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.InProgress,
willRetry: false,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
const backgroundJobReceiptFailureWithRetry: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.Failure,
errorMessage: 'Watch out for falling rocks!',
willRetry: true,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
const backgroundJobReceiptInProgress2: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.InProgress,
willRetry: true,
tries: 1,
resultType: BackgroundJobResultType.Void,
} as any;
const backgroundJobReceiptSuccess: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.Success,
willRetry: true,
tries: 1,
resultType: BackgroundJobResultType.Void,
} as any;
jest
.mocked(BackgroundJobReceiptQuery.byIdAsync)
.mockResolvedValueOnce(backgroundJobReceiptInProgress)
.mockResolvedValueOnce(backgroundJobReceiptInProgress)
.mockResolvedValueOnce(backgroundJobReceiptFailureWithRetry)
.mockResolvedValueOnce(backgroundJobReceiptInProgress2)
.mockResolvedValueOnce(backgroundJobReceiptSuccess);
const result = await pollForBackgroundJobReceiptAsync(
graphqlClient,
backgroundJobReceiptInProgress,
{ pollInterval: 100 }
);
expect(result).toEqual(backgroundJobReceiptSuccess);
});
it('times out after 90 checks', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
const receiptId = '123';
const backgroundJobReceiptInProgress: BackgroundJobReceiptDataFragment = {
id: receiptId,
state: BackgroundJobState.InProgress,
willRetry: false,
tries: 0,
resultType: BackgroundJobResultType.Void,
} as any;
jest
.mocked(BackgroundJobReceiptQuery.byIdAsync)
.mockResolvedValue(backgroundJobReceiptInProgress);
await expect(
pollForBackgroundJobReceiptAsync(graphqlClient, backgroundJobReceiptInProgress, {
pollInterval: 10,
})
).rejects.toThrow('Background job timed out.');
});
});
@@ -1,10 +1,10 @@
import { Env, Workflow } from '@expo/eas-build-job';
import { silent as silentResolveFrom } from 'resolve-from';
import { Fingerprint, FingerprintDiffItem } from './types';
import mapMapAsync from './expodash/mapMapAsync';
import { Fingerprint, FingerprintDiffItem } from './fingerprint';
import Log from '../log';
import { ora } from '../ora';
import mapMapAsync from '../utils/expodash/mapMapAsync';
export type FingerprintOptions = {
workflow?: Workflow;
@@ -103,21 +103,8 @@ async function createFingerprintWithoutLoggingAsync(
if (options.debug) {
fingerprintOptions.debug = true;
}
return await withTemporaryEnvAsync(options.env ?? {}, () =>
Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions)
);
}
async function withTemporaryEnvAsync(envVars: Env, fn: () => Promise<any>): Promise<any> {
const originalEnv = { ...process.env };
Object.assign(process.env, envVars);
try {
return await fn();
} finally {
process.env = originalEnv;
}
// eslint-disable-next-line @typescript-eslint/return-await
return await Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions);
}
/**
@@ -52,13 +52,8 @@ export function abridgedDiff(str1: string, str2: string, contextLines: number =
const flushChunk = (): void => {
if (currentChunk.length > 0) {
const contextLines = currentChunkPriorContext.length + currentChunkAfterContext.length;
const originalRange = `${(startOriginal ?? 1) - currentChunkPriorContext.length},${
contextLines + removedLines
}`;
const modifiedRange = `${(startModified ?? 1) - currentChunkPriorContext.length},${
contextLines + addedLines
}`;
const originalRange = `${startOriginal},${removedLines || 0}`;
const modifiedRange = `${startModified},${addedLines || 0}`;
// `git diff` style header
output.push(chalk.cyan(`@@ -${originalRange} +${modifiedRange} @@`));
output.push(...currentChunkPriorContext);
@@ -10,10 +10,6 @@ export default function formatFields(
fields: FormatFieldsItem[],
options: FormatFieldsOptions = { labelFormat: chalk.dim }
): string {
if (fields.length === 0) {
return '';
}
const columnWidth = fields.reduce((a, b) => (a.label.length > b.label.length ? a : b)).label
.length;
@@ -1,148 +0,0 @@
import { CombinedError } from '@urql/core';
import { clearIntervalAsync, setIntervalAsync } from 'set-interval-async';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { BackgroundJobReceiptDataFragment, BackgroundJobState } from '../graphql/generated';
import { BackgroundJobReceiptQuery } from '../graphql/queries/BackgroundJobReceiptQuery';
export enum BackgroundJobReceiptPollErrorType {
NULL_RECEIPT,
JOB_FAILED_NO_WILL_RETRY,
TIMEOUT,
}
export type BackgroundJobReceiptPollErrorData =
| {
errorType: BackgroundJobReceiptPollErrorType.NULL_RECEIPT;
}
| {
errorType: BackgroundJobReceiptPollErrorType.JOB_FAILED_NO_WILL_RETRY;
receiptErrorMessage: string | undefined | null;
}
| {
errorType: BackgroundJobReceiptPollErrorType.TIMEOUT;
};
export class BackgroundJobReceiptPollError extends Error {
readonly errorData: BackgroundJobReceiptPollErrorData;
constructor(errorData: BackgroundJobReceiptPollErrorData) {
super(BackgroundJobReceiptPollError.createErrorMessage(errorData));
this.errorData = errorData;
}
static createErrorMessage(errorData: BackgroundJobReceiptPollErrorData): string {
switch (errorData.errorType) {
case BackgroundJobReceiptPollErrorType.NULL_RECEIPT:
return 'Background job receipt was null.';
case BackgroundJobReceiptPollErrorType.JOB_FAILED_NO_WILL_RETRY:
return `Background job failed with error: ${errorData.receiptErrorMessage}`;
case BackgroundJobReceiptPollErrorType.TIMEOUT:
return 'Background job timed out.';
}
}
}
export type BackgroundJobPollErrorCondition = (error: CombinedError) => {
errorIndicatesSuccess: boolean;
};
async function fetchBackgroundJobReceiptAsync(
graphqlClient: ExpoGraphqlClient,
receiptId: string
): Promise<[BackgroundJobReceiptDataFragment | null, CombinedError | null]> {
try {
return [await BackgroundJobReceiptQuery.byIdAsync(graphqlClient, receiptId), null];
} catch (error) {
if (error instanceof CombinedError) {
return [null, error];
}
throw error;
}
}
export function pollForBackgroundJobReceiptAsync(
graphqlClient: ExpoGraphqlClient,
backgroundJobReceipt: BackgroundJobReceiptDataFragment
): Promise<BackgroundJobReceiptDataFragment>;
export function pollForBackgroundJobReceiptAsync(
graphqlClient: ExpoGraphqlClient,
backgroundJobReceipt: BackgroundJobReceiptDataFragment,
options?: {
onBackgroundJobReceiptPollError?: BackgroundJobPollErrorCondition;
pollInterval?: number;
}
): Promise<BackgroundJobReceiptDataFragment | null>;
export async function pollForBackgroundJobReceiptAsync(
graphqlClient: ExpoGraphqlClient,
backgroundJobReceipt: BackgroundJobReceiptDataFragment,
options?: {
onBackgroundJobReceiptPollError?: BackgroundJobPollErrorCondition;
pollInterval?: number;
}
): Promise<BackgroundJobReceiptDataFragment | null> {
return await new Promise<BackgroundJobReceiptDataFragment | null>((resolve, reject) => {
let numChecks = 0;
const intervalHandle = setIntervalAsync(
async function pollForDeletionFinishedAsync() {
function failBackgroundDeletion(error: BackgroundJobReceiptPollError): void {
void clearIntervalAsync(intervalHandle);
reject(error);
}
const [receipt, error] = await fetchBackgroundJobReceiptAsync(
graphqlClient,
backgroundJobReceipt.id
);
if (!receipt) {
if (error instanceof CombinedError) {
const errorResult = options?.onBackgroundJobReceiptPollError?.(error);
if (errorResult?.errorIndicatesSuccess) {
void clearIntervalAsync(intervalHandle);
resolve(null);
return;
}
}
failBackgroundDeletion(
new BackgroundJobReceiptPollError({
errorType: BackgroundJobReceiptPollErrorType.NULL_RECEIPT,
})
);
return;
}
// job failed and will not retry
if (receipt.state === BackgroundJobState.Failure && !receipt.willRetry) {
failBackgroundDeletion(
new BackgroundJobReceiptPollError({
errorType: BackgroundJobReceiptPollErrorType.JOB_FAILED_NO_WILL_RETRY,
receiptErrorMessage: receipt.errorMessage,
})
);
return;
}
// all else fails, stop polling after 90 checks. This should only happen if there's an
// issue with receipts not setting `willRetry` to false when they fail within a reasonable
// amount of time.
if (numChecks > 90) {
failBackgroundDeletion(
new BackgroundJobReceiptPollError({
errorType: BackgroundJobReceiptPollErrorType.TIMEOUT,
})
);
return;
}
if (receipt.state === BackgroundJobState.Success) {
void clearIntervalAsync(intervalHandle);
resolve(receipt);
return;
}
numChecks++;
},
options?.pollInterval ?? 1000
);
});
}
@@ -18,7 +18,8 @@ describe(Ignore, () => {
'/root'
);
const ignore = await Ignore.createForCopyingAsync('/root');
const ignore = new Ignore('/root');
await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('dir/aaa')).toBe(true);
@@ -35,7 +36,8 @@ describe(Ignore, () => {
'/root'
);
const ignore = await Ignore.createForCopyingAsync('/root');
const ignore = new Ignore('/root');
await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(false);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('ccc')).toBe(true);
@@ -52,7 +54,8 @@ describe(Ignore, () => {
'/root'
);
const ignore = await Ignore.createForCopyingAsync('/root');
const ignore = new Ignore('/root');
await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('node_modules/aaa')).toBe(true);
@@ -69,36 +72,18 @@ describe(Ignore, () => {
'/root'
);
const ignore = await Ignore.createForCopyingAsync('/root');
const ignore = new Ignore('/root');
await ignore.initIgnoreAsync();
expect(ignore.ignores('dir/ccc')).toBe(true);
});
it('ignores .git if copying', async () => {
it('ignores .git', async () => {
vol.fromJSON({}, '/root');
const ignore = await Ignore.createForCopyingAsync('/root');
const ignore = new Ignore('/root');
await ignore.initIgnoreAsync();
expect(ignore.ignores('.git')).toBe(true);
});
describe('for checking', () => {
it('does not necessarily ignore .git', async () => {
vol.fromJSON({}, '/root');
const ignore = await Ignore.createForCheckingAsync('/root');
expect(ignore.ignores('.git')).toBe(false);
});
it('ignores .git if present in .easignore', async () => {
vol.fromJSON(
{
'.easignore': '.git\n',
},
'/root'
);
const ignore = await Ignore.createForCheckingAsync('/root');
expect(ignore.ignores('.git')).toBe(true);
});
});
it('does not throw an error if there is a trailing backslash in the gitignore', async () => {
vol.fromJSON(
@@ -108,7 +93,8 @@ describe(Ignore, () => {
'/root'
);
const ignore = await Ignore.createForCopyingAsync('/root');
const ignore = new Ignore('/root');
await ignore.initIgnoreAsync();
expect(() => ignore.ignores('dir/test')).not.toThrowError();
});
});
@@ -1,333 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import Log from '../../../log';
import GitClient from '../git';
describe('git', () => {
describe('GitClient that does not require a commit', () => {
let vcs: GitClient;
let repoRoot: string;
afterAll(async () => {
await fs.rm(repoRoot, { recursive: true, force: true });
});
beforeAll(async () => {
repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});
});
describe('isFileIgnoredAsync', () => {
const testFiles = [
{ commit: true, gitignore: true, easignore: true },
{ commit: true, gitignore: true, easignore: false },
{ commit: true, gitignore: false, easignore: true },
{ commit: true, gitignore: false, easignore: false },
{ commit: false, gitignore: true, easignore: true },
{ commit: false, gitignore: true, easignore: false },
{ commit: false, gitignore: false, easignore: true },
{ commit: false, gitignore: false, easignore: false },
].map(combo => {
const filename = `${combo.commit ? 'tracked' : 'new'}-${
combo.gitignore ? 'gitignored' : 'gitnonignored'
}-${combo.easignore ? 'easignored' : 'easnonignored'}.txt`;
return { ...combo, filename };
});
async function setupTestFiles(): Promise<void> {
await Promise.all(
testFiles.map(async file => {
const content = `File that is ${Object.values(file).join(', ')}`;
await fs.writeFile(`${repoRoot}/${file.filename}`, content);
})
);
// Commit the "committed" files
await spawnAsync('git', ['add', '*tracked*.txt'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'test setup'], { cwd: repoRoot });
}
beforeAll(async () => {
await setupTestFiles();
});
describe('with only .easignore', () => {
beforeAll(async () => {
await fs.writeFile(`${repoRoot}/.easignore`, '*easignored*\n');
});
afterAll(async () => {
await fs.rm(`${repoRoot}/.easignore`);
});
it.each(testFiles.filter(file => file.easignore))(
'$filename should be ignored',
async file => {
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(true);
}
);
it.each(testFiles.filter(file => !file.easignore))(
'$filename should not be ignored',
async file => {
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(false);
}
);
});
describe('with only .gitignore', () => {
beforeAll(async () => {
await fs.writeFile(`${repoRoot}/.gitignore`, '*gitignored*\n');
});
afterAll(async () => {
await fs.rm(`${repoRoot}/.gitignore`);
});
it.each(testFiles.filter(file => file.gitignore && !file.commit))(
'$filename should be ignored',
async file => {
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(true);
}
);
it.each(testFiles.filter(file => !file.gitignore || file.commit))(
'$filename should not be ignored',
async file => {
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(false);
}
);
});
describe('with both .gitignore and .easignore', () => {
beforeAll(async () => {
await fs.writeFile(`${repoRoot}/.gitignore`, '*gitignored*\n');
await fs.writeFile(`${repoRoot}/.easignore`, '*easignored*\n');
});
afterAll(async () => {
await fs.rm(`${repoRoot}/.gitignore`);
await fs.rm(`${repoRoot}/.easignore`);
});
it.each(testFiles.filter(file => file.easignore))(
'$filename should be ignored',
async file => {
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(true);
}
);
it.each(testFiles.filter(file => !file.easignore))(
'$filename should not be ignored',
async file => {
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(false);
}
);
});
});
it('is able to delete a submodule ignored by .easignore', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});
await spawnAsync(
'git',
['submodule', 'add', 'https://github.com/expo/results.git', 'results'],
{ cwd: repoRoot }
);
await spawnAsync('git', ['add', 'results'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'add submodule'], { cwd: repoRoot });
const repoCloneNonIgnored = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoCloneNonIgnored)).resolves.not.toThrow();
await expect(fs.stat(path.join(repoCloneNonIgnored, 'results'))).resolves.not.toThrow();
await fs.writeFile(`${repoRoot}/.easignore`, 'results');
const repoCloneIgnored = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoCloneIgnored)).resolves.not.toThrow();
await expect(fs.stat(path.join(repoCloneIgnored, 'results'))).rejects.toThrow('ENOENT');
});
it('if .git is present in .easignore', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});
await fs.writeFile(`${repoRoot}/.easignore`, '.git\n');
await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'temp commit'], { cwd: repoRoot });
const repoClone = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoClone)).resolves.not.toThrow();
await expect(fs.stat(path.join(repoClone, '.git'))).rejects.toThrow('ENOENT');
});
it('if .git is not present in .easignore', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});
await fs.writeFile(`${repoRoot}/.easignore`, '');
await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'temp commit'], { cwd: repoRoot });
const repoClone = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoClone)).resolves.not.toThrow();
await expect(fs.readdir(path.join(repoClone, '.git'))).resolves.toBeDefined();
});
});
it('does not include files that have been removed in the working directory', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});
await fs.writeFile(`${repoRoot}/committed-file.txt`, 'file');
await fs.writeFile(`${repoRoot}/file-to-remove.txt`, 'file');
await spawnAsync('git', ['add', 'committed-file.txt', 'file-to-remove.txt'], {
cwd: repoRoot,
});
await spawnAsync('git', ['commit', '-m', 'add files'], { cwd: repoRoot });
await fs.rm(`${repoRoot}/file-to-remove.txt`);
await spawnAsync('git', ['add', 'file-to-remove.txt'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'remove file'], { cwd: repoRoot });
await fs.writeFile(`${repoRoot}/new-file.txt`, 'file');
await fs.writeFile(`${repoRoot}/new-tracked-file.txt`, 'file');
const repoClone = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoClone)).resolves.not.toThrow();
await expect(fs.stat(path.join(repoClone, 'file-to-remove.txt'))).rejects.toThrow('ENOENT');
await expect(fs.stat(path.join(repoClone, 'committed-file.txt'))).resolves.not.toThrow();
await expect(fs.stat(path.join(repoClone, 'new-file.txt'))).resolves.not.toThrow();
await expect(fs.stat(path.join(repoClone, 'new-tracked-file.txt'))).resolves.not.toThrow();
vcs.requireCommit = true;
await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'tmp commit'], { cwd: repoRoot });
const requireCommitClone = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(requireCommitClone)).resolves.not.toThrow();
await expect(fs.stat(path.join(requireCommitClone, 'file-to-remove.txt'))).rejects.toThrow(
'ENOENT'
);
await expect(
fs.stat(path.join(requireCommitClone, 'committed-file.txt'))
).resolves.not.toThrow();
await expect(fs.stat(path.join(requireCommitClone, 'new-file.txt'))).resolves.not.toThrow();
await expect(
fs.stat(path.join(requireCommitClone, 'new-tracked-file.txt'))
).resolves.not.toThrow();
});
describe('when requireCommit is true', () => {
it('adheres to .easignore', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: true,
maybeCwdOverride: repoRoot,
});
const warn = jest.spyOn(Log, 'warn');
await fs.writeFile(`${repoRoot}/.easignore`, '*easignored*\n');
await fs.writeFile(`${repoRoot}/.gitignore`, '*gitignored*\n');
await fs.writeFile(`${repoRoot}/easignored-file.txt`, 'file');
await fs.writeFile(`${repoRoot}/nonignored-file.txt`, 'file');
await fs.writeFile(`${repoRoot}/gitignored-file.txt`, 'file');
await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'tmp commit'], { cwd: repoRoot });
const copyRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(copyRoot)).resolves.not.toThrow();
expect(warn).toHaveBeenCalledWith(expect.stringContaining('.easignore'));
await expect(fs.stat(path.join(copyRoot, 'easignored-file.txt'))).rejects.toThrow('ENOENT');
await expect(fs.stat(path.join(copyRoot, 'gitignored-file.txt'))).rejects.toThrow('ENOENT');
await expect(fs.stat(path.join(copyRoot, 'nonignored-file.txt'))).resolves.not.toThrow();
});
it('prints a warning only once if .easignore exists', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: true,
maybeCwdOverride: repoRoot,
});
const warn = jest.spyOn(Log, 'warn');
await fs.writeFile(`${repoRoot}/.easignore`, '*easignored*\n');
await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'tmp commit'], { cwd: repoRoot });
const copyRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(copyRoot)).resolves.not.toThrow();
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(expect.stringContaining('.easignore'));
const anotherCopyRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(anotherCopyRoot)).resolves.not.toThrow();
expect(warn).toHaveBeenCalledTimes(1);
});
describe('when EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING is set', () => {
beforeAll(() => {
process.env.EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING = '1';
});
afterAll(() => {
delete process.env.EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING;
});
it('does not print a warning', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: true,
maybeCwdOverride: repoRoot,
});
const warn = jest.spyOn(Log, 'warn');
warn.mockClear();
await fs.writeFile(`${repoRoot}/.easignore`, '*easignored*\n');
await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'tmp commit'], { cwd: repoRoot });
const copyRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(copyRoot)).resolves.not.toThrow();
expect(warn).toHaveBeenCalledTimes(0);
});
});
});
});
@@ -1,44 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import NoVcsClient from '../noVcs';
describe('noVcs', () => {
describe('NoVcsClient', () => {
let vcs: NoVcsClient;
let repoRoot: string;
let globalEasProjectRoot: string | undefined;
afterEach(async () => {
await fs.rm(repoRoot, { recursive: true, force: true });
process.env.EAS_PROJECT_ROOT = globalEasProjectRoot;
});
beforeEach(async () => {
repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
vcs = new NoVcsClient({ cwdOverride: repoRoot });
globalEasProjectRoot = process.env.EAS_PROJECT_ROOT;
delete process.env.EAS_PROJECT_ROOT;
});
it('should return the current working directory when not in Git repository', async () => {
expect(await vcs.getRootPathAsync()).toBe(process.cwd());
});
it('should return the Git root when in Git repository', async () => {
await spawnAsync('git', ['init'], { cwd: repoRoot });
expect(await fs.realpath(await vcs.getRootPathAsync())).toBe(await fs.realpath(repoRoot));
});
it('should return the project root when EAS_PROJECT_ROOT is set', async () => {
process.env.EAS_PROJECT_ROOT = 'project-root';
expect(await vcs.getRootPathAsync()).toBe(path.resolve(process.cwd(), 'project-root'));
process.env.EAS_PROJECT_ROOT = '/app';
expect(await vcs.getRootPathAsync()).toBe('/app');
});
});
});
+32 -206
View File
@@ -2,8 +2,6 @@ import * as PackageManagerUtils from '@expo/package-manager';
import spawnAsync from '@expo/spawn-async';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
import getenv from 'getenv';
import path from 'path';
import Log, { learnMore } from '../../log';
@@ -16,19 +14,11 @@ import {
gitStatusAsync,
isGitInstalledAsync,
} from '../git';
import { EASIGNORE_FILENAME, Ignore, makeShallowCopyAsync } from '../local';
import { Client } from '../vcs';
let hasWarnedAboutEasignoreInRequireCommit = false;
export default class GitClient extends Client {
private readonly maybeCwdOverride?: string;
public requireCommit: boolean;
constructor(options: { maybeCwdOverride?: string; requireCommit: boolean }) {
constructor(private readonly maybeCwdOverride?: string) {
super();
this.maybeCwdOverride = options.maybeCwdOverride;
this.requireCommit = options.requireCommit;
}
public override async ensureRepoExistsAsync(): Promise<void> {
@@ -129,6 +119,10 @@ export default class GitClient extends Client {
}
}
public override async isCommitRequiredAsync(): Promise<boolean> {
return await this.hasUncommittedChangesAsync();
}
public override async showChangedFilesAsync(): Promise<void> {
const gitStatusOutput = await gitStatusAsync({
showUntracked: true,
@@ -150,133 +144,49 @@ export default class GitClient extends Client {
).stdout.trim();
}
public override async isCommitRequiredAsync(): Promise<boolean> {
if (!this.requireCommit) {
return false;
}
return await this.hasUncommittedChangesAsync();
}
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
if (await this.isCommitRequiredAsync()) {
if (await this.hasUncommittedChangesAsync()) {
// it should already be checked before this function is called, but in case it wasn't
// we want to ensure that any changes were introduced by call to `setGitCaseSensitivityAsync`
throw new Error('You have some uncommitted changes in your repository.');
}
const rootPath = await this.getRootPathAsync();
const sourceEasignorePath = path.join(rootPath, EASIGNORE_FILENAME);
const doesEasignoreExist = await fs.exists(sourceEasignorePath);
const shouldSuppressWarning =
hasWarnedAboutEasignoreInRequireCommit ||
getenv.boolish('EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING', false);
if (this.requireCommit && doesEasignoreExist && !shouldSuppressWarning) {
Log.warn(
`You have "requireCommit: true" in "eas.json" and also ".easignore". If ".easignore" does remove files, note that the repository checked out in EAS will not longer be Git-clean.`
);
hasWarnedAboutEasignoreInRequireCommit = true;
}
let gitRepoUri;
if (process.platform === 'win32') {
// getRootDirectoryAsync() will return C:/path/to/repo on Windows and path
// prefix should be file:///
gitRepoUri = `file:///${rootPath}`;
gitRepoUri = `file:///${await this.getRootPathAsync()}`;
} else {
// getRootDirectoryAsync() will /path/to/repo, and path prefix should be
// file:/// so only file:// needs to be prepended
gitRepoUri = `file://${rootPath}`;
gitRepoUri = `file://${await this.getRootPathAsync()}`;
}
await assertEnablingGitCaseSensitivityDoesNotCauseNewUncommittedChangesAsync(rootPath);
const isCaseSensitive = await isGitCaseSensitiveAsync(rootPath);
const isCaseSensitive = await isGitCaseSensitiveAsync(this.maybeCwdOverride);
await setGitCaseSensitivityAsync(true, this.maybeCwdOverride);
try {
await setGitCaseSensitivityAsync(true, rootPath);
if (await this.hasUncommittedChangesAsync()) {
Log.error('Detected inconsistent filename casing between your local filesystem and git.');
Log.error('This will likely cause your build to fail. Impacted files:');
await spawnAsync('git', ['status', '--short'], {
stdio: 'inherit',
cwd: this.maybeCwdOverride,
});
Log.newLine();
Log.error(
`Error: Resolve filename casing inconsistencies before proceeding. ${learnMore(
'https://expo.fyi/macos-ignorecase'
)}`
);
throw new Error('You have some uncommitted changes in your repository.');
}
await spawnAsync(
'git',
[
'clone',
// If we do not require a commit, we are going to later
// copy the working directory into the destination path,
// so we can skip the checkout step (which also adds files
// that have been removed in the working directory).
this.requireCommit ? null : '--no-checkout',
'--no-hardlinks',
'--depth',
'1',
gitRepoUri,
destinationPath,
].flatMap(e => e ?? []),
{ cwd: rootPath }
);
const sourceEasignorePath = path.join(rootPath, EASIGNORE_FILENAME);
if (await fs.exists(sourceEasignorePath)) {
Log.debug('.easignore exists, deleting files that should be ignored', {
sourceEasignorePath,
});
const cachedFilesWeShouldHaveIgnored = (
await spawnAsync(
'git',
[
'ls-files',
'--exclude-from',
sourceEasignorePath,
// `--ignored --cached` makes git print files that should be
// ignored by rules from `--exclude-from`, but instead are currently cached.
'--ignored',
'--cached',
// separates file names with null characters
'-z',
],
{ cwd: destinationPath }
)
).stdout
.split('\0')
// ls-files' output is terminated by a null character
.filter(file => file !== '');
Log.debug('cachedFilesWeShouldHaveIgnored', {
cachedFilesWeShouldHaveIgnored,
});
await Promise.all(
cachedFilesWeShouldHaveIgnored.map(file =>
// `ls-files` does not go over files within submodules. If submodule is
// ignored, it is listed as a single path, so we need to `rm -rf` it.
fs.rm(path.join(destinationPath, file), { recursive: true, force: true })
)
);
// Special-case `.git` which `git ls-files` will never consider ignored.
// We don't want to ignore anything by default. We want to know what does
// the user want to ignore.
const ignore = await Ignore.createForCheckingAsync(rootPath);
if (ignore.ignores('.git')) {
await fs.rm(path.join(destinationPath, '.git'), { recursive: true, force: true });
Log.debug('deleted .git', {
destinationPath,
});
['clone', '--no-hardlinks', '--depth', '1', gitRepoUri, destinationPath],
{
cwd: this.maybeCwdOverride,
}
}
);
} finally {
await setGitCaseSensitivityAsync(isCaseSensitive, rootPath);
}
if (!this.requireCommit) {
Log.debug('making shallow copy', { requireCommit: this.requireCommit });
// After we create the shallow Git copy, we copy the files
// again. This way we include the changed and untracked files
// (`git clone` only copies the committed changes).
//
// We only do this if `requireCommit` is false because `requireCommit: true`
// setups expect no changes in files (e.g. locked files should remain locked).
await makeShallowCopyAsync(rootPath, destinationPath);
} else {
Log.debug('not making shallow copy', { requireCommit: this.requireCommit });
await setGitCaseSensitivityAsync(isCaseSensitive, this.maybeCwdOverride);
}
}
@@ -341,47 +251,11 @@ export default class GitClient extends Client {
);
}
/** NOTE: This method does not support checking whether `.git` is ignored by `.easignore` rules. */
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
const rootPath = await this.getRootPathAsync();
let isTracked: boolean;
try {
await spawnAsync('git', ['ls-files', '--error-unmatch', filePath], {
cwd: rootPath,
await spawnAsync('git', ['check-ignore', '-q', filePath], {
cwd: this.maybeCwdOverride ?? path.normalize(await this.getRootPathAsync()),
});
isTracked = true;
} catch {
isTracked = false;
}
const easIgnorePath = path.join(rootPath, EASIGNORE_FILENAME);
if (await fs.exists(easIgnorePath)) {
const ignore = await Ignore.createForCheckingAsync(rootPath);
const wouldNotBeCopiedToClone = ignore.ignores(filePath);
const wouldBeDeletedFromClone =
(
await spawnAsync(
'git',
['ls-files', '--exclude-from', easIgnorePath, '--ignored', '--cached', filePath],
{ cwd: rootPath }
)
).stdout.trim() !== '';
// File is considered ignored if:
// - makeShallowCopyAsync() will not copy it to the clone
// AND
// - it will not be copied to the clone because it's not tracked
// - or it will get copied to the clone, but then will be deleted by .easignore rules
return wouldNotBeCopiedToClone && (!isTracked || wouldBeDeletedFromClone);
}
if (isTracked) {
return false; // Tracked files aren't ignored even if they match ignore patterns
}
try {
await spawnAsync('git', ['check-ignore', '-q', filePath], { cwd: rootPath });
return true;
} catch {
return false;
@@ -512,51 +386,3 @@ async function setGitCaseSensitivityAsync(
});
}
}
async function assertEnablingGitCaseSensitivityDoesNotCauseNewUncommittedChangesAsync(
cwd: string
): Promise<void> {
// Remember uncommited changes before case sensitivity change
// for later comparison so we log to the user only the files
// that were marked as changed after the case sensitivity change.
const uncommittedChangesBeforeCaseSensitivityChange = await gitStatusAsync({
showUntracked: true,
cwd,
});
const isCaseSensitive = await isGitCaseSensitiveAsync(cwd);
await setGitCaseSensitivityAsync(true, cwd);
try {
const uncommitedChangesAfterCaseSensitivityChange = await gitStatusAsync({
showUntracked: true,
cwd,
});
if (
uncommitedChangesAfterCaseSensitivityChange !== uncommittedChangesBeforeCaseSensitivityChange
) {
const baseUncommitedChangesSet = new Set(
uncommittedChangesBeforeCaseSensitivityChange.split('\n')
);
const errorMessage = [
'Detected inconsistent filename casing between your local filesystem and git.',
'This will likely cause your job to fail. Impacted files:',
...uncommitedChangesAfterCaseSensitivityChange.split('\n').flatMap(changedFile => {
// This file was changed before the case sensitivity change too.
if (baseUncommitedChangesSet.has(changedFile)) {
return [];
}
return [changedFile];
}),
`Resolve filename casing inconsistencies before proceeding. ${learnMore(
'https://expo.fyi/macos-ignorecase'
)}`,
];
throw new Error(errorMessage.join('\n'));
}
} finally {
await setGitCaseSensitivityAsync(isCaseSensitive, cwd);
}
}
@@ -0,0 +1,45 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import path from 'path';
import GitClient from './git';
import Log from '../../log';
import { Ignore, makeShallowCopyAsync } from '../local';
export default class GitNoCommitClient extends GitClient {
public override async isCommitRequiredAsync(): Promise<boolean> {
return false;
}
public override async getRootPathAsync(): Promise<string> {
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
}
public override async makeShallowCopyAsync(destinationPath: string): Promise<void> {
// normalize converts C:/some/path to C:\some\path on windows
const srcPath = path.normalize(await this.getRootPathAsync());
await makeShallowCopyAsync(srcPath, destinationPath);
}
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
// normalize converts C:/some/path to C:\some\path on windows
const rootPath = path.normalize(await this.getRootPathAsync());
const ignore = new Ignore(rootPath);
await ignore.initIgnoreAsync();
return ignore.ignores(filePath);
}
public override async trackFileAsync(file: string): Promise<void> {
try {
await super.trackFileAsync(file);
} catch {
// In the no commit workflow it doesn't matter if we fail to track changes,
// so we can ignore if this throws an exception
Log.warn(
`Unable to track ${chalk.bold(path.basename(file))} in Git. Proceeding without tracking.`
);
Log.warn(` Reason: the command ${chalk.bold(`"git add ${file}"`)} exited with an error.`);
Log.newLine();
}
}
}
+5 -44
View File
@@ -1,58 +1,19 @@
import spawnAsync from '@expo/spawn-async';
import path from 'path';
import Log from '../../log';
import { Ignore, makeShallowCopyAsync } from '../local';
import { Ignore, getRootPath, makeShallowCopyAsync } from '../local';
import { Client } from '../vcs';
let hasWarnedAboutEasProjectRoot = false;
export default class NoVcsClient extends Client {
private readonly cwdOverride?: string;
constructor(options: { cwdOverride?: string } = {}) {
super();
this.cwdOverride = options.cwdOverride;
}
public async getRootPathAsync(): Promise<string> {
// If EAS_PROJECT_ROOT is absolute, return it.
// If it is relative or empty, resolve it from Git root or process.cwd().
// Honor `EAS_PROJECT_ROOT` if it is set.
if (process.env.EAS_PROJECT_ROOT && path.isAbsolute(process.env.EAS_PROJECT_ROOT)) {
return path.normalize(process.env.EAS_PROJECT_ROOT);
}
// If `EAS_PROJECT_ROOT` is not set, try to get the root path from Git.
try {
return (
await spawnAsync('git', ['rev-parse', '--show-toplevel'], {
cwd: this.cwdOverride,
})
).stdout.trim();
} catch (err) {
if (!hasWarnedAboutEasProjectRoot) {
Log.warn(`Failed to get Git root path with \`git rev-parse --show-toplevel\`.`, err);
Log.warn('Falling back to using current working directory as project root.');
Log.warn(
'You can set `EAS_PROJECT_ROOT` environment variable to let eas-cli know where your project is located.'
);
hasWarnedAboutEasProjectRoot = true;
}
}
return path.resolve(process.cwd(), process.env.EAS_PROJECT_ROOT ?? '.');
return getRootPath();
}
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
const srcPath = path.normalize(await this.getRootPathAsync());
const srcPath = getRootPath();
await makeShallowCopyAsync(srcPath, destinationPath);
}
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
const rootPath = path.normalize(await this.getRootPathAsync());
const ignore = await Ignore.createForCheckingAsync(rootPath);
const ignore = new Ignore(getRootPath());
await ignore.initIgnoreAsync();
return ignore.ignores(filePath);
}
+10 -11
View File
@@ -1,26 +1,25 @@
import chalk from 'chalk';
import GitClient from './clients/git';
import GitNoCommitClient from './clients/gitNoCommit';
import NoVcsClient from './clients/noVcs';
import { Client } from './vcs';
const NO_VCS_WARNING = `Using EAS CLI without version control system is not recommended, use this mode only if you know what you are doing.`;
let wasNoVcsWarningPrinted = false;
export function resolveVcsClient(requireCommit: boolean = false): Client {
if (process.env.EAS_NO_VCS) {
if (process.env.NODE_ENV !== 'test') {
if (!wasNoVcsWarningPrinted) {
// This log might be printed before cli arguments are evaluated,
// so it needs to go to stderr in case command is run in JSON
// only mode.
// eslint-disable-next-line no-console
console.error(chalk.yellow(NO_VCS_WARNING));
wasNoVcsWarningPrinted = true;
}
// This log might be printed before cli arguments are evaluated,
// so it needs to go to stderr in case command is run in JSON
// only mode.
// eslint-disable-next-line no-console
console.error(chalk.yellow(NO_VCS_WARNING));
}
return new NoVcsClient();
}
return new GitClient({ requireCommit });
if (requireCommit) {
return new GitClient();
}
return new GitNoCommitClient();
}
+29 -70
View File
@@ -1,14 +1,24 @@
import fg from 'fast-glob';
import fs from 'fs/promises';
import fsExtra from 'fs-extra';
import fs from 'fs-extra';
import createIgnore, { Ignore as SingleFileIgnore } from 'ignore';
import path from 'path';
import Log from '../log';
export const EASIGNORE_FILENAME = '.easignore';
const EASIGNORE_FILENAME = '.easignore';
const GITIGNORE_FILENAME = '.gitignore';
const DEFAULT_IGNORE = `
.git
node_modules
`;
export function getRootPath(): string {
const rootPath = process.env.EAS_PROJECT_ROOT ?? process.cwd();
if (!path.isAbsolute(rootPath)) {
return path.resolve(process.cwd(), rootPath);
}
return rootPath;
}
/**
* Ignore wraps the 'ignore' package to support multiple .gitignore files
* in subdirectories.
@@ -20,41 +30,17 @@ const GITIGNORE_FILENAME = '.gitignore';
* - if .easignore exists, .gitignore files are not used.
*/
export class Ignore {
public ignoreMapping: (readonly [string, SingleFileIgnore])[] = [];
private ignoreMapping: (readonly [string, SingleFileIgnore])[] = [];
private constructor(private readonly rootDir: string) {}
constructor(private readonly rootDir: string) {}
static async createForCopyingAsync(rootDir: string): Promise<Ignore> {
const ignore = new Ignore(rootDir);
await ignore.initIgnoreAsync({
defaultIgnore: `
.git
node_modules
`,
});
return ignore;
}
/** Does not include the default .git and node_modules ignore rules. */
static async createForCheckingAsync(rootDir: string): Promise<Ignore> {
const ignore = new Ignore(rootDir);
await ignore.initIgnoreAsync({
defaultIgnore: ``,
});
return ignore;
}
public async initIgnoreAsync({ defaultIgnore }: { defaultIgnore: string }): Promise<void> {
public async initIgnoreAsync(): Promise<void> {
const easIgnorePath = path.join(this.rootDir, EASIGNORE_FILENAME);
if (await fsExtra.pathExists(easIgnorePath)) {
if (await fs.pathExists(easIgnorePath)) {
this.ignoreMapping = [
['', createIgnore().add(defaultIgnore)],
['', createIgnore().add(await fsExtra.readFile(easIgnorePath, 'utf-8'))],
['', createIgnore().add(DEFAULT_IGNORE)],
['', createIgnore().add(await fs.readFile(easIgnorePath, 'utf-8'))],
];
Log.debug('initializing ignore mapping with .easignore', {
ignoreMapping: this.ignoreMapping,
});
return;
}
const ignoreFilePaths = (
@@ -71,16 +57,11 @@ node_modules
ignoreFilePaths.map(async filePath => {
return [
filePath.slice(0, filePath.length - GITIGNORE_FILENAME.length),
createIgnore().add(await fsExtra.readFile(path.join(this.rootDir, filePath), 'utf-8')),
createIgnore().add(await fs.readFile(path.join(this.rootDir, filePath), 'utf-8')),
] as const;
})
);
this.ignoreMapping = [['', createIgnore().add(defaultIgnore)], ...ignoreMapping];
Log.debug('initializing ignore mapping with .gitignore files', {
ignoreFilePaths,
ignoreMapping: this.ignoreMapping,
});
this.ignoreMapping = [['', createIgnore().add(DEFAULT_IGNORE)], ...ignoreMapping];
}
public ignores(relativePath: string): boolean {
@@ -93,37 +74,15 @@ node_modules
}
}
export async function makeShallowCopyAsync(_src: string, dst: string): Promise<void> {
// `node:fs` on Windows adds a namespace prefix (e.g. `\\?\`) to the path provided
// to the `filter` function in `fs.cp`. We need to ensure that we compare the right paths
// (both with prefix), otherwise the `relativePath` ends up being wrong and causes no files
// to be ignored.
const src = path.toNamespacedPath(path.normalize(_src));
Log.debug('makeShallowCopyAsync', { src, dst });
const ignore = await Ignore.createForCopyingAsync(src);
Log.debug('makeShallowCopyAsync ignoreMapping', { ignoreMapping: ignore.ignoreMapping });
await fs.cp(src, dst, {
recursive: true,
// Preserve symlinks without re-resolving them to their original targets
verbatimSymlinks: true,
filter: (_srcFilePath: string) => {
const srcFilePath = path.toNamespacedPath(_srcFilePath);
export async function makeShallowCopyAsync(src: string, dst: string): Promise<void> {
const ignore = new Ignore(src);
await ignore.initIgnoreAsync();
await fs.copy(src, dst, {
filter: (srcFilePath: string) => {
if (srcFilePath === src) {
return true;
}
const relativePath = path.relative(src, srcFilePath);
const shouldCopyTheItem = !ignore.ignores(relativePath);
Log.debug(shouldCopyTheItem ? 'copying' : 'skipping', {
src,
srcFilePath,
relativePath,
});
return shouldCopyTheItem;
return !ignore.ignores(path.relative(src, srcFilePath));
},
});
}
+5 -59
View File
@@ -141,53 +141,6 @@ export async function createManifestAsync(
return { conflictingVariableNames, manifest };
}
interface RawSourceMap {
version: number;
sources: string[];
names: string[];
sourceRoot?: string;
sourcesContent?: string[];
mappings: string;
file: string;
}
const sourceMapExtensionRe = /\.map$/;
async function normalizeSourceMapAsync(
file: RecursiveFileEntry
): Promise<WorkerFileEntry | undefined> {
// This logic normalizes source maps like Wrangler does
// See: https://github.com/cloudflare/workers-sdk/blob/508a1a3/packages/wrangler/src/deployment-bundle/source-maps.ts#L31-L63
const data = await fs.promises.readFile(file.path, 'utf-8');
try {
const map = JSON.parse(data) as RawSourceMap;
if (typeof map !== 'object' || !map) {
return undefined;
}
let sources = map.sources || [];
if (Array.isArray(map.sources)) {
// Remote null-byte prefixes from virtual file names
sources = map.sources.map(sourceName =>
sourceName.charCodeAt(0) === 0 ? sourceName.slice(1) : sourceName
);
}
return {
normalizedPath: file.normalizedPath,
path: file.path,
data: JSON.stringify({
version: 3,
file: file.normalizedPath.replace(sourceMapExtensionRe, ''),
sourcesContent: sources.map(() => null),
sources,
names: map.names,
mappings: map.mappings,
}),
};
} catch {
return undefined;
}
}
interface WorkerFileEntry {
normalizedPath: string;
path: string;
@@ -197,18 +150,11 @@ interface WorkerFileEntry {
/** Reads worker files while normalizing sourcemaps and providing normalized paths */
async function* listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerFileEntry> {
for await (const file of listFilesRecursively(workerPath)) {
if (file.normalizedPath.endsWith('.map')) {
const normalizedSourceMap = await normalizeSourceMapAsync(file);
if (normalizedSourceMap) {
yield normalizedSourceMap;
}
} else {
yield {
normalizedPath: file.normalizedPath,
path: file.path,
data: await fs.promises.readFile(file.path),
};
}
yield {
normalizedPath: file.normalizedPath,
path: file.path,
data: await fs.promises.readFile(file.path),
};
}
}
+2 -16
View File
@@ -130,22 +130,8 @@ export async function uploadAsync(params: UploadParams): Promise<UploadResult> {
}
const getErrorMessageAsync = async (): Promise<string> => {
const rayId = response.headers.get('cf-ray');
const contentType = response.headers.get('Content-Type');
if (contentType?.startsWith('text/html')) {
// NOTE(@kitten): We've received a CDN error most likely. There's not much we can do
// except for quoting the Request ID, so a user can send it to us. We can check
// why a request was blocked by looking up a WAF event via the "Ray ID" here:
// https://dash.cloudflare.com/e6f39f67f543faa6038768e8f37e4234/expo.app/security/events
let message = `CDN firewall has aborted the upload with ${response.statusText}.`;
if (rayId) {
message += `\nReport this error quoting Request ID ${rayId}`;
}
return `Upload of "${filePath}" failed: ${message}`;
} else {
const json = await response.json().catch(() => null);
return json?.error ?? `Upload of "${filePath}" failed: ${response.statusText}`;
}
const body = await response.json().catch(() => null);
return body?.error ?? `Upload of "${filePath}" failed: ${response.statusText}`;
};
if (
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "16.3.3",
"version": "14.5.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/eas-build-job": "1.0.173",
"@expo/eas-build-job": "1.0.169",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "11.2.0",
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff. Show more