mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f400d7766 | ||
|
|
4338aea702 | ||
|
|
ecf42e68f3 | ||
|
|
f23579fa32 | ||
|
|
afdc626fcb | ||
|
|
f9f4583a39 | ||
|
|
2d955440ca | ||
|
|
642bb6fa4d | ||
|
|
fb1f2d2add | ||
|
|
dbcca53f1d | ||
|
|
4a0dc9f426 | ||
|
|
146656c7f7 | ||
|
|
e37411040f | ||
|
|
b71226d90f | ||
|
|
9a9fe11f73 | ||
|
|
1b62b6feae | ||
|
|
ebb215db1a | ||
|
|
cdf49377cd | ||
|
|
8fb84e86bb | ||
|
|
d660e3602d | ||
|
|
5b9d47c02b | ||
|
|
61b601de88 | ||
|
|
c5867c8c07 | ||
|
|
f2d5c210a7 | ||
|
|
6e70f7ee4b | ||
|
|
1d36f4f636 | ||
|
|
f8deba78a5 |
No files matched your search
@@ -12,6 +12,8 @@ jobs:
|
||||
profile: system-test
|
||||
custom_job:
|
||||
name: Custom Job
|
||||
runs_on: linux-medium-nested-virtualization
|
||||
steps:
|
||||
- name: Echo
|
||||
run: echo "This is a custom job step"
|
||||
- uses: eas/start_android_emulator
|
||||
@@ -8,10 +8,33 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto-enable `--non-interactive` when `--json` is passed, so users no longer need to specify both flags. ([#3476](https://github.com/expo/eas-cli/pull/3476) by [@krystofwoldrich](https://github.com/krystofwoldrich))
|
||||
- Auto-generate Android keystore in non-interactive mode instead of throwing an error. ([#3477](https://github.com/expo/eas-cli/pull/3477) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `--source-maps` flag to prevent uploading server sourcemaps ([#3478](https://github.com/expo/eas-cli/pull/3478) by [@hassankhan](https://github.com/hassankhan))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Provide an override for the new --environment flag requirement in the update command. ([#3442](https://github.com/expo/eas-cli/pull/3442) by [@douglowder](https://github.com/douglowder))
|
||||
- Add missing `--include=dev` for `npm install` commands ([#3459](https://github.com/expo/eas-cli/pull/3459) by [@kitten](https://github.com/kitten))
|
||||
- Add missing `--production false` for `yarn install` commands for Yarn Classic ([#3459](https://github.com/expo/eas-cli/pull/3459) by [@kitten](https://github.com/kitten))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `oclif` to v4. ([#3472](https://github.com/expo/eas-cli/pull/3472) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [18.1.0](https://github.com/expo/eas-cli/releases/tag/v18.1.0) - 2026-03-04
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support YAML anchors and aliases when parsing workflow files. ([#3435](https://github.com/expo/eas-cli/pull/3435) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Remove --environment flag requirement for update:configure. ([#3440](https://github.com/expo/eas-cli/pull/3440) by [@douglowder](https://github.com/douglowder))
|
||||
- Fix login spinner interfering with prompts in `eas go` command when not logged in. ([#3451](https://github.com/expo/eas-cli/pull/3451) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
## [18.0.6](https://github.com/expo/eas-cli/releases/tag/v18.0.6) - 2026-02-27
|
||||
|
||||
## [18.0.5](https://github.com/expo/eas-cli/releases/tag/v18.0.5) - 2026-02-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
@@ -296,9 +296,11 @@ export EXPO_DEBUG=1 # Enable debug logging
|
||||
|
||||
### Validation
|
||||
|
||||
All changes should be validated with TypeScript and the linter before committing:
|
||||
All changes should be validated before committing. At minimum run package-level tests (e.g. `jest-unit`)
|
||||
for touched code, then run type checks/linting and format the repo:
|
||||
|
||||
```bash
|
||||
yarn fmt # Apply Oxfmt formatting
|
||||
yarn typecheck # Validate TypeScript types
|
||||
yarn lint # Run Oxlint
|
||||
yarn fmt:check # Run Oxfmt format check
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.0.6",
|
||||
"version": "18.2.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-watch-typeahead": "2.2.2",
|
||||
"lerna": "9.0.3",
|
||||
"oclif": "^3.9.0",
|
||||
"oclif": "^4.22.85",
|
||||
"oxfmt": "0.28.0",
|
||||
"oxlint": "1.43.0",
|
||||
"oxlint-tsgolint": "0.11.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.0.6",
|
||||
"version": "18.2.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -38,14 +38,14 @@
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/eas-build-job": "18.2.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.0.2",
|
||||
"@expo/steps": "18.2.0",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
|
||||
@@ -19,3 +19,7 @@ export enum LoggerLevel {
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export enum PipeMode {
|
||||
COMBINED = 'combined',
|
||||
}
|
||||
@@ -26,8 +26,14 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('NPM_CORRUPTED_PACKAGE');
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.message).toBe(
|
||||
'Unknown error. See logs of the Install dependencies build phase for more information.'
|
||||
);
|
||||
expect(err.trackingCode).toBe('NPM_CORRUPTED_PACKAGE');
|
||||
expect(err.metadata).toEqual({
|
||||
packageName: '@typescript-eslint/[email protected]',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects log for invalid bundler and reports it to user', async () => {
|
||||
@@ -45,7 +51,11 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
|
||||
expect(err.message).toBe(
|
||||
'Your project requires a different version of the Ruby "bundler" program than the version installed in this EAS Build environment. You can specify which version of "bundler" to install by specifying the version under "build"→[buildProfileName]→"ios"→"bundler" in eas.json.'
|
||||
);
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
expect(err.metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not detect errors if they show up in different build phase', async () => {
|
||||
@@ -63,7 +73,10 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.message).toBe(
|
||||
'Unknown error. See logs of the Install dependencies build phase for more information.'
|
||||
);
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects npm cache error if cache is enabled', async () => {
|
||||
@@ -82,8 +95,11 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('NPM_CACHE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.message).toBe(
|
||||
'Unknown error. See logs of the Install dependencies build phase for more information.'
|
||||
);
|
||||
expect(err.trackingCode).toBe('NPM_CACHE_ERROR');
|
||||
});
|
||||
|
||||
it('does not detect npm cache error if cache is disabled', async () => {
|
||||
@@ -103,7 +119,10 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.message).toBe(
|
||||
'Unknown error. See logs of the Install dependencies build phase for more information.'
|
||||
);
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects xcode line error', async () => {
|
||||
@@ -126,7 +145,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/path/to/'
|
||||
);
|
||||
expect(err.errorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects minimum deployment target error correctly', async () => {
|
||||
@@ -144,7 +163,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects provisioning profile mismatch error correctly', async () => {
|
||||
@@ -162,7 +181,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects generic "Run Fastlane" error for resign correctly', async () => {
|
||||
@@ -178,7 +197,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects build error in "Run Fastlane" phase correctly', async () => {
|
||||
@@ -194,7 +213,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects provisioning profile mismatch error correctly', async () => {
|
||||
@@ -240,8 +259,8 @@ note`;
|
||||
'/path/to'
|
||||
);
|
||||
expect(err.errorCode).toBe('XCODE_BUILD_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('XCODE_BUILD_ERROR');
|
||||
expect(err.userFacingMessage)
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
expect(err.message)
|
||||
.toBe(`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:
|
||||
- The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
|
||||
- Some other error
|
||||
@@ -263,9 +282,9 @@ Refer to "Xcode Logs" below for additional, more detailed logs.`);
|
||||
'/fake/path'
|
||||
);
|
||||
|
||||
expect(err.errorCode).toBe('MAVEN_CACHE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.userFacingMessage).toBe(
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.trackingCode).toBe('MAVEN_CACHE_ERROR');
|
||||
expect(err.message).toBe(
|
||||
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
|
||||
);
|
||||
});
|
||||
@@ -285,8 +304,8 @@ Refer to "Xcode Logs" below for additional, more detailed logs.`);
|
||||
);
|
||||
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.userFacingMessage).toBe(
|
||||
expect(err.trackingCode).toBeUndefined();
|
||||
expect(err.message).toBe(
|
||||
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ import { ErrorContext, ErrorHandler } from './errors.types';
|
||||
export class TrackedBuildError extends Error {
|
||||
constructor(
|
||||
public errorCode: string,
|
||||
public message: string
|
||||
public message: string,
|
||||
public metadata?: Record<string, unknown>
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
@@ -154,7 +155,9 @@ export const buildErrorHandlers: ErrorHandler<TrackedBuildError>[] = [
|
||||
// [stderr] WARN tarball tarball data for @typescript-eslint/[email protected] (sha512-cozo/GbwixVR0sgfHItz3t1yXu521yn71Wj6PlYCFA3WPhy51CUPkifFKfBis91bDclGmAY45hhaAXVjdn4new==) seems to be corrupted. Trying again.
|
||||
regexp: /tarball tarball data for ([^ ]*) .* seems to be corrupted. Trying again/,
|
||||
createError: (match: RegExpMatchArray) =>
|
||||
new TrackedBuildError('NPM_CORRUPTED_PACKAGE', `npm: corrupted package ${match[1]}`),
|
||||
new TrackedBuildError('NPM_CORRUPTED_PACKAGE', `npm: corrupted package ${match[1]}`, {
|
||||
packageName: match[1],
|
||||
}),
|
||||
})),
|
||||
...[BuildPhase.INSTALL_DEPENDENCIES, BuildPhase.PREBUILD].map(phase => ({
|
||||
phase,
|
||||
|
||||
@@ -80,27 +80,17 @@ export async function resolveBuildPhaseErrorAsync(
|
||||
new errors.UnknownError(errorContext.phase));
|
||||
const buildError = resolveError(buildErrorHandlers, logLines, errorContext, xcodeBuildLogs);
|
||||
|
||||
const isUnknownUserError =
|
||||
!userFacingError ||
|
||||
(
|
||||
[
|
||||
errors.ErrorCode.UNKNOWN_ERROR,
|
||||
errors.ErrorCode.UNKNOWN_GRADLE_ERROR,
|
||||
errors.ErrorCode.UNKNOWN_FASTLANE_ERROR,
|
||||
] as string[]
|
||||
).includes(userFacingError.errorCode);
|
||||
const message =
|
||||
(isUnknownUserError ? buildError?.message : userFacingError.message) ?? userFacingError.message;
|
||||
const errorCode =
|
||||
(isUnknownUserError ? buildError?.errorCode : userFacingError.errorCode) ??
|
||||
userFacingError.errorCode;
|
||||
const trackingCode =
|
||||
buildError && buildError.errorCode !== userFacingError.errorCode
|
||||
? buildError.errorCode
|
||||
: undefined;
|
||||
|
||||
return new errors.BuildError(message, {
|
||||
errorCode,
|
||||
userFacingErrorCode: userFacingError.errorCode,
|
||||
userFacingMessage: userFacingError.message,
|
||||
return new errors.BuildError(userFacingError.message, {
|
||||
errorCode: userFacingError.errorCode,
|
||||
trackingCode,
|
||||
docsUrl: userFacingError.docsUrl,
|
||||
innerError: error,
|
||||
cause: error,
|
||||
buildPhase: phase,
|
||||
metadata: buildError?.metadata,
|
||||
});
|
||||
}
|
||||
@@ -24,7 +24,8 @@ export async function installDependenciesAsync({
|
||||
let args: string[];
|
||||
switch (packageManager) {
|
||||
case PackageManager.NPM: {
|
||||
args = useFrozenLockfile ? ['ci', '--include=dev'] : ['install'];
|
||||
args = useFrozenLockfile ? ['ci'] : ['install'];
|
||||
args.push('--include=dev');
|
||||
break;
|
||||
}
|
||||
case PackageManager.PNPM: {
|
||||
@@ -44,7 +45,12 @@ export async function installDependenciesAsync({
|
||||
];
|
||||
}
|
||||
} else {
|
||||
args = ['install', ...(useFrozenLockfile ? ['--frozen-lockfile'] : [])];
|
||||
args = [
|
||||
'install',
|
||||
...(useFrozenLockfile ? ['--frozen-lockfile'] : []),
|
||||
'--production',
|
||||
'false',
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
// leaving message empty, website will display err.stack which already includes err.message
|
||||
this.logger.error({ err }, '');
|
||||
} else {
|
||||
this.logger.error(`Error: ${buildError.userFacingMessage}`);
|
||||
this.logger.error(`Error: ${buildError.message}`);
|
||||
}
|
||||
return buildError;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { spawnAsync } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import { AndroidEmulatorUtils } from '../../../utils/AndroidEmulatorUtils';
|
||||
import { IosSimulatorUtils } from '../../../utils/IosSimulatorUtils';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../../../utils/findMaestroPathsFlowsToExecuteAsync';
|
||||
import { retryAsync } from '../../../utils/retry';
|
||||
import { createInternalEasMaestroTestFunction } from '../internalMaestroTest';
|
||||
|
||||
jest.mock('@expo/steps', () => ({
|
||||
@@ -13,11 +16,22 @@ jest.mock('@expo/steps', () => ({
|
||||
spawnAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@expo/turtle-spawn', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/IosSimulatorUtils');
|
||||
jest.mock('../../../utils/AndroidEmulatorUtils');
|
||||
jest.mock('../../../utils/findMaestroPathsFlowsToExecuteAsync');
|
||||
jest.mock('../../../utils/retry', () => ({
|
||||
retryAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedSpawnAsync = jest.mocked(spawnAsync);
|
||||
const mockedSpawn = jest.mocked(spawn);
|
||||
const mockedRetryAsync = jest.mocked(retryAsync);
|
||||
const mockedAndroidUtils = jest.mocked(AndroidEmulatorUtils);
|
||||
const mockedIosUtils = jest.mocked(IosSimulatorUtils);
|
||||
const mockedFindFlows = jest.mocked(findMaestroPathsFlowsToExecuteAsync);
|
||||
|
||||
@@ -28,8 +42,21 @@ describe(createInternalEasMaestroTestFunction, () => {
|
||||
vol.mkdirSync(os.tmpdir(), { recursive: true });
|
||||
|
||||
mockedSpawnAsync.mockResolvedValue(undefined as any);
|
||||
mockedSpawn.mockResolvedValue({ stdout: 'source-emulator\n', stderr: '' } as any);
|
||||
mockUploadArtifact.mockResolvedValue({ artifactId: null });
|
||||
|
||||
mockedRetryAsync.mockImplementation(async (fn, { retryOptions }) => {
|
||||
let lastErr: unknown;
|
||||
for (let attemptCount = 0; attemptCount <= retryOptions.retries; attemptCount++) {
|
||||
try {
|
||||
return await fn(attemptCount);
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
});
|
||||
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([
|
||||
{ name: 'iPhone 15', udid: 'test-udid-123' } as any,
|
||||
]);
|
||||
@@ -39,6 +66,18 @@ describe(createInternalEasMaestroTestFunction, () => {
|
||||
mockedIosUtils.collectLogsAsync.mockResolvedValue({ outputPath: '/tmp/logs.txt' } as any);
|
||||
mockedIosUtils.deleteAsync.mockResolvedValue(undefined as any);
|
||||
|
||||
mockedAndroidUtils.getAttachedDevicesAsync.mockResolvedValue([
|
||||
{ serialId: 'emulator-5554', state: 'device' } as any,
|
||||
]);
|
||||
mockedAndroidUtils.cloneAsync.mockResolvedValue(undefined as any);
|
||||
mockedAndroidUtils.startAsync.mockResolvedValue({
|
||||
serialId: 'emulator-5556',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any);
|
||||
mockedAndroidUtils.waitForReadyAsync.mockResolvedValue(undefined as any);
|
||||
mockedAndroidUtils.collectLogsAsync.mockResolvedValue({ outputPath: '/tmp/logs.txt' } as any);
|
||||
mockedAndroidUtils.deleteAsync.mockResolvedValue(undefined as any);
|
||||
|
||||
mockedFindFlows.mockResolvedValue(['/project/.maestro/home.yml']);
|
||||
});
|
||||
|
||||
@@ -95,4 +134,77 @@ describe(createInternalEasMaestroTestFunction, () => {
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
|
||||
it('retries Android clone startup with increasing timeouts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-1',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-2',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any);
|
||||
mockedAndroidUtils.waitForReadyAsync
|
||||
.mockRejectedValueOnce(new Error('network unavailable'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { platform: 'android' },
|
||||
});
|
||||
|
||||
await step.executeAsync();
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-1',
|
||||
timeoutMs: 60_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-2',
|
||||
timeoutMs: 120_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.deleteAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-1',
|
||||
deviceName: 'eas-simulator-0-0',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('fails Android flow when clone startup exhausts all attempts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-1',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-2',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-3',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any);
|
||||
mockedAndroidUtils.waitForReadyAsync.mockRejectedValue(new Error('network unavailable'));
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { platform: 'android' },
|
||||
});
|
||||
|
||||
await expect(step.executeAsync()).rejects.toThrow('network unavailable');
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-3',
|
||||
timeoutMs: 180_000,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import {
|
||||
AndroidEmulatorUtils,
|
||||
AndroidVirtualDeviceName,
|
||||
} from '../../../utils/AndroidEmulatorUtils';
|
||||
import { retryAsync } from '../../../utils/retry';
|
||||
import { createStartAndroidEmulatorBuildFunction } from '../startAndroidEmulator';
|
||||
|
||||
jest.mock('@expo/turtle-spawn', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/retry', () => ({
|
||||
retryAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/AndroidEmulatorUtils', () => ({
|
||||
AndroidEmulatorUtils: {
|
||||
defaultSystemImagePackage: 'system-images;android-30;default;x86_64',
|
||||
getAvailableDevicesAsync: jest.fn(),
|
||||
createAsync: jest.fn(),
|
||||
cloneAsync: jest.fn(),
|
||||
startAsync: jest.fn(),
|
||||
waitForReadyAsync: jest.fn(),
|
||||
deleteAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedSpawn = jest.mocked(spawn);
|
||||
const mockedRetryAsync = jest.mocked(retryAsync);
|
||||
const mockedAndroidUtils = jest.mocked(AndroidEmulatorUtils);
|
||||
|
||||
function createStep(callInputs?: Record<string, unknown>) {
|
||||
const logger = createMockLogger();
|
||||
const fn = createStartAndroidEmulatorBuildFunction();
|
||||
const globalCtx = createGlobalContextMock({ logger });
|
||||
globalCtx.updateEnv({ HOME: '/home/expo', ANDROID_HOME: '/android/home' });
|
||||
return fn.createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs,
|
||||
});
|
||||
}
|
||||
|
||||
function createStartResult(serialId: string) {
|
||||
return {
|
||||
serialId: serialId as any,
|
||||
emulatorPromise: Promise.resolve({}) as any,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe(createStartAndroidEmulatorBuildFunction, () => {
|
||||
beforeEach(() => {
|
||||
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
|
||||
mockedAndroidUtils.getAvailableDevicesAsync.mockResolvedValue([]);
|
||||
mockedAndroidUtils.createAsync.mockResolvedValue(undefined);
|
||||
mockedAndroidUtils.cloneAsync.mockResolvedValue(undefined);
|
||||
mockedAndroidUtils.waitForReadyAsync.mockResolvedValue(undefined);
|
||||
mockedAndroidUtils.deleteAsync.mockResolvedValue(undefined);
|
||||
|
||||
mockedRetryAsync.mockImplementation(async (fn, { retryOptions }) => {
|
||||
let lastErr: unknown;
|
||||
for (let attemptCount = 0; attemptCount <= retryOptions.retries; attemptCount++) {
|
||||
try {
|
||||
return await fn(attemptCount);
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
});
|
||||
});
|
||||
|
||||
it('retries base emulator startup with increasing readiness timeouts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce(createStartResult('emulator-1111'))
|
||||
.mockResolvedValueOnce(createStartResult('emulator-2222'));
|
||||
mockedAndroidUtils.waitForReadyAsync
|
||||
.mockRejectedValueOnce(new Error('network unavailable'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-1111',
|
||||
timeoutMs: 60_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-2222',
|
||||
timeoutMs: 120_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.deleteAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-1111',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('retries clone startup independently and cleans up failed clone attempts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce(createStartResult('emulator-base'))
|
||||
.mockResolvedValueOnce(createStartResult('emulator-clone-1-attempt-1'))
|
||||
.mockResolvedValueOnce(createStartResult('emulator-clone-1-attempt-2'))
|
||||
.mockResolvedValueOnce(createStartResult('emulator-clone-2-attempt-1'));
|
||||
|
||||
mockedAndroidUtils.waitForReadyAsync
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(new Error('clone network unavailable'))
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
await createStep({ count: 2 }).executeAsync();
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-base',
|
||||
timeoutMs: 60_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-clone-1-attempt-1',
|
||||
timeoutMs: 60_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-clone-1-attempt-2',
|
||||
timeoutMs: 120_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-clone-2-attempt-1',
|
||||
timeoutMs: 60_000,
|
||||
})
|
||||
);
|
||||
|
||||
expect(mockedAndroidUtils.deleteAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-clone-1-attempt-1',
|
||||
deviceName: 'eas-simulator-1' as AndroidVirtualDeviceName,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('fails after exhausting all startup attempts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce(createStartResult('emulator-attempt-1'))
|
||||
.mockResolvedValueOnce(createStartResult('emulator-attempt-2'))
|
||||
.mockResolvedValueOnce(createStartResult('emulator-attempt-3'));
|
||||
mockedAndroidUtils.waitForReadyAsync.mockRejectedValue(new Error('network unavailable'));
|
||||
|
||||
await expect(createStep().executeAsync()).rejects.toThrow('network unavailable');
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-3',
|
||||
timeoutMs: 180_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.deleteAsync).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
isClosedVersionTrainError,
|
||||
isInvalidBundleIdentifierError,
|
||||
isMissingPurposeStringError,
|
||||
parseMissingUsageDescriptionKeys,
|
||||
} from '../uploadToAsc';
|
||||
|
||||
describe(isClosedVersionTrainError, () => {
|
||||
it('returns true when all errors are closed-version-train codes', () => {
|
||||
expect(
|
||||
isClosedVersionTrainError([
|
||||
{ code: '90062' },
|
||||
{ code: '90186' },
|
||||
{ code: '90478' },
|
||||
{ code: '90062' },
|
||||
])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when any other error code is present', () => {
|
||||
expect(
|
||||
isClosedVersionTrainError([{ code: '90062' }, { code: '90186' }, { code: '12345' }])
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when there are no errors', () => {
|
||||
expect(isClosedVersionTrainError([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(isInvalidBundleIdentifierError, () => {
|
||||
it('returns true when all errors are invalid-bundle-id codes', () => {
|
||||
expect(
|
||||
isInvalidBundleIdentifierError([{ code: '90054' }, { code: '90055' }, { code: '90055' }])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when any other error code is present', () => {
|
||||
expect(isInvalidBundleIdentifierError([{ code: '90055' }, { code: '90713' }])).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when there are no errors', () => {
|
||||
expect(isInvalidBundleIdentifierError([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(isMissingPurposeStringError, () => {
|
||||
it('returns true when all errors are missing-purpose-string codes', () => {
|
||||
expect(isMissingPurposeStringError([{ code: '90683' }, { code: '90683' }])).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when any other error code is present', () => {
|
||||
expect(isMissingPurposeStringError([{ code: '90683' }, { code: '90054' }])).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when there are no errors', () => {
|
||||
expect(isMissingPurposeStringError([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(parseMissingUsageDescriptionKeys, () => {
|
||||
it('extracts missing UsageDescription keys from ASC messages', () => {
|
||||
expect(
|
||||
parseMissingUsageDescriptionKeys([
|
||||
{
|
||||
description:
|
||||
'The Info.plist file should contain a NSPhotoLibraryUsageDescription key with a user-facing purpose string. (90683)',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'The Info.plist file should contain a NSCameraUsageDescription key with a user-facing purpose string. (90683)',
|
||||
},
|
||||
])
|
||||
).toEqual(['NSPhotoLibraryUsageDescription', 'NSCameraUsageDescription']);
|
||||
});
|
||||
|
||||
it('deduplicates repeated keys', () => {
|
||||
expect(
|
||||
parseMissingUsageDescriptionKeys([
|
||||
{
|
||||
description:
|
||||
'The Info.plist file should contain a NSCameraUsageDescription key with a user-facing purpose string. (90683)',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'The Info.plist file should contain a NSCameraUsageDescription key with a user-facing purpose string. (90683)',
|
||||
},
|
||||
])
|
||||
).toEqual(['NSCameraUsageDescription']);
|
||||
});
|
||||
|
||||
it('returns an empty array when no usage description key is present', () => {
|
||||
expect(
|
||||
parseMissingUsageDescriptionKeys([{ description: 'Some other upload validation error.' }])
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '../../utils/AndroidEmulatorUtils';
|
||||
import { IosSimulatorName, IosSimulatorUtils } from '../../utils/IosSimulatorUtils';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../../utils/findMaestroPathsFlowsToExecuteAsync';
|
||||
import { retryAsync } from '../../utils/retry';
|
||||
import { PlatformToProperNounMap } from '../../utils/strings';
|
||||
|
||||
export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): BuildFunction {
|
||||
@@ -413,6 +414,9 @@ const MaestroOutputFormatToExtensionMap: Record<string, string | undefined> = {
|
||||
html: 'html',
|
||||
};
|
||||
|
||||
const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
|
||||
const ANDROID_STARTUP_RETRIES_COUNT = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length - 1;
|
||||
|
||||
async function withCleanDeviceAsync<TResult>({
|
||||
platform,
|
||||
sourceDeviceIdentifier,
|
||||
@@ -434,7 +438,7 @@ async function withCleanDeviceAsync<TResult>({
|
||||
}): Promise<{ fnResult: TResult; logsResult: Result<{ outputPath: string }> | null }> {
|
||||
// Clone and start the device
|
||||
|
||||
let localDeviceIdentifier: IosSimulatorName | AndroidDeviceSerialId;
|
||||
let localDeviceIdentifier: IosSimulatorName | AndroidDeviceSerialId | null = null;
|
||||
|
||||
switch (platform) {
|
||||
case 'ios': {
|
||||
@@ -458,28 +462,81 @@ async function withCleanDeviceAsync<TResult>({
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
logger.info(`Cloning Android Emulator ${sourceDeviceIdentifier} to ${localDeviceName}...`);
|
||||
await AndroidEmulatorUtils.cloneAsync({
|
||||
sourceDeviceName: sourceDeviceIdentifier as AndroidVirtualDeviceName,
|
||||
destinationDeviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
logger.info(`Starting Android Emulator ${localDeviceName}...`);
|
||||
const { serialId } = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
logger.info(`Waiting for Android Emulator ${localDeviceName} to be ready...`);
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId,
|
||||
env,
|
||||
});
|
||||
localDeviceIdentifier = serialId;
|
||||
await retryAsync(
|
||||
async attemptCount => {
|
||||
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
||||
const attempt = attemptCount + 1;
|
||||
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
||||
let serialId: AndroidDeviceSerialId | null = null;
|
||||
try {
|
||||
logger.info(
|
||||
`Cloning Android Emulator ${sourceDeviceIdentifier} to ${localDeviceName} (attempt ${attempt}/${maxAttempts})...`
|
||||
);
|
||||
await AndroidEmulatorUtils.cloneAsync({
|
||||
sourceDeviceName: sourceDeviceIdentifier as AndroidVirtualDeviceName,
|
||||
destinationDeviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
logger.info(
|
||||
`Starting Android Emulator ${localDeviceName} (attempt ${attempt}/${maxAttempts})...`
|
||||
);
|
||||
const startResult = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
serialId = startResult.serialId;
|
||||
logger.info(`Waiting for Android Emulator ${localDeviceName} to be ready...`);
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId,
|
||||
env,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
localDeviceIdentifier = serialId;
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err },
|
||||
`Failed to start Android Emulator ${localDeviceName} on attempt ${attempt}/${maxAttempts}.`
|
||||
);
|
||||
try {
|
||||
if (serialId) {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId,
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
logger.warn(
|
||||
{ err: cleanupErr },
|
||||
`Failed to clean up Android Emulator ${localDeviceName}.`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{
|
||||
logger,
|
||||
retryOptions: {
|
||||
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!localDeviceIdentifier) {
|
||||
throw new Error('Device did not return an identifier after startup.');
|
||||
}
|
||||
|
||||
// Run the function
|
||||
|
||||
const fnResult = await asyncResult(fn({ deviceIdentifier: localDeviceIdentifier }));
|
||||
@@ -510,14 +567,14 @@ async function withCleanDeviceAsync<TResult>({
|
||||
logger.info(`Collecting logs from ${localDeviceName}...`);
|
||||
logsResult = await asyncResult(
|
||||
AndroidEmulatorUtils.collectLogsAsync({
|
||||
serialId: localDeviceIdentifier as AndroidDeviceSerialId,
|
||||
serialId: localDeviceIdentifier as unknown as AndroidDeviceSerialId,
|
||||
env,
|
||||
})
|
||||
);
|
||||
|
||||
logger.info(`Cleaning up ${localDeviceName}...`);
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId: localDeviceIdentifier as AndroidDeviceSerialId,
|
||||
serialId: localDeviceIdentifier as unknown as AndroidDeviceSerialId,
|
||||
env,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -58,11 +58,6 @@ export function createRestoreCacheFunction(): BuildFunction {
|
||||
const { logger } = stepsCtx;
|
||||
|
||||
try {
|
||||
if (stepsCtx.global.staticContext.job.platform) {
|
||||
logger.error('Caches are not supported in build jobs yet.');
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = z
|
||||
.array(z.string())
|
||||
.parse(((inputs.path.value ?? '') as string).split(/[\r\n]+/))
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
} from '../../utils/AndroidEmulatorUtils';
|
||||
import { retryAsync } from '../../utils/retry';
|
||||
|
||||
const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
|
||||
const ANDROID_STARTUP_RETRIES_COUNT = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length - 1;
|
||||
|
||||
export function createStartAndroidEmulatorBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
@@ -72,25 +75,78 @@ export function createStartAndroidEmulatorBuildFunction(): BuildFunction {
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Creating emulator device');
|
||||
await AndroidEmulatorUtils.createAsync({
|
||||
deviceName,
|
||||
systemImagePackage,
|
||||
deviceIdentifier: deviceIdentifier ?? null,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
let emulatorPromise = null;
|
||||
let serialId = null;
|
||||
await retryAsync(
|
||||
async attemptCount => {
|
||||
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
||||
const attempt = attemptCount + 1;
|
||||
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
||||
const attemptSuffix = attempt > 1 ? ` (attempt ${attempt}/${maxAttempts})` : '';
|
||||
let attemptSerialId = null;
|
||||
|
||||
logger.info('Starting emulator device');
|
||||
const { emulatorPromise, serialId } = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName,
|
||||
env,
|
||||
});
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
env,
|
||||
serialId,
|
||||
});
|
||||
logger.info(`${deviceName} is ready.`);
|
||||
try {
|
||||
logger.info(`Creating emulator device${attemptSuffix}.`);
|
||||
await AndroidEmulatorUtils.createAsync({
|
||||
deviceName,
|
||||
systemImagePackage,
|
||||
deviceIdentifier: deviceIdentifier ?? null,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info(`Starting emulator device${attemptSuffix}.`);
|
||||
const startResult = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName,
|
||||
env,
|
||||
});
|
||||
attemptSerialId = startResult.serialId;
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
env,
|
||||
serialId: attemptSerialId,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
logger.info(`${deviceName} is ready.`);
|
||||
|
||||
serialId = attemptSerialId;
|
||||
emulatorPromise = startResult.emulatorPromise;
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err },
|
||||
`${deviceName} failed to start on attempt ${attempt}/${maxAttempts}.`
|
||||
);
|
||||
try {
|
||||
if (attemptSerialId) {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId: attemptSerialId,
|
||||
deviceName,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
deviceName,
|
||||
env,
|
||||
});
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
logger.warn({ err: cleanupErr }, `Failed to clean up ${deviceName}.`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{
|
||||
logger,
|
||||
retryOptions: {
|
||||
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!serialId || !emulatorPromise) {
|
||||
throw new Error(`Failed to start emulator ${deviceName}.`);
|
||||
}
|
||||
|
||||
const count = Number(inputs.count.value ?? 1);
|
||||
if (count > 1) {
|
||||
@@ -105,27 +161,70 @@ export function createStartAndroidEmulatorBuildFunction(): BuildFunction {
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cloneIdentifier = `eas-simulator-${i + 1}` as AndroidVirtualDeviceName;
|
||||
logger.info(`Cloning ${deviceName} to ${cloneIdentifier}...`);
|
||||
await AndroidEmulatorUtils.cloneAsync({
|
||||
sourceDeviceName: deviceName,
|
||||
destinationDeviceName: cloneIdentifier,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
await retryAsync(
|
||||
async attemptCount => {
|
||||
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
||||
const attempt = attemptCount + 1;
|
||||
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
||||
const attemptSuffix = attempt > 1 ? ` (attempt ${attempt}/${maxAttempts})` : '';
|
||||
let cloneSerialId = null;
|
||||
try {
|
||||
logger.info(`Cloning ${deviceName} to ${cloneIdentifier}${attemptSuffix}.`);
|
||||
await AndroidEmulatorUtils.cloneAsync({
|
||||
sourceDeviceName: deviceName,
|
||||
destinationDeviceName: cloneIdentifier,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info('Starting emulator device');
|
||||
const { serialId } = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName: cloneIdentifier,
|
||||
env,
|
||||
});
|
||||
logger.info(`Starting emulator device${attemptSuffix}.`);
|
||||
const startResult = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName: cloneIdentifier,
|
||||
env,
|
||||
});
|
||||
cloneSerialId = startResult.serialId;
|
||||
|
||||
logger.info('Waiting for emulator to become ready');
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId,
|
||||
env,
|
||||
});
|
||||
logger.info('Waiting for emulator to become ready');
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId: cloneSerialId,
|
||||
env,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info(`${cloneIdentifier} is ready.`);
|
||||
logger.info(`${cloneIdentifier} is ready.`);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err },
|
||||
`${cloneIdentifier} failed to start on attempt ${attempt}/${maxAttempts}.`
|
||||
);
|
||||
try {
|
||||
if (cloneSerialId) {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId: cloneSerialId,
|
||||
deviceName: cloneIdentifier,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
deviceName: cloneIdentifier,
|
||||
env,
|
||||
});
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
logger.warn({ err: cleanupErr }, `Failed to clean up ${cloneIdentifier}.`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{
|
||||
logger,
|
||||
retryOptions: {
|
||||
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepInput,
|
||||
@@ -12,6 +14,8 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AscApiClient, AscApiClientPostApi } from '../utils/ios/AscApiClient';
|
||||
import { AscApiUtils } from '../utils/ios/AscApiUtils';
|
||||
import { readIpaInfoAsync } from './readIpaInfo';
|
||||
|
||||
export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
@@ -101,34 +105,21 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
|
||||
const client = new AscApiClient({ token, logger: stepsCtx.logger });
|
||||
|
||||
stepsCtx.logger.info('Reading App information...');
|
||||
const appResponse = await client.getAsync(
|
||||
'/v1/apps/:id',
|
||||
{ 'fields[apps]': ['bundleId', 'name'] },
|
||||
{ id: appleAppIdentifier }
|
||||
);
|
||||
stepsCtx.logger.info(
|
||||
`Uploading Build to "${appResponse.data.attributes.name}" (${appResponse.data.attributes.bundleId})...`
|
||||
`Reading App information for Apple app identifier: ${appleAppIdentifier}...`
|
||||
);
|
||||
const appResponse = await AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
|
||||
const ascAppBundleIdentifier = appResponse.data.attributes.bundleId;
|
||||
stepsCtx.logger.info(
|
||||
`Uploading Build to "${appResponse.data.attributes.name}" (${ascAppBundleIdentifier})...`
|
||||
);
|
||||
|
||||
stepsCtx.logger.info('Creating Build Upload...');
|
||||
const buildUploadResponse = await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: bundleShortVersion,
|
||||
cfBundleVersion: bundleVersion,
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: appleAppIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
const buildUploadResponse = await AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
});
|
||||
|
||||
const buildUploadId = buildUploadResponse.data.id;
|
||||
@@ -234,6 +225,9 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
|
||||
stepsCtx.logger.info('Checking build upload status...');
|
||||
const waitingForBuildStartedAt = Date.now();
|
||||
const waitingLogIntervalMs = 10 * 1000;
|
||||
let lastWaitLogTime = 0;
|
||||
let lastWaitLogState: string | null = null;
|
||||
while (Date.now() - waitingForBuildStartedAt < 30 * 60 * 1000 /* 30 minutes */) {
|
||||
const {
|
||||
data: {
|
||||
@@ -246,7 +240,14 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
);
|
||||
|
||||
if (state.state === 'AWAITING_UPLOAD' || state.state === 'PROCESSING') {
|
||||
stepsCtx.logger.info(`Waiting for build upload to complete... (status = ${state.state})`);
|
||||
const now = Date.now();
|
||||
if (lastWaitLogState !== state.state || now - lastWaitLogTime >= waitingLogIntervalMs) {
|
||||
stepsCtx.logger.info(
|
||||
`Waiting for build upload to complete... (status = ${state.state})`
|
||||
);
|
||||
lastWaitLogTime = now;
|
||||
lastWaitLogState = state.state;
|
||||
}
|
||||
await setTimeout(2000);
|
||||
continue;
|
||||
}
|
||||
@@ -265,6 +266,50 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
}
|
||||
|
||||
if (state.state === 'FAILED') {
|
||||
if (isInvalidBundleIdentifierError(errors)) {
|
||||
const ipaInfoResult = await asyncResult(readIpaInfoAsync(ipaPath));
|
||||
const ipaBundleIdentifier = ipaInfoResult.ok
|
||||
? ipaInfoResult.value.bundleIdentifier
|
||||
: null;
|
||||
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID',
|
||||
`Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` +
|
||||
`IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` +
|
||||
`App Store Connect app bundle identifier: ${ascAppBundleIdentifier}\n\n` +
|
||||
'Bundle identifier cannot be changed for an existing App Store Connect app. ' +
|
||||
'If you selected the wrong app, change the Apple app identifier in the submit profile. ' +
|
||||
'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).'
|
||||
);
|
||||
}
|
||||
if (isMissingPurposeStringError(errors)) {
|
||||
const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors);
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING',
|
||||
`Build upload was rejected by App Store Connect because Info.plist is missing one or more privacy purpose strings.\n\n` +
|
||||
`${
|
||||
missingUsageDescriptionKeys.length > 0
|
||||
? `Missing keys reported by App Store Connect:\n- ${missingUsageDescriptionKeys.join(
|
||||
'\n- '
|
||||
)}\n\n`
|
||||
: ''
|
||||
}` +
|
||||
'Add the missing keys with clear user-facing explanations, then rebuild and submit again.\n' +
|
||||
'If you use Continuous Native Generation (CNG), update `ios.infoPlist` in app.json/app.config.js.\n' +
|
||||
'If you do not use CNG, update your app target Info.plist directly.',
|
||||
{
|
||||
docsUrl: 'https://docs.expo.dev/guides/permissions/#ios',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (isClosedVersionTrainError(errors)) {
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN',
|
||||
`Build upload was rejected by App Store Connect because the ${bundleShortVersion} app version is not accepted for new build submissions. ` +
|
||||
'This usually means the version train is closed or lower than a previously approved version. ' +
|
||||
'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version, then rebuild and submit again.'
|
||||
);
|
||||
}
|
||||
throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
|
||||
} else if (state.state === 'COMPLETE') {
|
||||
stepsCtx.logger.info(`Build upload (ID: ${buildUploadId}) complete!`);
|
||||
@@ -279,6 +324,36 @@ function itemizeMessages(messages: { description: string; code: string }[]): str
|
||||
return `- ${messages.map(m => `${m.description} (${m.code})`).join('\n- ')}`;
|
||||
}
|
||||
|
||||
export function isClosedVersionTrainError(messages: { code: string }[]): boolean {
|
||||
return (
|
||||
messages.length > 0 &&
|
||||
messages.every(message => ['90062', '90186', '90478'].includes(message.code))
|
||||
);
|
||||
}
|
||||
|
||||
export function isInvalidBundleIdentifierError(messages: { code: string }[]): boolean {
|
||||
return (
|
||||
messages.length > 0 && messages.every(message => ['90054', '90055'].includes(message.code))
|
||||
);
|
||||
}
|
||||
|
||||
export function isMissingPurposeStringError(messages: { code: string }[]): boolean {
|
||||
return messages.length > 0 && messages.every(message => message.code === '90683');
|
||||
}
|
||||
|
||||
export function parseMissingUsageDescriptionKeys(messages: { description: string }[]): string[] {
|
||||
const usageDescriptionKeyRegex = /\b(\w+UsageDescription)\b/g;
|
||||
const keys = new Set<string>();
|
||||
for (const message of messages) {
|
||||
const matches = message.description.matchAll(usageDescriptionKeyRegex);
|
||||
for (const match of matches) {
|
||||
keys.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
async function uploadChunksAsync({
|
||||
uploadOperations,
|
||||
ipaPath,
|
||||
|
||||
@@ -11,7 +11,43 @@ type ApiSchema = {
|
||||
};
|
||||
};
|
||||
|
||||
const AscErrorResponseSchema = z.object({
|
||||
errors: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
code: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
detail: z.string().optional(),
|
||||
source: z.unknown().optional(),
|
||||
})
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
const GetApi = {
|
||||
'/v1/apps': {
|
||||
path: z.object({}),
|
||||
request: z.object({
|
||||
'fields[apps]': z.array(z.enum(['bundleId', 'name'])).refine(opts => {
|
||||
return opts.includes('bundleId') && opts.includes('name');
|
||||
}),
|
||||
limit: z.number().int().min(1).max(200).optional(),
|
||||
}),
|
||||
response: z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
type: z.literal('apps'),
|
||||
id: z.string(),
|
||||
attributes: z.object({
|
||||
bundleId: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
'/v1/apps/:id': {
|
||||
path: z.object({
|
||||
id: z.string(),
|
||||
@@ -222,6 +258,13 @@ const PatchApi = {
|
||||
},
|
||||
} satisfies ApiSchema;
|
||||
|
||||
export type AscApiClientGetApi = {
|
||||
[Path in keyof typeof GetApi]: {
|
||||
request: z.input<(typeof GetApi)[Path]['request']>;
|
||||
response: z.output<(typeof GetApi)[Path]['response']>;
|
||||
};
|
||||
};
|
||||
|
||||
export type AscApiClientPostApi = {
|
||||
[Path in keyof typeof PostApi]: {
|
||||
request: z.input<(typeof PostApi)[Path]['request']>;
|
||||
@@ -236,6 +279,17 @@ export type AscApiClientPatchApi = {
|
||||
};
|
||||
};
|
||||
|
||||
export class AscApiRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
public readonly responseJson: z.output<typeof AscErrorResponseSchema>,
|
||||
options?: { cause?: unknown }
|
||||
) {
|
||||
super(message, { cause: options?.cause });
|
||||
}
|
||||
}
|
||||
|
||||
export class AscApiClient {
|
||||
private readonly baseUrl = 'https://api.appstoreconnect.apple.com';
|
||||
private readonly token: string;
|
||||
@@ -346,12 +400,31 @@ export class AscApiClient {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const parsedAscErrorResponse = await asyncResult(
|
||||
(async () => AscErrorResponseSchema.parse(JSON.parse(text)))()
|
||||
);
|
||||
if (parsedAscErrorResponse.ok) {
|
||||
throw new AscApiRequestError(
|
||||
`Unexpected response (${response.status}) from App Store Connect: ${text}`,
|
||||
response.status,
|
||||
parsedAscErrorResponse.value,
|
||||
{ cause: response }
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected response (${response.status}) from App Store Connect: ${text}`, {
|
||||
cause: response,
|
||||
});
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const text = await response.text();
|
||||
const parsedJson = await asyncResult((async () => JSON.parse(text))());
|
||||
if (!parsedJson.ok) {
|
||||
throw new Error(
|
||||
`Malformed JSON response from App Store Connect (${response.status}): ${text}`
|
||||
);
|
||||
}
|
||||
const json = parsedJson.value;
|
||||
|
||||
this.logger?.debug(`Response from App Store Connect: ${JSON.stringify(json, null, 2)}`);
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
|
||||
import {
|
||||
AscApiClient,
|
||||
AscApiClientGetApi,
|
||||
AscApiClientPostApi,
|
||||
AscApiRequestError,
|
||||
} from './AscApiClient';
|
||||
|
||||
export namespace AscApiUtils {
|
||||
export async function getAppInfoAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
}: {
|
||||
client: Pick<AscApiClient, 'getAsync'>;
|
||||
appleAppIdentifier: string;
|
||||
}): Promise<AscApiClientGetApi['/v1/apps/:id']['response']> {
|
||||
try {
|
||||
return await client.getAsync(
|
||||
'/v1/apps/:id',
|
||||
{ 'fields[apps]': ['bundleId', 'name'] },
|
||||
{ id: appleAppIdentifier }
|
||||
);
|
||||
} catch (error) {
|
||||
const notFoundErrors =
|
||||
error instanceof AscApiRequestError && error.status === 404
|
||||
? error.responseJson.errors
|
||||
: [];
|
||||
const isAppNotFoundError =
|
||||
notFoundErrors.length > 0 && notFoundErrors.every(item => item.code === 'NOT_FOUND');
|
||||
if (!isAppNotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let visibleAppsSummary: string | null = null;
|
||||
try {
|
||||
visibleAppsSummary = await getVisibleAppsSummaryAsync(client);
|
||||
} catch {
|
||||
// Don't hide the original NOT_FOUND error with a secondary lookup failure.
|
||||
throw error;
|
||||
}
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_APP_NOT_FOUND',
|
||||
`App Store Connect app for application identifier ${appleAppIdentifier} was not found. ` +
|
||||
'Verify the configured application identifier and that the App Store Connect API key has access to the application in the correct App Store Connect account.' +
|
||||
(visibleAppsSummary
|
||||
? `\n\nExample applications visible to this API key:\n${visibleAppsSummary}`
|
||||
: ''),
|
||||
{
|
||||
docsUrl: 'https://expo.fyi/asc-app-id',
|
||||
cause: error,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
}: {
|
||||
client: Pick<AscApiClient, 'postAsync'>;
|
||||
appleAppIdentifier: string;
|
||||
bundleShortVersion: string;
|
||||
bundleVersion: string;
|
||||
}): Promise<AscApiClientPostApi['/v1/buildUploads']['response']> {
|
||||
try {
|
||||
return await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: bundleShortVersion,
|
||||
cfBundleVersion: bundleVersion,
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: appleAppIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errors =
|
||||
error instanceof AscApiRequestError && error.status === 409
|
||||
? error.responseJson.errors
|
||||
: [];
|
||||
const isDuplicateVersionError =
|
||||
errors.length > 0 &&
|
||||
errors.every(item => item.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE');
|
||||
|
||||
if (isDuplicateVersionError) {
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE',
|
||||
`Increment Build Number: Build number ${bundleVersion} for app version ${bundleShortVersion} has already been used. ` +
|
||||
'App Store Connect requires unique build numbers within each app version (version train). ' +
|
||||
'Increment it by setting ios.buildNumber in app.json, or set "autoIncrement": true in eas.json (recommended). Then rebuild and resubmit.',
|
||||
{
|
||||
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
||||
cause: error,
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getVisibleAppsSummaryAsync(
|
||||
client: Pick<AscApiClient, 'getAsync'>
|
||||
): Promise<string | null> {
|
||||
const appsResponse = await client.getAsync('/v1/apps', {
|
||||
'fields[apps]': ['bundleId', 'name'],
|
||||
limit: 10,
|
||||
});
|
||||
if (appsResponse.data.length === 0) {
|
||||
return ' (none)';
|
||||
}
|
||||
return appsResponse.data
|
||||
.map(app => `- ${app.attributes.name} (${app.attributes.bundleId}) (ID: ${app.id})`)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import { AscApiClient } from '../AscApiClient';
|
||||
import { AscApiClient, AscApiRequestError } from '../AscApiClient';
|
||||
|
||||
// nock needs real fetch implementation
|
||||
jest.unmock('node-fetch');
|
||||
@@ -280,4 +280,54 @@ describe(AscApiClient, () => {
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('throws AscApiRequestError for structured ASC error payload', async () => {
|
||||
const appId = '1491144534';
|
||||
const responseFixture = {
|
||||
errors: [
|
||||
{
|
||||
status: '409',
|
||||
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE',
|
||||
title:
|
||||
'The provided entity includes an attribute with a value that has already been used',
|
||||
detail: 'The bundle version must be higher than the previously uploaded version.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(409, responseFixture);
|
||||
|
||||
await expect(
|
||||
client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appId })
|
||||
).rejects.toBeInstanceOf(AscApiRequestError);
|
||||
});
|
||||
|
||||
it('throws regular Error for non-structured ASC error payload', async () => {
|
||||
const appId = '1491144534';
|
||||
|
||||
nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(503, 'Service unavailable');
|
||||
|
||||
await expect(
|
||||
client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appId })
|
||||
).rejects.not.toBeInstanceOf(AscApiRequestError);
|
||||
});
|
||||
|
||||
it('throws controlled error when success response body is not valid JSON', async () => {
|
||||
const appId = '1491144534';
|
||||
|
||||
nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(200, 'not-json');
|
||||
|
||||
await expect(
|
||||
client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appId })
|
||||
).rejects.toThrow('Malformed JSON response from App Store Connect (200): not-json');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import { AscApiRequestError } from '../AscApiClient';
|
||||
import { AscApiUtils } from '../AscApiUtils';
|
||||
|
||||
describe('AscApiUtils', () => {
|
||||
describe('getAppInfoAsync', () => {
|
||||
it('returns app info when lookup succeeds', async () => {
|
||||
const response = {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: '1491144534',
|
||||
attributes: {
|
||||
name: 'Example App',
|
||||
bundleId: 'com.example.app',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const client = {
|
||||
getAsync: jest.fn().mockResolvedValue(response),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier: '1491144534' })
|
||||
).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it('throws UserFacingError with visible apps when app id is not found', async () => {
|
||||
const notFoundPayload = {
|
||||
errors: [
|
||||
{
|
||||
status: '404',
|
||||
code: 'NOT_FOUND',
|
||||
detail: "There is no resource of type 'apps' with id '1234567890'",
|
||||
},
|
||||
],
|
||||
};
|
||||
const notFoundError = new AscApiRequestError(
|
||||
'Unexpected response (404) from App Store Connect',
|
||||
404,
|
||||
notFoundPayload
|
||||
);
|
||||
const client = {
|
||||
getAsync: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(notFoundError)
|
||||
.mockResolvedValueOnce({
|
||||
data: [
|
||||
{
|
||||
type: 'apps',
|
||||
id: '1111111111',
|
||||
attributes: { name: 'Visible App', bundleId: 'com.visible.app' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier: '1234567890' })
|
||||
).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorCode: 'EAS_UPLOAD_TO_ASC_APP_NOT_FOUND',
|
||||
docsUrl: 'https://expo.fyi/asc-app-id',
|
||||
message: expect.stringContaining(
|
||||
'App Store Connect app for application identifier 1234567890 was not found'
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rethrows original not-found error when app-list lookup fails', async () => {
|
||||
const notFoundPayload = {
|
||||
errors: [
|
||||
{
|
||||
status: '404',
|
||||
code: 'NOT_FOUND',
|
||||
},
|
||||
],
|
||||
};
|
||||
const notFoundError = new AscApiRequestError(
|
||||
'Unexpected response (404) from App Store Connect',
|
||||
404,
|
||||
notFoundPayload
|
||||
);
|
||||
|
||||
const listingError = new Error('listing failed');
|
||||
const client = {
|
||||
getAsync: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(notFoundError)
|
||||
.mockRejectedValueOnce(listingError),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier: '1234567890' })
|
||||
).rejects.toBe(notFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBuildUploadAsync', () => {
|
||||
it('throws UserFacingError when ASC duplicate version error is returned', async () => {
|
||||
const payload = {
|
||||
errors: [
|
||||
{
|
||||
status: '409',
|
||||
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE',
|
||||
detail: 'The bundle version must be higher than the previously uploaded version.',
|
||||
},
|
||||
],
|
||||
};
|
||||
const duplicateError = new AscApiRequestError(
|
||||
'Unexpected response (409) from App Store Connect',
|
||||
409,
|
||||
payload
|
||||
);
|
||||
|
||||
const client = {
|
||||
postAsync: jest.fn().mockRejectedValue(duplicateError),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
})
|
||||
).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorCode: 'EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE',
|
||||
message: expect.stringContaining('Increment Build Number'),
|
||||
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns response when upload initialization succeeds', async () => {
|
||||
const response = {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: 'fdf9c476-aaa4-4ead-b91c-6e3cc3a47805',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const client = {
|
||||
postAsync: jest.fn().mockResolvedValue(response),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
})
|
||||
).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it('rethrows when error payload includes mixed error codes', async () => {
|
||||
const payload = {
|
||||
errors: [
|
||||
{
|
||||
status: '409',
|
||||
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE',
|
||||
},
|
||||
{
|
||||
status: '409',
|
||||
code: 'SOME_OTHER_ERROR',
|
||||
},
|
||||
],
|
||||
};
|
||||
const mixedError = new AscApiRequestError(
|
||||
'Unexpected response (409) from App Store Connect',
|
||||
409,
|
||||
payload
|
||||
);
|
||||
const client = {
|
||||
postAsync: jest.fn().mockRejectedValue(mixedError),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
})
|
||||
).rejects.toBe(mixedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
||||
import assert from 'assert';
|
||||
import FastGlob from 'fast-glob';
|
||||
@@ -17,6 +18,8 @@ export type AndroidDeviceName = string & z.BRAND<'AndroidDeviceName'>;
|
||||
export type AndroidDeviceSerialId = string & z.BRAND<'AndroidDeviceSerialId'>;
|
||||
|
||||
export namespace AndroidEmulatorUtils {
|
||||
const RETRY_INTERVAL_MS = 1_000;
|
||||
|
||||
export const defaultSystemImagePackage = `system-images;android-30;default;${
|
||||
process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64'
|
||||
}`;
|
||||
@@ -352,10 +355,15 @@ export namespace AndroidEmulatorUtils {
|
||||
export async function waitForReadyAsync({
|
||||
serialId,
|
||||
env,
|
||||
timeoutMs = 3 * 60 * 1_000,
|
||||
logger,
|
||||
}: {
|
||||
serialId: AndroidDeviceSerialId;
|
||||
env: NodeJS.ProcessEnv;
|
||||
timeoutMs?: number;
|
||||
logger?: bunyan;
|
||||
}): Promise<void> {
|
||||
const retries = Math.max(0, Math.ceil(timeoutMs / RETRY_INTERVAL_MS) - 1);
|
||||
await retryAsync(
|
||||
async () => {
|
||||
const { stdout } = await spawn(
|
||||
@@ -367,17 +375,48 @@ export namespace AndroidEmulatorUtils {
|
||||
if (!stdout.startsWith('1')) {
|
||||
throw new Error(`Emulator (${serialId}) boot has not completed.`);
|
||||
}
|
||||
|
||||
const hasNetworkConnection = await hasNetworkConnectionAsync({ serialId, env });
|
||||
if (!hasNetworkConnection) {
|
||||
throw new Error(`Emulator (${serialId}) network is not ready.`);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Retry every second for 3 minutes.
|
||||
retryOptions: {
|
||||
retries: 3 * 60,
|
||||
retryIntervalMs: 1_000,
|
||||
retries,
|
||||
retryIntervalMs: RETRY_INTERVAL_MS,
|
||||
},
|
||||
logger,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function hasNetworkConnectionAsync({
|
||||
serialId,
|
||||
env,
|
||||
}: {
|
||||
serialId: AndroidDeviceSerialId;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<boolean> {
|
||||
const networkReadyCheckCommand = env.ANDROID_EMULATOR_NETWORK_READY_COMMAND?.trim();
|
||||
if (networkReadyCheckCommand) {
|
||||
const customNetworkCheckResult = await asyncResult(
|
||||
spawn('adb', ['-s', serialId, 'shell', networkReadyCheckCommand], { env })
|
||||
);
|
||||
return customNetworkCheckResult.ok;
|
||||
}
|
||||
|
||||
const netcatResult = await asyncResult(
|
||||
spawn(
|
||||
'adb',
|
||||
['-s', serialId, 'shell', 'nc', '-w', '1', '1.1.1.1', '443'],
|
||||
// Close stdin to make netcat exit cleanly on Android images that don't support `-z`.
|
||||
{ env, stdio: ['ignore', 'pipe', 'pipe'] }
|
||||
)
|
||||
);
|
||||
return netcatResult.ok;
|
||||
}
|
||||
|
||||
export async function collectLogsAsync({
|
||||
serialId,
|
||||
env,
|
||||
@@ -419,18 +458,13 @@ export namespace AndroidEmulatorUtils {
|
||||
return { outputPath };
|
||||
}
|
||||
|
||||
export async function deleteAsync({
|
||||
export async function stopAsync({
|
||||
serialId,
|
||||
env,
|
||||
}: {
|
||||
serialId: AndroidDeviceSerialId;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
const adbEmuAvdName = await spawn('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
||||
env,
|
||||
});
|
||||
const deviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0];
|
||||
|
||||
await spawn('adb', ['-s', serialId, 'emu', 'kill'], { env });
|
||||
|
||||
await retryAsync(
|
||||
@@ -443,12 +477,40 @@ export namespace AndroidEmulatorUtils {
|
||||
{
|
||||
retryOptions: {
|
||||
retries: 3 * 60,
|
||||
retryIntervalMs: 1_000,
|
||||
retryIntervalMs: RETRY_INTERVAL_MS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await spawn('avdmanager', ['delete', 'avd', '-n', deviceName], { env });
|
||||
export async function deleteAsync({
|
||||
serialId,
|
||||
deviceName,
|
||||
env,
|
||||
}: {
|
||||
serialId?: AndroidDeviceSerialId;
|
||||
deviceName?: AndroidVirtualDeviceName;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
let resolvedDeviceName = deviceName;
|
||||
if (!resolvedDeviceName && serialId) {
|
||||
const adbEmuAvdName = await spawn('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
||||
env,
|
||||
});
|
||||
resolvedDeviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0] as
|
||||
| AndroidVirtualDeviceName
|
||||
| undefined;
|
||||
}
|
||||
|
||||
assert(resolvedDeviceName, 'Either "serialId" or "deviceName" must resolve to a device name.');
|
||||
|
||||
const serialIdToStop =
|
||||
serialId ?? (await getSerialIdAsync({ deviceName: resolvedDeviceName, env })) ?? undefined;
|
||||
if (serialIdToStop) {
|
||||
await stopAsync({ serialId: serialIdToStop, env });
|
||||
}
|
||||
|
||||
await spawn('avdmanager', ['delete', 'avd', '-n', resolvedDeviceName], { env });
|
||||
}
|
||||
|
||||
export async function startScreenRecordingAsync({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AndroidEmulatorUtils,
|
||||
AndroidVirtualDeviceName,
|
||||
} from '../AndroidEmulatorUtils';
|
||||
import { retryAsync } from '../retry';
|
||||
|
||||
// We need to use real fs for cloning devices to work.
|
||||
jest.unmock('fs');
|
||||
@@ -104,7 +105,10 @@ describe('AndroidEmulatorUtils', () => {
|
||||
});
|
||||
expect(stdout).toContain('data');
|
||||
|
||||
await spawn('adb', ['-s', serialId, 'emu', 'kill'], { env: process.env });
|
||||
await AndroidEmulatorUtils.stopAsync({
|
||||
serialId,
|
||||
env: process.env,
|
||||
});
|
||||
await asyncResult(emulatorPromise);
|
||||
|
||||
const cloneDeviceName = (deviceName + '-clone') as AndroidVirtualDeviceName;
|
||||
@@ -131,12 +135,133 @@ describe('AndroidEmulatorUtils', () => {
|
||||
expect(stdoutClone).toContain('data');
|
||||
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId,
|
||||
deviceName,
|
||||
env: process.env,
|
||||
});
|
||||
await asyncResult(emulatorPromiseClone);
|
||||
}, 60_000);
|
||||
|
||||
it('should delete a running emulator by device name', async () => {
|
||||
const deviceName = 'android-emulator-delete-by-name-test' as AndroidVirtualDeviceName;
|
||||
const avdPath = `${process.env.HOME}/.android/avd/${deviceName}.avd`;
|
||||
|
||||
await AndroidEmulatorUtils.createAsync({
|
||||
deviceName,
|
||||
systemImagePackage: AndroidEmulatorUtils.defaultSystemImagePackage,
|
||||
deviceIdentifier: null,
|
||||
env: process.env,
|
||||
logger: createMockLogger({ logToConsole: true }),
|
||||
});
|
||||
|
||||
const { serialId, emulatorPromise } = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName,
|
||||
env: { ...process.env, ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: '1' },
|
||||
});
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
deviceName,
|
||||
env: process.env,
|
||||
});
|
||||
await asyncResult(emulatorPromise);
|
||||
|
||||
const devices = await AndroidEmulatorUtils.getAttachedDevicesAsync({ env: process.env });
|
||||
expect(devices.map(device => device.serialId)).not.toContain(serialId);
|
||||
|
||||
await expect(fs.promises.access(avdPath)).rejects.toThrow();
|
||||
}, 60_000);
|
||||
|
||||
it('should retry when first startup forces network check command failure', async () => {
|
||||
const deviceName = 'android-emulator-network-retry-test' as AndroidVirtualDeviceName;
|
||||
let attemptCounter = 0;
|
||||
let sawNetworkNotReadyError = false;
|
||||
|
||||
await retryAsync(
|
||||
async attemptCount => {
|
||||
attemptCounter += 1;
|
||||
const shouldFailNetworkCheck = attemptCount === 0;
|
||||
const envForAttempt: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: '1',
|
||||
ANDROID_EMULATOR_NETWORK_READY_COMMAND: shouldFailNetworkCheck
|
||||
? 'echo "network not ready" && exit 1'
|
||||
: 'echo "network ready"',
|
||||
};
|
||||
|
||||
let serialId: AndroidDeviceSerialId | null = null;
|
||||
let emulatorPromise: Promise<unknown> | null = null;
|
||||
|
||||
try {
|
||||
await AndroidEmulatorUtils.createAsync({
|
||||
deviceName,
|
||||
systemImagePackage: AndroidEmulatorUtils.defaultSystemImagePackage,
|
||||
deviceIdentifier: null,
|
||||
env: envForAttempt,
|
||||
logger: createMockLogger({ logToConsole: true }),
|
||||
});
|
||||
|
||||
const startResult = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName,
|
||||
env: envForAttempt,
|
||||
});
|
||||
serialId = startResult.serialId;
|
||||
emulatorPromise = asyncResult(startResult.emulatorPromise);
|
||||
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId,
|
||||
env: envForAttempt,
|
||||
timeoutMs: 30_000,
|
||||
logger: createMockLogger({ logToConsole: true }),
|
||||
});
|
||||
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId,
|
||||
deviceName,
|
||||
env: process.env,
|
||||
});
|
||||
await emulatorPromise;
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes('network is not ready')) {
|
||||
sawNetworkNotReadyError = true;
|
||||
}
|
||||
try {
|
||||
if (serialId) {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId,
|
||||
deviceName,
|
||||
env: process.env,
|
||||
});
|
||||
} else {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
deviceName,
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Cleanup failed during retry test', cleanupError);
|
||||
}
|
||||
if (emulatorPromise) {
|
||||
await emulatorPromise;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{
|
||||
logger: createMockLogger({ logToConsole: true }),
|
||||
retryOptions: {
|
||||
retries: 1,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(sawNetworkNotReadyError).toBe(true);
|
||||
expect(attemptCounter).toBe(2);
|
||||
}, 360_000);
|
||||
|
||||
it('should work with screen recording', async () => {
|
||||
const deviceName = 'android-emulator-screen-recording-test' as AndroidVirtualDeviceName;
|
||||
await AndroidEmulatorUtils.createAsync({
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
import { AndroidEmulatorUtils } from '../AndroidEmulatorUtils';
|
||||
import { retryAsync } from '../retry';
|
||||
|
||||
jest.mock('@expo/turtle-spawn', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../retry', () => ({
|
||||
retryAsync: jest.fn(async fn => await fn(0)),
|
||||
}));
|
||||
|
||||
const mockedSpawn = jest.mocked(spawn);
|
||||
const mockedRetryAsync = jest.mocked(retryAsync);
|
||||
|
||||
describe('AndroidEmulatorUtils', () => {
|
||||
beforeEach(() => {
|
||||
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
|
||||
mockedRetryAsync.mockImplementation(async fn => await fn(0));
|
||||
});
|
||||
|
||||
describe(AndroidEmulatorUtils.waitForReadyAsync, () => {
|
||||
it('checks boot completion and verifies network with netcat to 1.1.1.1:443', async () => {
|
||||
mockedSpawn.mockImplementation((async (_command: string, args: string[]) => {
|
||||
if (args[3] === 'getprop') {
|
||||
return { stdout: '1\n', stderr: '' } as any;
|
||||
}
|
||||
if (args[3] === 'nc' && args[6] === '1.1.1.1' && args[7] === '443') {
|
||||
return { stdout: '', stderr: '' } as any;
|
||||
}
|
||||
throw new Error(`Unexpected adb command args: ${args.join(' ')}`);
|
||||
}) as any);
|
||||
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId: 'emulator-5554' as any,
|
||||
env: process.env,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
|
||||
expect(mockedRetryAsync).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
retryOptions: expect.objectContaining({
|
||||
retries: 59,
|
||||
retryIntervalMs: 1_000,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(mockedSpawn).toHaveBeenCalledWith(
|
||||
'adb',
|
||||
['-s', 'emulator-5554', 'shell', 'nc', '-w', '1', '1.1.1.1', '443'],
|
||||
{ env: process.env, stdio: ['ignore', 'pipe', 'pipe'] }
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when network is unavailable despite completed boot', async () => {
|
||||
mockedSpawn.mockImplementation((async (_command: string, args: string[]) => {
|
||||
if (args[3] === 'getprop') {
|
||||
return { stdout: '1\n', stderr: '' } as any;
|
||||
}
|
||||
if (args[3] === 'nc') {
|
||||
throw new Error('network unreachable');
|
||||
}
|
||||
throw new Error(`Unexpected adb command args: ${args.join(' ')}`);
|
||||
}) as any);
|
||||
|
||||
await expect(
|
||||
AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId: 'emulator-5554' as any,
|
||||
env: process.env,
|
||||
})
|
||||
).rejects.toThrow('network is not ready');
|
||||
});
|
||||
|
||||
it('uses overridden network readiness command when provided', async () => {
|
||||
mockedSpawn.mockImplementation((async (_command: string, args: string[]) => {
|
||||
if (args[3] === 'getprop') {
|
||||
return { stdout: '1\n', stderr: '' } as any;
|
||||
}
|
||||
if (args[3] === 'exit 0') {
|
||||
return { stdout: '', stderr: '' } as any;
|
||||
}
|
||||
throw new Error(`Unexpected adb command args: ${args.join(' ')}`);
|
||||
}) as any);
|
||||
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId: 'emulator-5554' as any,
|
||||
env: {
|
||||
...process.env,
|
||||
ANDROID_EMULATOR_NETWORK_READY_COMMAND: 'exit 0',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedSpawn).toHaveBeenCalledWith(
|
||||
'adb',
|
||||
['-s', 'emulator-5554', 'shell', 'exit 0'],
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
ANDROID_EMULATOR_NETWORK_READY_COMMAND: 'exit 0',
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(mockedSpawn).not.toHaveBeenCalledWith(
|
||||
'adb',
|
||||
['-s', 'emulator-5554', 'shell', 'nc', '-w', '1', '1.1.1.1', '443'],
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(AndroidEmulatorUtils.stopAsync, () => {
|
||||
it('kills emulator and waits for it to detach', async () => {
|
||||
mockedSpawn.mockImplementation((async (_command: string, args: string[]) => {
|
||||
if (args[2] === 'emu' && args[3] === 'kill') {
|
||||
return { stdout: '', stderr: '' } as any;
|
||||
}
|
||||
if (args[0] === 'devices') {
|
||||
return { stdout: 'List of devices attached\n\n', stderr: '' } as any;
|
||||
}
|
||||
throw new Error(`Unexpected adb command args: ${args.join(' ')}`);
|
||||
}) as any);
|
||||
|
||||
await AndroidEmulatorUtils.stopAsync({
|
||||
serialId: 'emulator-5554' as any,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(mockedSpawn).toHaveBeenCalledWith('adb', ['-s', 'emulator-5554', 'emu', 'kill'], {
|
||||
env: process.env,
|
||||
});
|
||||
expect(mockedSpawn).toHaveBeenCalledWith('adb', ['devices', '-l'], {
|
||||
env: process.env,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(AndroidEmulatorUtils.deleteAsync, () => {
|
||||
it('stops by serial id and then deletes the AVD', async () => {
|
||||
mockedSpawn.mockResolvedValue({ stdout: '', stderr: '' } as any);
|
||||
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId: 'emulator-5554' as any,
|
||||
deviceName: 'eas-simulator-1' as any,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(mockedSpawn).toHaveBeenCalledWith('adb', ['-s', 'emulator-5554', 'emu', 'kill'], {
|
||||
env: process.env,
|
||||
});
|
||||
expect(mockedSpawn).toHaveBeenCalledWith(
|
||||
'avdmanager',
|
||||
['delete', 'avd', '-n', 'eas-simulator-1'],
|
||||
{
|
||||
env: process.env,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.0.3",
|
||||
"version": "18.2.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.0.2"
|
||||
"@expo/steps": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-build-job",
|
||||
"version": "18.0.2",
|
||||
"version": "18.2.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "MIT",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,4 +1,60 @@
|
||||
import { UserFacingError } from '../errors';
|
||||
import { BuildError, ExpoError, UserFacingError } from '../errors';
|
||||
import { BuildPhase } from '../logs';
|
||||
|
||||
class TestExpoError extends ExpoError {
|
||||
constructor(message: string, details: ConstructorParameters<typeof BuildError>[1]) {
|
||||
super(message, details);
|
||||
}
|
||||
}
|
||||
|
||||
describe(ExpoError, () => {
|
||||
it('stores shared metadata and formats external payload', () => {
|
||||
const cause = new Error('root cause');
|
||||
const error = new TestExpoError('canonical message', {
|
||||
errorCode: 'ERR_CODE',
|
||||
trackingCode: 'TRACKING_CODE',
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
buildPhase: BuildPhase.PREBUILD,
|
||||
metadata: { packageName: '@typescript-eslint/typescript-estree' },
|
||||
cause,
|
||||
});
|
||||
|
||||
expect(error.errorCode).toBe('ERR_CODE');
|
||||
expect(error.trackingCode).toBe('TRACKING_CODE');
|
||||
expect(error.docsUrl).toBe('https://docs.example.dev');
|
||||
expect(error.metadata).toEqual({
|
||||
packageName: '@typescript-eslint/typescript-estree',
|
||||
});
|
||||
expect(error.buildPhase).toBe(BuildPhase.PREBUILD);
|
||||
expect(error.cause).toBe(cause);
|
||||
expect(error.format()).toEqual({
|
||||
errorCode: 'ERR_CODE',
|
||||
message: 'canonical message',
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
buildPhase: BuildPhase.PREBUILD,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(BuildError, () => {
|
||||
it('formats using canonical message and errorCode', () => {
|
||||
const error = new BuildError('canonical message', {
|
||||
errorCode: 'ERR_CODE',
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
buildPhase: BuildPhase.PREBUILD,
|
||||
metadata: { packageName: '@expo/config' },
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(ExpoError);
|
||||
expect(error.metadata).toEqual({ packageName: '@expo/config' });
|
||||
expect(error.format()).toEqual({
|
||||
errorCode: 'ERR_CODE',
|
||||
message: 'canonical message',
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
buildPhase: BuildPhase.PREBUILD,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(UserFacingError, () => {
|
||||
it('supports docsUrl in options', () => {
|
||||
@@ -6,6 +62,7 @@ describe(UserFacingError, () => {
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(ExpoError);
|
||||
expect(error.docsUrl).toBe('https://docs.example.dev');
|
||||
expect(error.cause).toBeUndefined();
|
||||
});
|
||||
@@ -17,14 +74,16 @@ describe(UserFacingError, () => {
|
||||
expect(error.cause).toBe(cause);
|
||||
});
|
||||
|
||||
it('supports docsUrl and cause in options', () => {
|
||||
it('supports docsUrl, metadata, and cause in options', () => {
|
||||
const cause = new Error('root cause');
|
||||
const error = new UserFacingError('ERR_CODE', 'message', {
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
metadata: { packageName: 'expo' },
|
||||
cause,
|
||||
});
|
||||
|
||||
expect(error.docsUrl).toBe('https://docs.example.dev');
|
||||
expect(error.metadata).toEqual({ packageName: 'expo' });
|
||||
expect(error.cause).toBe(cause);
|
||||
});
|
||||
});
|
||||
@@ -17,52 +17,79 @@ export interface ExternalBuildError {
|
||||
buildPhase?: BuildPhase;
|
||||
}
|
||||
|
||||
interface BuildErrorDetails {
|
||||
export type ErrorMetadata = Record<string, unknown>;
|
||||
|
||||
interface ExpoErrorDetails<TMetadata extends ErrorMetadata = ErrorMetadata> {
|
||||
errorCode: string;
|
||||
userFacingMessage: string;
|
||||
userFacingErrorCode: string;
|
||||
trackingCode?: string;
|
||||
docsUrl?: string;
|
||||
innerError?: Error;
|
||||
buildPhase?: BuildPhase;
|
||||
/**
|
||||
* Metadata object for the error. Used internally for Sentry/logging/debugging.
|
||||
* It is not included in the external build error payload.
|
||||
*/
|
||||
metadata?: TMetadata;
|
||||
/**
|
||||
* Underlying error that caused this error to be created. Used internally to
|
||||
* propagate blame stack traces to the response.
|
||||
*/
|
||||
cause?: unknown;
|
||||
}
|
||||
|
||||
export class BuildError extends Error {
|
||||
export abstract class ExpoError<TMetadata extends ErrorMetadata = ErrorMetadata> extends Error {
|
||||
public errorCode: string;
|
||||
public userFacingMessage: string;
|
||||
public userFacingErrorCode: string;
|
||||
// Internal-only classification used for Sentry, analytics, and worker internalErrorCode.
|
||||
// The public error saved on builds and job runs is always `errorCode`.
|
||||
public trackingCode?: string;
|
||||
public docsUrl?: string;
|
||||
public innerError?: Error;
|
||||
public readonly metadata?: TMetadata;
|
||||
public buildPhase?: BuildPhase;
|
||||
|
||||
constructor(message: string, details: BuildErrorDetails) {
|
||||
super(message);
|
||||
constructor(message: string, details: ExpoErrorDetails<TMetadata>) {
|
||||
super(message, { cause: details.cause });
|
||||
this.errorCode = details.errorCode;
|
||||
this.userFacingErrorCode = details.userFacingErrorCode;
|
||||
this.userFacingMessage = details.userFacingMessage;
|
||||
this.trackingCode = details.trackingCode;
|
||||
this.docsUrl = details.docsUrl;
|
||||
this.innerError = details.innerError;
|
||||
this.metadata = details.metadata;
|
||||
this.buildPhase = details.buildPhase;
|
||||
}
|
||||
|
||||
public format(): ExternalBuildError {
|
||||
return {
|
||||
errorCode: this.userFacingErrorCode,
|
||||
message: this.userFacingMessage,
|
||||
errorCode: this.errorCode,
|
||||
message: this.message,
|
||||
docsUrl: this.docsUrl,
|
||||
buildPhase: this.buildPhase,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class UserFacingError extends Error {
|
||||
public docsUrl?: string;
|
||||
export class BuildError<
|
||||
TMetadata extends ErrorMetadata = ErrorMetadata,
|
||||
> extends ExpoError<TMetadata> {
|
||||
constructor(message: string, details: ExpoErrorDetails<TMetadata>) {
|
||||
super(message, details);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserFacingError<
|
||||
TMetadata extends ErrorMetadata = ErrorMetadata,
|
||||
> extends ExpoError<TMetadata> {
|
||||
constructor(
|
||||
public errorCode: string,
|
||||
public message: string,
|
||||
options?: { docsUrl?: string; cause?: unknown }
|
||||
options?: {
|
||||
docsUrl?: string;
|
||||
metadata?: TMetadata;
|
||||
cause?: unknown;
|
||||
}
|
||||
) {
|
||||
super(message, { cause: options?.cause });
|
||||
this.docsUrl = options?.docsUrl;
|
||||
super(message, {
|
||||
errorCode,
|
||||
docsUrl: options?.docsUrl,
|
||||
metadata: options?.metadata,
|
||||
cause: options?.cause,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +110,6 @@ export class UnknownBuildError extends BuildError {
|
||||
const message = 'Unknown error. See logs for more information.';
|
||||
super(message, {
|
||||
errorCode,
|
||||
userFacingMessage: message,
|
||||
userFacingErrorCode: errorCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -95,8 +120,6 @@ export class UnknownCustomBuildError extends BuildError {
|
||||
const message = 'Unknown custom build error. See logs for more information.';
|
||||
super(message, {
|
||||
errorCode,
|
||||
userFacingMessage: message,
|
||||
userFacingErrorCode: errorCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export enum BuildPhase {
|
||||
EAS_BUILD_INTERNAL = 'EAS_BUILD_INTERNAL',
|
||||
PREBUILD = 'PREBUILD',
|
||||
PREPARE_CREDENTIALS = 'PREPARE_CREDENTIALS',
|
||||
CONFIGURE_ANDROID_VERSION = 'CONFIGURE_ANDROID_VERSION',
|
||||
CALCULATE_EXPO_UPDATES_RUNTIME_VERSION = 'CALCULATE_EXPO_UPDATES_RUNTIME_VERSION',
|
||||
CONFIGURE_EXPO_UPDATES = 'CONFIGURE_EXPO_UPDATES',
|
||||
EAGER_BUNDLE = 'EAGER_BUNDLE',
|
||||
@@ -80,6 +81,7 @@ export const buildPhaseDisplayName: Record<BuildPhase, string> = {
|
||||
[BuildPhase.EAS_BUILD_INTERNAL]: 'Resolve build configuration',
|
||||
[BuildPhase.PREBUILD]: 'Prebuild',
|
||||
[BuildPhase.PREPARE_CREDENTIALS]: 'Prepare credentials',
|
||||
[BuildPhase.CONFIGURE_ANDROID_VERSION]: 'Configure Android version',
|
||||
[BuildPhase.CALCULATE_EXPO_UPDATES_RUNTIME_VERSION]: 'Calculate expo-updates runtime version',
|
||||
[BuildPhase.CONFIGURE_EXPO_UPDATES]: 'Configure expo-updates',
|
||||
[BuildPhase.EAGER_BUNDLE]: 'Bundle JavaScript',
|
||||
@@ -143,6 +145,7 @@ export const buildPhaseWebsiteId: Record<BuildPhase, string> = {
|
||||
[BuildPhase.EAS_BUILD_INTERNAL]: 'resolve-build-configuration',
|
||||
[BuildPhase.PREBUILD]: 'prebuild',
|
||||
[BuildPhase.PREPARE_CREDENTIALS]: 'prepare-credentials',
|
||||
[BuildPhase.CONFIGURE_ANDROID_VERSION]: 'configure-android-version',
|
||||
[BuildPhase.CALCULATE_EXPO_UPDATES_RUNTIME_VERSION]: 'calculate-expo-updates-runtime-version',
|
||||
[BuildPhase.CONFIGURE_EXPO_UPDATES]: 'configure-expo-updates',
|
||||
[BuildPhase.EAGER_BUNDLE]: 'eager-bundle',
|
||||
|
||||
+511
-497
File diff suppressed because it is too large.
Load diff
@@ -1,17 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core');
|
||||
const path = require('path');
|
||||
(async () => {
|
||||
const oclif = require('@oclif/core');
|
||||
|
||||
const project = path.join(__dirname, '..', 'tsconfig.json');
|
||||
// Keep dev entrypoint failures verbose for local debugging.
|
||||
oclif.settings.debug = true;
|
||||
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
require('ts-node').register({ project });
|
||||
|
||||
// In dev mode, always show stack traces
|
||||
oclif.settings.disableJsonFlag = true
|
||||
|
||||
// Start the CLI
|
||||
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
|
||||
await oclif.execute({ development: true, dir: __dirname });
|
||||
})();
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/core')
|
||||
.run()
|
||||
.then(require('@oclif/core/flush'))
|
||||
.catch(require('@oclif/core/handle'));
|
||||
(async () => {
|
||||
const oclif = require('@oclif/core');
|
||||
await oclif.execute({ dir: __dirname });
|
||||
})();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.0.6",
|
||||
"version": "18.2.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -47,8 +47,8 @@
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/eas-json": "18.0.2",
|
||||
"@expo/eas-build-job": "18.2.0",
|
||||
"@expo/eas-json": "18.2.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "18.0.1",
|
||||
@@ -63,10 +63,10 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.0.2",
|
||||
"@expo/steps": "18.2.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^1.26.2",
|
||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
"@segment/ajv-human-errors": "^2.1.2",
|
||||
"@urql/core": "4.0.11",
|
||||
"@urql/exchange-retry": "1.2.0",
|
||||
|
||||
@@ -99,11 +99,7 @@ describe(BuildVersionGetView, () => {
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
|
||||
const cmd = mockTestCommand(
|
||||
BuildVersionGetView,
|
||||
['--non-interactive', '--json', '--platform=android'],
|
||||
ctx
|
||||
);
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--json', '--platform=android'], ctx);
|
||||
await cmd.run();
|
||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
@@ -126,11 +122,7 @@ describe(BuildVersionGetView, () => {
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
|
||||
const cmd = mockTestCommand(
|
||||
BuildVersionGetView,
|
||||
['--non-interactive', '--json', '--platform=ios'],
|
||||
ctx
|
||||
);
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--json', '--platform=ios'], ctx);
|
||||
await cmd.run();
|
||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
@@ -150,11 +142,7 @@ describe(BuildVersionGetView, () => {
|
||||
});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
|
||||
const cmd = mockTestCommand(
|
||||
BuildVersionGetView,
|
||||
['--non-interactive', '--json', '--platform=android'],
|
||||
ctx
|
||||
);
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--json', '--platform=android'], ctx);
|
||||
await cmd.run();
|
||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
@@ -175,11 +163,7 @@ describe(BuildVersionGetView, () => {
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
|
||||
const cmd = mockTestCommand(
|
||||
BuildVersionGetView,
|
||||
['--non-interactive', '--json', '--platform=android'],
|
||||
ctx
|
||||
);
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--json', '--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
@@ -218,11 +202,7 @@ describe(BuildVersionGetView, () => {
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
|
||||
const cmd = mockTestCommand(
|
||||
BuildVersionGetView,
|
||||
['--non-interactive', '--json', '--platform=android'],
|
||||
ctx
|
||||
);
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--json', '--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowError(
|
||||
`This project is not configured for using remote version source. Add ${chalk.bold(
|
||||
'{"cli": { "appVersionSource": "remote" }}'
|
||||
|
||||
@@ -133,11 +133,23 @@ export function mockTestCommand<T extends Command>(
|
||||
argv: string[],
|
||||
ctx: any
|
||||
): T {
|
||||
const cmd = new commandConstructor(argv, new Config({ root: __dirname }));
|
||||
const config = getMockOclifConfig();
|
||||
const cmd = new commandConstructor(argv, config);
|
||||
(cmd as any).getContextAsync = jest.fn(() => ctx);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
export function getMockOclifConfig(
|
||||
options: Partial<ConstructorParameters<typeof Config>[0]> = {}
|
||||
): Config {
|
||||
const config = new Config({ root: __dirname, ...options });
|
||||
config.runHook = async () => ({
|
||||
failures: [],
|
||||
successes: [],
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
export class NoErrorThrownError extends Error {}
|
||||
|
||||
export const getErrorAsync = async <TError = any>(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { ExitError } from '@oclif/core/lib/errors';
|
||||
import { Errors } from '@oclif/core';
|
||||
import path from 'path';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
@@ -8,6 +8,8 @@ import { validatePNGsForManagedProjectAsync } from '../validate';
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
|
||||
const { ExitError } = Errors;
|
||||
|
||||
describe(validatePNGsForManagedProjectAsync, () => {
|
||||
it('does not validate PNGs for generic projects', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
|
||||
@@ -42,9 +42,9 @@ beforeEach(() => {
|
||||
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
|
||||
});
|
||||
|
||||
const createTestEasCommand = (): typeof EasCommand => {
|
||||
const createTestEasCommand = () => {
|
||||
class TestEasCommand extends EasCommand {
|
||||
async runAsync(): Promise<void> {}
|
||||
override async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'testEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
@@ -109,7 +109,7 @@ describe(EasCommand.name, () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const logErrorSpy = jest.spyOn(Log, 'error');
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
const error = new Error('Unexpected, internal error message');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
throw error;
|
||||
@@ -126,7 +126,7 @@ describe(EasCommand.name, () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const logErrorSpy = jest.spyOn(Log, 'error');
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
const graphQLErrors = ['Unexpected GraphQL error message'];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
@@ -144,7 +144,7 @@ describe(EasCommand.name, () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const logErrorSpy = jest.spyOn(Log, 'error');
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
const graphQLError = new GraphQLError(
|
||||
'Unexpected GraphQL error message',
|
||||
null,
|
||||
@@ -176,7 +176,7 @@ describe(EasCommand.name, () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const logErrorSpy = jest.spyOn(Log, 'error');
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
const graphQLErrors = [
|
||||
new GraphQLError('Unexpected GraphQL error message', null, null, null, null, null, {
|
||||
errorCode: 'UNKNOWN_GRAPHQL_ERROR',
|
||||
@@ -212,7 +212,7 @@ describe(EasCommand.name, () => {
|
||||
|
||||
it('re-throws the error with default base message', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
throw new Error('Error message');
|
||||
});
|
||||
@@ -226,7 +226,7 @@ describe(EasCommand.name, () => {
|
||||
|
||||
it('re-throws the error with a different default base message in case of CombinedError', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
const graphQLErrors = ['Unexpected GraphQL error message'];
|
||||
throw new CombinedError({ graphQLErrors });
|
||||
@@ -241,7 +241,7 @@ describe(EasCommand.name, () => {
|
||||
|
||||
it('re-throws the error with a different default base message in case of CombinedError with multiple GraphQLErrors', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype, 'runAsync');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
const graphQLErrors = [
|
||||
new GraphQLError('Unexpected GraphQL error message', null, null, null, null, null, {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { resolveNonInteractiveAndJsonFlags } from '../flags';
|
||||
|
||||
describe(resolveNonInteractiveAndJsonFlags, () => {
|
||||
test('returns both false when no flags are set', () => {
|
||||
expect(resolveNonInteractiveAndJsonFlags({})).toEqual({
|
||||
json: false,
|
||||
nonInteractive: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('--json implies --non-interactive', () => {
|
||||
expect(resolveNonInteractiveAndJsonFlags({ json: true })).toEqual({
|
||||
json: true,
|
||||
nonInteractive: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('--non-interactive alone does not enable json', () => {
|
||||
expect(resolveNonInteractiveAndJsonFlags({ 'non-interactive': true })).toEqual({
|
||||
json: false,
|
||||
nonInteractive: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('both flags explicitly set', () => {
|
||||
expect(resolveNonInteractiveAndJsonFlags({ json: true, 'non-interactive': true })).toEqual({
|
||||
json: true,
|
||||
nonInteractive: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('both flags explicitly false', () => {
|
||||
expect(resolveNonInteractiveAndJsonFlags({ json: false, 'non-interactive': false })).toEqual({
|
||||
json: false,
|
||||
nonInteractive: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('--json false with --non-interactive true', () => {
|
||||
expect(resolveNonInteractiveAndJsonFlags({ json: false, 'non-interactive': true })).toEqual({
|
||||
json: false,
|
||||
nonInteractive: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,14 +2,23 @@ import { Flags } from '@oclif/core';
|
||||
|
||||
export const EasNonInteractiveAndJsonFlags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
dependsOn: ['non-interactive'],
|
||||
description:
|
||||
'Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.',
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
};
|
||||
|
||||
export function resolveNonInteractiveAndJsonFlags(flags: {
|
||||
json?: boolean;
|
||||
'non-interactive'?: boolean;
|
||||
}): { json: boolean; nonInteractive: boolean } {
|
||||
const json = flags.json ?? false;
|
||||
const nonInteractive = flags['non-interactive'] || json;
|
||||
return { json, nonInteractive };
|
||||
}
|
||||
|
||||
export const EasEnvironmentFlagParameters = {
|
||||
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
|
||||
};
|
||||
@@ -28,28 +37,28 @@ export const EASMultiEnvironmentFlag = {
|
||||
};
|
||||
|
||||
export const EASVariableFormatFlag = {
|
||||
format: Flags.enum({
|
||||
format: Flags.option({
|
||||
description: 'Output format',
|
||||
options: ['long', 'short'],
|
||||
options: ['long', 'short'] as const,
|
||||
default: 'short',
|
||||
}),
|
||||
})(),
|
||||
};
|
||||
|
||||
export const EASVariableVisibilityFlag = {
|
||||
visibility: Flags.enum<'plaintext' | 'sensitive' | 'secret'>({
|
||||
visibility: Flags.option({
|
||||
description: 'Visibility of the variable',
|
||||
options: ['plaintext', 'sensitive', 'secret'],
|
||||
}),
|
||||
options: ['plaintext', 'sensitive', 'secret'] as const,
|
||||
})(),
|
||||
};
|
||||
|
||||
export type EASEnvironmentVariableScopeFlagValue = 'project' | 'account';
|
||||
|
||||
export const EASEnvironmentVariableScopeFlag = {
|
||||
scope: Flags.enum<EASEnvironmentVariableScopeFlagValue>({
|
||||
scope: Flags.option({
|
||||
description: 'Scope for the variable',
|
||||
options: ['project', 'account'],
|
||||
options: ['project', 'account'] as const,
|
||||
default: 'project',
|
||||
}),
|
||||
})(),
|
||||
};
|
||||
|
||||
export const EASNonInteractiveFlag = {
|
||||
@@ -65,6 +74,15 @@ export const EasJsonOnlyFlag = {
|
||||
};
|
||||
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development".',
|
||||
required: false,
|
||||
default: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
export const EasUpdateEnvironmentRequiredFlag = {
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development". Required for projects using Expo SDK 55 or greater.',
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { OptionFlag } from '@oclif/core/lib/interfaces';
|
||||
import { Flags, Interfaces } from '@oclif/core';
|
||||
|
||||
import { EasNonInteractiveAndJsonFlags } from './flags';
|
||||
import { EasNonInteractiveAndJsonFlags, resolveNonInteractiveAndJsonFlags } from './flags';
|
||||
|
||||
export const getPaginatedQueryOptions = (
|
||||
flags: Partial<
|
||||
Record<keyof typeof EasPaginatedQueryFlags | keyof typeof EasNonInteractiveAndJsonFlags, any>
|
||||
>
|
||||
): PaginatedQueryOptions => {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
return {
|
||||
...('limit' in flags && { limit: flags.limit }),
|
||||
offset: flags.offset ?? 0,
|
||||
nonInteractive: flags['non-interactive'] ?? false,
|
||||
json: flags.json ?? false,
|
||||
nonInteractive,
|
||||
json,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const getLimitFlagWithCustomValues = ({
|
||||
}: {
|
||||
defaultTo: number;
|
||||
limit: number;
|
||||
}): OptionFlag<number | undefined> =>
|
||||
}): Interfaces.OptionFlag<number | undefined> =>
|
||||
Flags.integer({
|
||||
description: `The number of items to fetch each query. Defaults to ${defaultTo} and is capped at ${limit}.`,
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { parsedYamlFromWorkflowContents } from '../parse';
|
||||
|
||||
describe('parsedYamlFromWorkflowContents', () => {
|
||||
it('parses YAML workflow contents', () => {
|
||||
const yamlConfig = `
|
||||
jobs:
|
||||
test:
|
||||
type: custom
|
||||
`;
|
||||
|
||||
expect(parsedYamlFromWorkflowContents({ yamlConfig })).toEqual({
|
||||
jobs: {
|
||||
test: {
|
||||
type: 'custom',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('supports shared env merge keys across multiple jobs', () => {
|
||||
const yamlConfig = `
|
||||
shared_env: &shared_env
|
||||
EXPO_TOKEN: token
|
||||
CI: "1"
|
||||
jobs:
|
||||
android:
|
||||
type: custom
|
||||
env:
|
||||
<<: *shared_env
|
||||
PLATFORM: android
|
||||
ios:
|
||||
type: custom
|
||||
env:
|
||||
<<: *shared_env
|
||||
PLATFORM: ios
|
||||
`;
|
||||
|
||||
expect(parsedYamlFromWorkflowContents({ yamlConfig })).toEqual({
|
||||
shared_env: {
|
||||
EXPO_TOKEN: 'token',
|
||||
CI: '1',
|
||||
},
|
||||
jobs: {
|
||||
android: {
|
||||
type: 'custom',
|
||||
env: {
|
||||
EXPO_TOKEN: 'token',
|
||||
CI: '1',
|
||||
PLATFORM: 'android',
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
type: 'custom',
|
||||
env: {
|
||||
EXPO_TOKEN: 'token',
|
||||
CI: '1',
|
||||
PLATFORM: 'ios',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DefaultEnvironment } from '../../build/utils/environment';
|
||||
import { booleanLike, stringLike } from '../../credentials/ios/types';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { parsedYamlFromWorkflowContents } from './parse';
|
||||
|
||||
const inputExtraProperties = {
|
||||
description: stringLike.optional().describe('Description of the input'),
|
||||
@@ -83,7 +83,7 @@ export function parseWorkflowInputsFromYaml(
|
||||
yamlConfig: string
|
||||
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
|
||||
try {
|
||||
const parsed = YAML.parse(yamlConfig);
|
||||
const parsed = parsedYamlFromWorkflowContents({ yamlConfig });
|
||||
return z
|
||||
.record(z.string(), WorkflowDispatchInputZ)
|
||||
.default({})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as YAML from 'yaml';
|
||||
|
||||
export function parsedYamlFromWorkflowContents(workflowFileContents: { yamlConfig: string }): any {
|
||||
return YAML.parse(workflowFileContents.yamlConfig, {
|
||||
// Keep in sync with backend parser options.
|
||||
merge: true,
|
||||
maxAliasCount: 50,
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import Log from '../../log';
|
||||
import { createValidator, getReadableErrors } from '../../metadata/utils/ajv';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
import { parsedYamlFromWorkflowContents } from './parse';
|
||||
|
||||
const jobTypesWithBuildProfile = new Set(['build', 'repack']);
|
||||
|
||||
@@ -162,11 +163,6 @@ function validateWorkflowStructure(parsedYaml: any, workflowJsonSchema: any): vo
|
||||
}
|
||||
}
|
||||
|
||||
export function parsedYamlFromWorkflowContents(workflowFileContents: { yamlConfig: string }): any {
|
||||
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
|
||||
return parsedYaml;
|
||||
}
|
||||
|
||||
export function workflowContentsFromParsedYaml(parsedYaml: any): string {
|
||||
return YAML.stringify(parsedYaml);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -201,13 +202,12 @@ function displayUsage(data: UsageDisplayData, usageData: AccountFullUsageData):
|
||||
export default class AccountUsage extends EasCommand {
|
||||
static override description = 'view account usage and billing for the current cycle';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'ACCOUNT_NAME',
|
||||
static override args = {
|
||||
ACCOUNT_NAME: Args.string({
|
||||
description:
|
||||
'Account name to view usage for. If not provided, the account will be selected interactively (or defaults to the only account if there is just one)',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasJsonOnlyFlag,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import { getAnalyticsEnabledAsync, setAnalyticsEnabledAsync } from '../analytics/AnalyticsManager';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
@@ -5,7 +6,11 @@ import Log from '../log';
|
||||
export default class AnalyticsView extends EasCommand {
|
||||
static override description = 'display or change analytics settings';
|
||||
|
||||
static override args = [{ name: 'STATUS', options: ['on', 'off'] }];
|
||||
static override args = {
|
||||
STATUS: Args.string({
|
||||
options: ['on', 'off'] as const,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { STATUS: status } = (await this.parse(AnalyticsView)).args;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -12,13 +16,12 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
export default class BranchCreate extends EasCommand {
|
||||
static override description = 'create a branch';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the branch to create',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
@@ -33,8 +36,9 @@ export default class BranchCreate extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags,
|
||||
} = await this.parse(BranchCreate);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -61,6 +65,9 @@ export default class BranchCreate extends EasCommand {
|
||||
initial: (await getDefaultBranchNameAsync(vcsClient)) ?? undefined,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
if (!name) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, { appId: projectId, name });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
@@ -5,7 +6,10 @@ import { scheduleBranchDeletionAsync } from '../../branch/delete';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { GetBranchInfoQuery, GetBranchInfoQueryVariables } from '../../graphql/generated';
|
||||
@@ -54,13 +58,12 @@ export default class BranchDelete extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the branch to delete',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
@@ -71,7 +74,7 @@ export default class BranchDelete extends EasCommand {
|
||||
args: { name: branchName },
|
||||
flags,
|
||||
} = await this.parse(BranchDelete);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { listAndRenderBranchesOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
@@ -19,11 +22,12 @@ export default class BranchList extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BranchList);
|
||||
const { nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(BranchList, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
EditUpdateBranchInput,
|
||||
@@ -69,8 +72,9 @@ export default class BranchRename extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
|
||||
flags: { from: currentName, to: newName, ...rawFlags },
|
||||
} = await this.parse(BranchRename);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(rawFlags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
@@ -12,13 +16,12 @@ import { enableJsonOutput } from '../../utils/json';
|
||||
export default class BranchView extends EasCommand {
|
||||
static override description = 'view a branch';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the branch to view',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasPaginatedQueryFlags,
|
||||
@@ -36,7 +39,7 @@ export default class BranchView extends EasCommand {
|
||||
args: { name: branchName },
|
||||
flags,
|
||||
} = await this.parse(BranchView);
|
||||
const { 'non-interactive': nonInteractive } = flags;
|
||||
const { nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -93,15 +93,17 @@ export async function selectBuildToCancelAsync(
|
||||
export default class BuildCancel extends EasCommand {
|
||||
static override description = 'cancel a build';
|
||||
|
||||
static override args = [{ name: 'BUILD_ID' }];
|
||||
static override args = {
|
||||
BUILD_ID: Args.string({}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
description: 'Filter builds by the platform if build ID is not provided',
|
||||
options: Object.values(RequestedPlatform),
|
||||
}),
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description: 'Filter builds by build profile if build ID is not provided',
|
||||
@@ -140,7 +142,7 @@ export default class BuildCancel extends EasCommand {
|
||||
await ensureBuildExistsAsync(graphqlClient, buildIdFromArg);
|
||||
}
|
||||
|
||||
let buildId: string | null = buildIdFromArg;
|
||||
let buildId: string | null = buildIdFromArg ?? null;
|
||||
if (!buildId) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Build ID must be provided in non-interactive mode');
|
||||
|
||||
@@ -19,11 +19,11 @@ export default class BuildConfigure extends EasCommand {
|
||||
static override description = 'configure the project to support EAS Build';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
description: 'Platform to configure',
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
options: ['android', 'ios', 'all'] as const,
|
||||
})(),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -79,14 +79,16 @@ export async function selectBuildToDeleteAsync(
|
||||
|
||||
export default class BuildDelete extends EasCommand {
|
||||
static override description = 'delete a build';
|
||||
static override args = [{ name: 'BUILD_ID' }];
|
||||
static override args = {
|
||||
BUILD_ID: Args.string({}),
|
||||
};
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
description: 'Filter builds by the platform if build ID is not provided',
|
||||
options: Object.values(RequestedPlatform),
|
||||
}),
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description: 'Filter builds by build profile if build ID is not provided',
|
||||
@@ -121,7 +123,7 @@ export default class BuildDelete extends EasCommand {
|
||||
await ensureBuildExistsAsync(graphqlClient, buildIdFromArg);
|
||||
}
|
||||
|
||||
let buildId: string | null = buildIdFromArg;
|
||||
let buildId: string | null = buildIdFromArg ?? null;
|
||||
if (!buildId) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Build ID must be provided in non-interactive mode');
|
||||
|
||||
@@ -34,10 +34,10 @@ export default class BuildDev extends EasCommand {
|
||||
'run dev client simulator/emulator build with matching fingerprint or create a new one';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: [Platform.IOS, Platform.ANDROID],
|
||||
}),
|
||||
options: [Platform.IOS, Platform.ANDROID] as const,
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
|
||||
|
||||
@@ -5,7 +5,10 @@ import { pathExists } from 'fs-extra';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
@@ -23,10 +26,10 @@ export default class Download extends EasCommand {
|
||||
description: 'Fingerprint hash of the build to download',
|
||||
required: true,
|
||||
}),
|
||||
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: [Platform.IOS, Platform.ANDROID],
|
||||
}),
|
||||
options: [Platform.IOS, Platform.ANDROID] as const,
|
||||
})(),
|
||||
'dev-client': Flags.boolean({
|
||||
description: 'Filter only dev-client builds.',
|
||||
allowNo: true,
|
||||
@@ -42,13 +45,13 @@ export default class Download extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
platform,
|
||||
fingerprint,
|
||||
'dev-client': developmentClient,
|
||||
'non-interactive': nonInteractive,
|
||||
...rawFlags
|
||||
},
|
||||
} = await this.parse(Download);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(rawFlags);
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -10,7 +10,10 @@ import path from 'path';
|
||||
import { LocalBuildMode } from '../../build/local';
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { StatuspageServiceName } from '../../graphql/generated';
|
||||
import Log, { link } from '../../log';
|
||||
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
|
||||
@@ -45,10 +48,10 @@ export default class Build extends EasCommand {
|
||||
static override description = 'start a build';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
options: ['android', 'ios', 'all'] as const,
|
||||
})(),
|
||||
'skip-credentials-check': Flags.boolean({
|
||||
default: false,
|
||||
hidden: true,
|
||||
@@ -96,7 +99,7 @@ export default class Build extends EasCommand {
|
||||
description:
|
||||
'Specify the "What to Test" information for the build in TestFlight (iOS-only). To be used with the `auto-submit` flag',
|
||||
}),
|
||||
'resource-class': Flags.enum({
|
||||
'resource-class': Flags.option({
|
||||
options: Object.values(ResourceClass),
|
||||
hidden: true,
|
||||
deprecated: {
|
||||
@@ -105,15 +108,15 @@ export default class Build extends EasCommand {
|
||||
),
|
||||
},
|
||||
description: 'The instance type that will be used to run this build [experimental]',
|
||||
}),
|
||||
})(),
|
||||
message: Flags.string({
|
||||
char: 'm',
|
||||
description: 'A short message describing the build',
|
||||
}),
|
||||
'build-logger-level': Flags.enum({
|
||||
'build-logger-level': Flags.option({
|
||||
description: 'The level of logs to output during the build process. Defaults to "info".',
|
||||
options: Object.values(LoggerLevel),
|
||||
}),
|
||||
})(),
|
||||
'freeze-credentials': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Prevent the build from updating credentials in non-interactive mode',
|
||||
@@ -184,16 +187,13 @@ export default class Build extends EasCommand {
|
||||
private sanitizeFlags(
|
||||
flags: RawBuildFlags
|
||||
): Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (!flags.local && flags.output) {
|
||||
Errors.error('--output is allowed only for local builds', { exit: 1 });
|
||||
}
|
||||
if (!flags.platform && nonInteractive) {
|
||||
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
if (flags.json && !nonInteractive) {
|
||||
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
const autoSubmit = flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined;
|
||||
if (flags['what-to-test'] && !autoSubmit) {
|
||||
Errors.error(
|
||||
@@ -242,7 +242,7 @@ export default class Build extends EasCommand {
|
||||
: {},
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
json: flags['json'],
|
||||
json,
|
||||
autoSubmit,
|
||||
submitProfile: flags['auto-submit-with-profile'] ?? profile,
|
||||
resourceClass: flags['resource-class'],
|
||||
|
||||
@@ -28,23 +28,23 @@ export default class BuildInspect extends EasCommand {
|
||||
'inspect the state of the project at specific build stages, useful for troubleshooting';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: [RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
options: [RequestedPlatform.Android, RequestedPlatform.Ios] as const,
|
||||
required: true,
|
||||
}),
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
stage: Flags.enum({
|
||||
stage: Flags.option({
|
||||
char: 's',
|
||||
description: STAGE_DESCRIPTION,
|
||||
options: [InspectStage.ARCHIVE, InspectStage.PRE_BUILD, InspectStage.POST_BUILD],
|
||||
options: [InspectStage.ARCHIVE, InspectStage.PRE_BUILD, InspectStage.POST_BUILD] as const,
|
||||
required: true,
|
||||
}),
|
||||
})(),
|
||||
output: Flags.string({
|
||||
char: 'o',
|
||||
description: 'Output directory.',
|
||||
|
||||
@@ -18,11 +18,11 @@ export default class BuildInternal extends EasCommand {
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
options: ['android', 'ios'] as const,
|
||||
required: true,
|
||||
}),
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Flags } from '@oclif/core';
|
||||
import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries';
|
||||
import { BuildDistributionType, BuildStatus } from '../../build/types';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
@@ -20,18 +23,18 @@ export default class BuildList extends EasCommand {
|
||||
static override description = 'list all builds for your project';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
options: Object.values(RequestedPlatform),
|
||||
char: 'p',
|
||||
}),
|
||||
status: Flags.enum({
|
||||
})(),
|
||||
status: Flags.option({
|
||||
options: Object.values(BuildStatus),
|
||||
description: 'Filter only builds with the specified status',
|
||||
}),
|
||||
distribution: Flags.enum({
|
||||
})(),
|
||||
distribution: Flags.option({
|
||||
options: Object.values(BuildDistributionType),
|
||||
description: 'Filter only builds with the specified distribution type',
|
||||
}),
|
||||
})(),
|
||||
channel: Flags.string(),
|
||||
'app-version': Flags.string({
|
||||
aliases: ['appVersion'],
|
||||
@@ -83,12 +86,11 @@ export default class BuildList extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildList);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
json: jsonFlag,
|
||||
platform: requestedPlatform,
|
||||
status: buildStatus,
|
||||
distribution: buildDistribution,
|
||||
'non-interactive': nonInteractive,
|
||||
} = flags;
|
||||
if (buildDistribution === BuildDistributionType.SIMULATOR) {
|
||||
Log.warn(
|
||||
|
||||
@@ -13,7 +13,10 @@ import { listAndSelectBuildOnAppAsync } from '../../build/queries';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags } from '../../commandUtils/pagination';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
@@ -66,10 +69,10 @@ export default class BuildResign extends EasCommand {
|
||||
static override description = 're-sign a build archive';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
options: ['android', 'ios'] as const,
|
||||
})(),
|
||||
'target-profile': Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
@@ -242,7 +245,7 @@ export default class BuildResign extends EasCommand {
|
||||
}
|
||||
|
||||
sanitizeFlags(flags: RawBuildResignFlags): BuildResignFlags {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (nonInteractive && !flags.id) {
|
||||
throw new Error(
|
||||
`${chalk.bold('--id')} is required when running with ${chalk.bold(
|
||||
@@ -251,7 +254,7 @@ export default class BuildResign extends EasCommand {
|
||||
);
|
||||
}
|
||||
return {
|
||||
json: flags.json,
|
||||
json,
|
||||
nonInteractive,
|
||||
offset: flags.offset,
|
||||
limit: flags.limit,
|
||||
|
||||
@@ -62,10 +62,10 @@ export default class Run extends EasCommand {
|
||||
description: 'ID of the simulator/emulator build to run',
|
||||
exclusive: ['latest, path, url'],
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
options: ['android', 'ios'] as const,
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
|
||||
@@ -5,7 +5,10 @@ import chalk from 'chalk';
|
||||
|
||||
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { toAppPlatform } from '../../../graphql/types/AppPlatform';
|
||||
import Log from '../../../log';
|
||||
@@ -22,10 +25,10 @@ export default class BuildVersionGetView extends EasCommand {
|
||||
public static override description = 'get the latest version from EAS servers';
|
||||
|
||||
public static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
options: ['android', 'ios', 'all'] as const,
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
@@ -44,7 +47,8 @@ export default class BuildVersionGetView extends EasCommand {
|
||||
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionGetView);
|
||||
if (flags.json) {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const {
|
||||
@@ -57,13 +61,13 @@ export default class BuildVersionGetView extends EasCommand {
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
if (!flags.platform && flags['non-interactive']) {
|
||||
if (!flags.platform && nonInteractive) {
|
||||
throw new Error('"--platform" flag is required in non-interactive mode.');
|
||||
}
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await ensureVersionSourceIsRemoteAsync(easJsonAccessor, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
@@ -94,7 +98,7 @@ export default class BuildVersionGetView extends EasCommand {
|
||||
buildProfile: profile,
|
||||
platform,
|
||||
vcsClient,
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
env,
|
||||
});
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
@@ -107,7 +111,7 @@ export default class BuildVersionGetView extends EasCommand {
|
||||
results[platform] = remoteVersions?.buildVersion;
|
||||
}
|
||||
}
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
const jsonResults: { versionCode?: string; buildNumber?: string } = {};
|
||||
if (results.android) {
|
||||
jsonResults.versionCode = results.android;
|
||||
|
||||
@@ -26,10 +26,10 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
static override description = 'update version of an app';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
options: ['android', 'ios'] as const,
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
|
||||
@@ -45,10 +45,10 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
'update a version in native code with a value stored on EAS servers';
|
||||
|
||||
public static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
options: ['android', 'ios', 'all'] as const,
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
@@ -11,7 +12,9 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
export default class BuildView extends EasCommand {
|
||||
static override description = 'view a build for your project';
|
||||
|
||||
static override args = [{ name: 'BUILD_ID' }];
|
||||
static override args = {
|
||||
BUILD_ID: Args.string({}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasJsonOnlyFlag,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { createAndLinkChannelAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
@@ -10,13 +14,12 @@ import { enableJsonOutput } from '../../utils/json';
|
||||
export default class ChannelCreate extends EasCommand {
|
||||
static override description = 'create a channel';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the channel to create',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
@@ -30,8 +33,9 @@ export default class ChannelCreate extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name: channelName },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags,
|
||||
} = await this.parse(ChannelCreate);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -53,6 +57,9 @@ export default class ChannelCreate extends EasCommand {
|
||||
message: 'Provide a channel name:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
if (!channelName) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
await createAndLinkChannelAsync(graphqlClient, {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import { scheduleChannelDeletionAsync } from '../../channel/delete';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
@@ -11,13 +15,12 @@ import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJ
|
||||
export default class ChannelDelete extends EasCommand {
|
||||
static override description = 'Delete a channel';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the channel to delete',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -30,8 +33,9 @@ export default class ChannelDelete extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { name: nameArg },
|
||||
flags: { json: jsonFlag, 'non-interactive': nonInteractive },
|
||||
flags,
|
||||
} = await this.parse(ChannelDelete);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
UpdateChannelBasicInfoFragment,
|
||||
@@ -58,13 +61,12 @@ export async function updateChannelBranchMappingAsync(
|
||||
export default class ChannelEdit extends EasCommand {
|
||||
static override description = 'point a channel at a new branch';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the channel to edit',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
@@ -81,8 +83,9 @@ export default class ChannelEdit extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args,
|
||||
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
|
||||
flags: { branch: branchFlag, ...rawFlags },
|
||||
} = await this.parse(ChannelEdit);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(rawFlags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EasPaginatedQueryFlags,
|
||||
getLimitFlagWithCustomValues,
|
||||
@@ -25,7 +28,7 @@ export default class ChannelList extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ChannelList);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
@@ -6,7 +6,10 @@ import gql from 'graphql-tag';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
PauseUpdateChannelMutation,
|
||||
@@ -50,13 +53,12 @@ export async function pauseUpdateChannelAsync(
|
||||
export default class ChannelPause extends EasCommand {
|
||||
static override description = 'pause a channel to stop it from sending updates';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the channel to edit',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
@@ -71,10 +73,8 @@ export default class ChannelPause extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args,
|
||||
flags: { json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelPause);
|
||||
const { args, flags } = await this.parse(ChannelPause);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
@@ -6,7 +6,10 @@ import gql from 'graphql-tag';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
ResumeUpdateChannelMutation,
|
||||
@@ -50,13 +53,12 @@ export async function resumeUpdateChannelAsync(
|
||||
export default class ChannelResume extends EasCommand {
|
||||
static override description = 'resume a channel to start sending updates';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the channel to edit',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
@@ -71,10 +73,8 @@ export default class ChannelResume extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args,
|
||||
flags: { json, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ChannelResume);
|
||||
const { args, flags } = await this.parse(ChannelResume);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { NonInteractiveOptions as CreateRolloutNonInteractiveOptions } from '../../rollout/actions/CreateRollout';
|
||||
import { NonInteractiveOptions as EditRolloutNonInteractiveOptions } from '../../rollout/actions/EditRollout';
|
||||
import {
|
||||
@@ -49,15 +52,14 @@ type ChannelRolloutArgsAndFlags = {
|
||||
export default class ChannelRollout extends EasCommand {
|
||||
static override description = 'Roll a new branch out on a channel incrementally.';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'channel',
|
||||
static override args = {
|
||||
channel: Args.string({
|
||||
description: 'channel on which the rollout should be done',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
action: Flags.enum({
|
||||
action: Flags.option({
|
||||
description: 'Rollout action to perform',
|
||||
options: Object.values(ActionRawFlagValue),
|
||||
required: false,
|
||||
@@ -97,17 +99,17 @@ export default class ChannelRollout extends EasCommand {
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})(),
|
||||
percent: Flags.integer({
|
||||
description:
|
||||
'Percent of users to send to the new branch. Use with --action=edit or --action=create',
|
||||
required: false,
|
||||
}),
|
||||
outcome: Flags.enum({
|
||||
outcome: Flags.option({
|
||||
description: 'End outcome of rollout. Use with --action=end',
|
||||
options: Object.values(EndOutcome),
|
||||
required: false,
|
||||
}),
|
||||
})(),
|
||||
branch: Flags.string({
|
||||
description: 'Branch to roll out. Use with --action=create',
|
||||
required: false,
|
||||
@@ -183,8 +185,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
runtimeVersion: rawFlags['runtime-version'],
|
||||
privateKeyPath: rawFlags['private-key-path'] ?? null,
|
||||
action: action ? this.getAction(action) : undefined,
|
||||
nonInteractive: rawFlags['non-interactive'],
|
||||
json: rawFlags.json,
|
||||
...resolveNonInteractiveAndJsonFlags(rawFlags),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
|
||||
import {
|
||||
@@ -5,20 +6,22 @@ import {
|
||||
selectChannelOnAppAsync,
|
||||
} from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
export default class ChannelView extends EasCommand {
|
||||
static override description = 'view a channel';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'name',
|
||||
static override args = {
|
||||
name: Args.string({
|
||||
required: false,
|
||||
description: 'Name of the channel to view',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
@@ -36,7 +39,7 @@ export default class ChannelView extends EasCommand {
|
||||
flags,
|
||||
} = await this.parse(ChannelView);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -6,7 +6,10 @@ import chalk from 'chalk';
|
||||
|
||||
import { evaluateConfigWithEnvVarsAsync } from '../build/evaluateConfigWithEnvVarsAsync';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../commandUtils/flags';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { appPlatformEmojis } from '../platform';
|
||||
@@ -17,7 +20,7 @@ export default class Config extends EasCommand {
|
||||
static override description = 'display project configuration (app.json + eas.json)';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum<Platform>({ char: 'p', options: [Platform.ANDROID, Platform.IOS] }),
|
||||
platform: Flags.option({ char: 'p', options: [Platform.ANDROID, Platform.IOS] as const })(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
@@ -39,14 +42,11 @@ export default class Config extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(Config);
|
||||
if (flags.json) {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const {
|
||||
platform: maybePlatform,
|
||||
profile: maybeProfile,
|
||||
'non-interactive': nonInteractive,
|
||||
} = flags;
|
||||
const { platform: maybePlatform, profile: maybeProfile } = flags;
|
||||
const { getDynamicPublicProjectConfigAsync, projectDir, getDynamicLoggedInAsync } =
|
||||
await this.getContextAsync(Config, {
|
||||
nonInteractive,
|
||||
@@ -80,7 +80,7 @@ export default class Config extends EasCommand {
|
||||
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
|
||||
if (flags['eas-json-only']) {
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ buildProfile: profile });
|
||||
} else {
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
@@ -99,7 +99,7 @@ export default class Config extends EasCommand {
|
||||
opts: { env: profile.env },
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ buildProfile: profile, appConfig });
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -10,10 +10,10 @@ export default class InitializeBuildCredentials extends EasCommand {
|
||||
static override description = 'Set up credentials for building your project.';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: [Platform.ANDROID, Platform.IOS],
|
||||
}),
|
||||
options: [Platform.ANDROID, Platform.IOS] as const,
|
||||
})(),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description: 'The name of the build profile in eas.json.',
|
||||
|
||||
@@ -7,7 +7,7 @@ export default class Credentials extends EasCommand {
|
||||
static override description = 'manage credentials';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
platform: Flags.option({ char: 'p', options: ['android', 'ios'] as const })(),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import Log from '../../../log';
|
||||
import { Ora, ora } from '../../../ora';
|
||||
import { toggleConfirmAsync } from '../../../prompts';
|
||||
@@ -22,7 +26,9 @@ export default class WorkerAliasDelete extends EasCommand {
|
||||
static override aliases = ['worker:alias:delete'];
|
||||
static override state = 'preview';
|
||||
|
||||
static override args = [{ name: 'ALIAS_NAME' }];
|
||||
static override args = {
|
||||
ALIAS_NAME: Args.string({}),
|
||||
};
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -105,9 +111,10 @@ export default class WorkerAliasDelete extends EasCommand {
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: any): DeployAliasDeleteFlags {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
return {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
json: flags['json'],
|
||||
nonInteractive,
|
||||
json,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import { WorkerDeploymentAliasFragment } from '../../../graphql/generated';
|
||||
import Log from '../../../log';
|
||||
import { Ora, ora } from '../../../ora';
|
||||
@@ -163,9 +166,10 @@ export default class WorkerAlias extends EasCommand {
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawDeployAliasFlags): DeployAliasFlags {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
return {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
json: flags['json'],
|
||||
nonInteractive,
|
||||
json,
|
||||
aliasName: flags.alias?.trim().toLowerCase(),
|
||||
deploymentIdentifier: flags.id?.trim().toLowerCase(),
|
||||
isProduction: flags.prod,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { Ora, ora } from '../../ora';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
@@ -23,7 +27,9 @@ export default class WorkerDelete extends EasCommand {
|
||||
static override aliases = ['worker:delete'];
|
||||
static override state = 'preview';
|
||||
|
||||
static override args = [{ name: 'DEPLOYMENT_ID' }];
|
||||
static override args = {
|
||||
DEPLOYMENT_ID: Args.string({}),
|
||||
};
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -104,9 +110,10 @@ export default class WorkerDelete extends EasCommand {
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawDeployDeleteFlags): DeployDeleteFlags {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
return {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
json: flags['json'],
|
||||
nonInteractive,
|
||||
json,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import * as path from 'node:path';
|
||||
|
||||
import { getHostingDeploymentsUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
@@ -48,6 +52,7 @@ interface DeployFlags {
|
||||
deploymentIdentifier?: string;
|
||||
exportDir: string;
|
||||
dryRun: boolean;
|
||||
sourceMaps: boolean;
|
||||
}
|
||||
|
||||
interface RawDeployFlags {
|
||||
@@ -59,6 +64,7 @@ interface RawDeployFlags {
|
||||
id?: string;
|
||||
'export-dir': string;
|
||||
'dry-run': boolean;
|
||||
'source-maps': boolean;
|
||||
}
|
||||
|
||||
interface UploadAssetBatchInstruction {
|
||||
@@ -102,6 +108,11 @@ export default class WorkerDeploy extends EasCommand {
|
||||
description: 'Outputs a tarball of the new deployment instead of uploading it.',
|
||||
default: false,
|
||||
}),
|
||||
'source-maps': Flags.boolean({
|
||||
description: 'Include source maps in the deployment.',
|
||||
default: true,
|
||||
allowNo: true,
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -142,7 +153,9 @@ export default class WorkerDeploy extends EasCommand {
|
||||
yield ['assets.json', JSON.stringify(params.assetMap)];
|
||||
yield ['manifest.json', JSON.stringify(params.manifest)];
|
||||
if (projectDist.type === 'server' && projectDist.serverPath) {
|
||||
const workerFiles = WorkerAssets.listWorkerFilesAsync(projectDist.serverPath);
|
||||
const workerFiles = WorkerAssets.listWorkerFilesAsync(projectDist.serverPath, {
|
||||
skipSourceMaps: !flags.sourceMaps,
|
||||
});
|
||||
for await (const workerFile of workerFiles) {
|
||||
yield [`server/${workerFile.normalizedPath}`, workerFile.data];
|
||||
}
|
||||
@@ -439,15 +452,17 @@ export default class WorkerDeploy extends EasCommand {
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawDeployFlags): DeployFlags {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
return {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
json: flags['json'],
|
||||
nonInteractive,
|
||||
json,
|
||||
isProduction: !!flags.prod,
|
||||
aliasName: flags.alias?.trim().toLowerCase(),
|
||||
deploymentIdentifier: flags.id?.trim(),
|
||||
exportDir: flags['export-dir'],
|
||||
environment: flags['environment'],
|
||||
dryRun: flags['dry-run'],
|
||||
sourceMaps: flags['source-maps'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
|
||||
import formatDevice from '../../devices/utils/formatDevice';
|
||||
@@ -8,7 +9,9 @@ import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
export default class DeviceView extends EasCommand {
|
||||
static override description = 'view a device for your project';
|
||||
|
||||
static override args = [{ name: 'UDID' }];
|
||||
static override args = {
|
||||
UDID: Args.string({}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { getMockAppFragment, getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
@@ -28,7 +26,7 @@ jest.mock('../../../utils/prompts');
|
||||
|
||||
describe(EnvCreate, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const variableId = 'testId';
|
||||
const testAccountId = 'test-account-id';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
@@ -21,7 +20,7 @@ describe(EnvDelete, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const mockContext = {
|
||||
privateProjectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
@@ -17,7 +16,7 @@ jest.mock('../../../log');
|
||||
|
||||
describe(EnvExec, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
const mockEnvironmentVariables = [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
@@ -21,7 +20,7 @@ jest.mock('../../../utils/prompts');
|
||||
|
||||
describe(EnvGet, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const mockVariables: EnvironmentVariableFragment[] = [
|
||||
{
|
||||
id: 'var1',
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { getMockAppFragment, getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
@@ -48,7 +46,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
|
||||
|
||||
describe(EnvList, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
@@ -25,7 +25,7 @@ jest.mock('../../../log');
|
||||
|
||||
describe(EnvPull, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const testProjectDir = '/test/project';
|
||||
const testTargetPath = '.env.local';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
@@ -23,7 +23,7 @@ jest.mock('../../../prompts');
|
||||
|
||||
describe(EnvPush, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const testProjectDir = '/test/project';
|
||||
const testEnvPath = '.env.test';
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { getMockAppFragment, getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
@@ -33,7 +32,7 @@ describe(EnvUpdate, () => {
|
||||
const projectId = 'test-project-id';
|
||||
const variableId = '1';
|
||||
const graphqlClient = {};
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const mockContext = {
|
||||
privateProjectConfig: { projectId },
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
+8
-9
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
@@ -59,14 +59,13 @@ interface CreateFlags {
|
||||
export default class EnvCreate extends EasCommand {
|
||||
static override description = 'create an environment variable for the current project or account';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Environment to create the variable in. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
name: Flags.string({
|
||||
@@ -79,10 +78,10 @@ export default class EnvCreate extends EasCommand {
|
||||
description: 'Overwrite existing variable',
|
||||
default: false,
|
||||
}),
|
||||
type: Flags.enum<'string' | 'file'>({
|
||||
type: Flags.option({
|
||||
description: 'The type of variable',
|
||||
options: ['string', 'file'],
|
||||
}),
|
||||
options: ['string', 'file'] as const,
|
||||
})(),
|
||||
...EASVariableVisibilityFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
|
||||
+5
-6
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -45,14 +45,13 @@ export default class EnvDelete extends EasCommand {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Current environment of the variable to delete. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
|
||||
+7
-8
@@ -1,3 +1,4 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -37,19 +38,17 @@ export default class EnvExec extends EasCommand {
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
required: true,
|
||||
description:
|
||||
"Environment to execute the command in. Default environments are 'production', 'preview', and 'development'.",
|
||||
},
|
||||
{
|
||||
name: 'bash_command',
|
||||
}),
|
||||
bash_command: Args.string({
|
||||
required: true,
|
||||
description: 'bash command to execute with the environment variables from the environment',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
private isNonInteractive: boolean = false;
|
||||
|
||||
|
||||
+5
-6
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -44,14 +44,13 @@ export default class EnvGet extends EasCommand {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Current environment of the variable. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
'variable-name': Flags.string({
|
||||
|
||||
+5
-6
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -106,14 +106,13 @@ export default class EnvList extends EasCommand {
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Environment to list the variables from. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvList);
|
||||
|
||||
+5
-6
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import dotenv from 'dotenv';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
@@ -24,14 +24,13 @@ export default class EnvPull extends EasCommand {
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Environment to pull variables from. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
|
||||
+6
-7
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
@@ -38,14 +38,13 @@ export default class EnvPush extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Environment to push variables to. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvPush);
|
||||
@@ -214,7 +213,7 @@ export default class EnvPush extends EasCommand {
|
||||
environment: string[] | undefined;
|
||||
force: boolean;
|
||||
},
|
||||
{ environment }: Record<string, string>
|
||||
{ environment }: { environment?: string }
|
||||
): { environment?: string[]; path: string; force: boolean } {
|
||||
const environments = flags.environment ?? (environment ? [environment] : undefined);
|
||||
|
||||
|
||||
+8
-9
@@ -1,4 +1,4 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
@@ -79,24 +79,23 @@ export default class EnvUpdate extends EasCommand {
|
||||
value: Flags.string({
|
||||
description: 'New value or the variable',
|
||||
}),
|
||||
type: Flags.enum<'string' | 'file'>({
|
||||
type: Flags.option({
|
||||
description: 'The type of variable',
|
||||
options: ['string', 'file'],
|
||||
}),
|
||||
options: ['string', 'file'] as const,
|
||||
})(),
|
||||
...EASVariableVisibilityFlag,
|
||||
...EASEnvironmentVariableScopeFlag,
|
||||
...EASMultiEnvironmentFlag,
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
static override args = {
|
||||
environment: Args.string({
|
||||
description:
|
||||
"Current environment of the variable to update. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
|
||||
Loaded 100 of 159 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user